diff --git a/browser_tests/tests/dialog.spec.ts b/browser_tests/tests/dialog.spec.ts index 611c0c3c71..cef0cfa12b 100644 --- a/browser_tests/tests/dialog.spec.ts +++ b/browser_tests/tests/dialog.spec.ts @@ -328,9 +328,11 @@ test.describe('Settings', () => { }) await newBlankWorkflowRow.click() - // Click edit button - const editKeybindingButton = newBlankWorkflowRow.locator('.pi-pencil') - await editKeybindingButton.click() + // Click add keybinding button (New Blank Workflow has no default keybinding) + const addKeybindingButton = newBlankWorkflowRow.locator( + '.icon-\\[lucide--plus\\]' + ) + await addKeybindingButton.click() // Set new keybinding const input = comfyPage.page.getByPlaceholder('Enter your keybind') diff --git a/src/components/dialog/content/setting/KeybindingPanel.vue b/src/components/dialog/content/setting/KeybindingPanel.vue index db48f4d502..3fe79fc8f0 100644 --- a/src/components/dialog/content/setting/KeybindingPanel.vue +++ b/src/components/dialog/content/setting/KeybindingPanel.vue @@ -5,101 +5,257 @@ :placeholder="$t('g.searchPlaceholder', { subject: $t('g.keybindings') })" /> - - - - - - - - - - - - - - + @select="ctxChangeKeybinding" + > + + {{ $t('g.changeKeybinding') }} + + + + {{ $t('g.addNewKeybinding') }} + + + + + {{ $t('g.resetToDefault') }} + + + + {{ $t('g.removeKeybinding') }} + + + + @@ -118,16 +274,26 @@ import { FilterMatchMode } from '@primevue/core/api' import Column from 'primevue/column' import DataTable from 'primevue/datatable' import { useToast } from 'primevue/usetoast' -import { computed, ref } from 'vue' +import { + ContextMenuContent, + ContextMenuItem, + ContextMenuPortal, + ContextMenuRoot, + ContextMenuSeparator, + ContextMenuTrigger +} from 'reka-ui' +import { computed, ref, watch } from 'vue' import { useI18n } from 'vue-i18n' -import SearchInput from '@/components/ui/search-input/SearchInput.vue' +import { showConfirmDialog } from '@/components/dialog/confirm/confirmDialog' import Button from '@/components/ui/button/Button.vue' +import SearchInput from '@/components/ui/search-input/SearchInput.vue' import { useEditKeybindingDialog } from '@/composables/useEditKeybindingDialog' import type { KeybindingImpl } from '@/platform/keybindings/keybinding' import { useKeybindingService } from '@/platform/keybindings/keybindingService' import { useKeybindingStore } from '@/platform/keybindings/keybindingStore' import { useCommandStore } from '@/stores/commandStore' +import { useDialogStore } from '@/stores/dialogStore' import { normalizeI18nKey } from '@/utils/formatUtil' import KeyComboDisplay from './keybinding/KeyComboDisplay.vue' @@ -139,11 +305,12 @@ const filters = ref({ const keybindingStore = useKeybindingStore() const keybindingService = useKeybindingService() const commandStore = useCommandStore() +const dialogStore = useDialogStore() const { t } = useI18n() interface ICommandData { id: string - keybinding: KeybindingImpl | null + keybindings: KeybindingImpl[] label: string source?: string } @@ -155,31 +322,166 @@ const commandsData = computed(() => { `commands.${normalizeI18nKey(command.id)}.label`, command.label ?? '' ), - keybinding: keybindingStore.getKeybindingByCommandId(command.id), + keybindings: keybindingStore.getKeybindingsByCommandId(command.id), source: command.source })) }) +const expandedCommandIds = ref>(new Set()) + +const expandedRows = computed({ + get() { + const result: Record = {} + for (const id of expandedCommandIds.value) { + result[id] = true + } + return result + }, + set(value: Record) { + expandedCommandIds.value = new Set(Object.keys(value)) + } +}) + +function toggleExpanded(commandId: string) { + if (expandedCommandIds.value.has(commandId)) { + expandedCommandIds.value.delete(commandId) + } else { + expandedCommandIds.value.add(commandId) + } +} + +watch(filters, () => expandedCommandIds.value.clear(), { deep: true }) + const selectedCommandData = ref(null) const editKeybindingDialog = useEditKeybindingDialog() -function editKeybinding(commandData: ICommandData) { +const contextMenuTarget = ref(null) + +function editKeybinding(commandData: ICommandData, binding: KeybindingImpl) { editKeybindingDialog.show({ commandId: commandData.id, commandLabel: commandData.label, - currentCombo: commandData.keybinding?.combo ?? null + currentCombo: binding.combo, + mode: 'edit', + existingBinding: binding }) } -async function removeKeybinding(commandData: ICommandData) { - if (commandData.keybinding) { - keybindingStore.unsetKeybinding(commandData.keybinding) +function addKeybinding(commandData: ICommandData) { + editKeybindingDialog.show({ + commandId: commandData.id, + commandLabel: commandData.label, + currentCombo: null, + mode: 'add' + }) +} + +function handleRowClick(event: { originalEvent: Event; data: ICommandData }) { + const target = event.originalEvent.target as HTMLElement + if (target.closest('.actions')) return + const commandData = event.data + if ( + commandData.keybindings.length >= 2 || + expandedCommandIds.value.has(commandData.id) + ) { + toggleExpanded(commandData.id) + } +} + +function handleRowDblClick(commandData: ICommandData) { + if (commandData.keybindings.length === 0) { + addKeybinding(commandData) + } else if (commandData.keybindings.length === 1) { + editKeybinding(commandData, commandData.keybindings[0]) + } +} + +function handleRowContextMenu(event: { + originalEvent: Event + data: ICommandData +}) { + contextMenuTarget.value = event.data +} + +function clearContextMenuTarget() { + contextMenuTarget.value = null +} + +async function removeSingleKeybinding( + commandData: ICommandData, + index: number +) { + const binding = commandData.keybindings[index] + if (binding) { + keybindingStore.unsetKeybinding(binding) + if (commandData.keybindings.length <= 2) { + expandedCommandIds.value.delete(commandData.id) + } await keybindingService.persistUserKeybindings() } } +function handleRemoveAllKeybindings(commandData: ICommandData) { + const dialog = showConfirmDialog({ + headerProps: { title: t('g.removeAllKeybindingsTitle') }, + props: { promptText: t('g.removeAllKeybindingsMessage') }, + footerProps: { + confirmText: t('g.removeAll'), + confirmVariant: 'destructive', + onCancel: () => dialogStore.closeDialog(dialog), + onConfirm: async () => { + keybindingStore.removeAllKeybindingsForCommand(commandData.id) + await keybindingService.persistUserKeybindings() + dialogStore.closeDialog(dialog) + } + } + }) +} + +function handleRemoveKeybindingFromMenu(commandData: ICommandData) { + if (commandData.keybindings.length >= 2) { + handleRemoveAllKeybindings(commandData) + } else { + removeSingleKeybinding(commandData, 0) + } +} + +function ctxChangeKeybinding() { + if (!contextMenuTarget.value) return + const target = contextMenuTarget.value + if (target.keybindings.length === 1) { + editKeybinding(target, target.keybindings[0]) + } else if (target.keybindings.length >= 2) { + if (!expandedCommandIds.value.has(target.id)) { + toggleExpanded(target.id) + } + } +} + +function ctxAddKeybinding() { + if (contextMenuTarget.value) { + addKeybinding(contextMenuTarget.value) + } +} + +function ctxResetToDefault() { + if (contextMenuTarget.value) { + resetKeybinding(contextMenuTarget.value) + } +} + +function ctxRemoveKeybinding() { + if ( + contextMenuTarget.value && + contextMenuTarget.value.keybindings.length > 0 + ) { + handleRemoveKeybindingFromMenu(contextMenuTarget.value) + } +} + async function resetKeybinding(commandData: ICommandData) { if (keybindingStore.resetKeybindingForCommand(commandData.id)) { + expandedCommandIds.value.delete(commandData.id) await keybindingService.persistUserKeybindings() } else { console.warn( @@ -189,14 +491,33 @@ async function resetKeybinding(commandData: ICommandData) { } const toast = useToast() -async function resetAllKeybindings() { - keybindingStore.resetAllKeybindings() - await keybindingService.persistUserKeybindings() - toast.add({ - severity: 'info', - summary: 'Info', - detail: 'All keybindings reset', - life: 3000 + +function resetAllKeybindings() { + const dialog = showConfirmDialog({ + headerProps: { + title: t('g.resetAllKeybindingsTitle') + }, + props: { + promptText: t('g.resetAllKeybindingsMessage') + }, + footerProps: { + confirmText: t('g.resetAll'), + confirmVariant: 'destructive', + onCancel: () => { + dialogStore.closeDialog(dialog) + }, + onConfirm: async () => { + keybindingStore.resetAllKeybindings() + await keybindingService.persistUserKeybindings() + dialogStore.closeDialog(dialog) + toast.add({ + severity: 'info', + summary: t('g.info'), + detail: t('g.allKeybindingsReset'), + life: 3000 + }) + } + } }) } diff --git a/src/components/dialog/content/setting/keybinding/EditKeybindingFooter.vue b/src/components/dialog/content/setting/keybinding/EditKeybindingFooter.vue index 77b0fb85ce..012e5fce9c 100644 --- a/src/components/dialog/content/setting/keybinding/EditKeybindingFooter.vue +++ b/src/components/dialog/content/setting/keybinding/EditKeybindingFooter.vue @@ -64,9 +64,18 @@ async function handleSave() { dialogStore.closeDialog({ key: DIALOG_KEY }) - const updated = keybindingStore.updateKeybindingOnCommand( - new KeybindingImpl({ commandId, combo }) - ) - if (updated) await keybindingService.persistUserKeybindings() + if (dialogState.mode === 'add') { + keybindingStore.addUserKeybinding(new KeybindingImpl({ commandId, combo })) + } else if (dialogState.existingBinding) { + keybindingStore.updateSpecificKeybinding( + dialogState.existingBinding, + new KeybindingImpl({ commandId, combo }) + ) + } else { + keybindingStore.updateKeybindingOnCommand( + new KeybindingImpl({ commandId, combo }) + ) + } + await keybindingService.persistUserKeybindings() } diff --git a/src/components/dialog/content/setting/keybinding/KeyComboDisplay.vue b/src/components/dialog/content/setting/keybinding/KeyComboDisplay.vue index 911a0f41b8..6e87002777 100644 --- a/src/components/dialog/content/setting/keybinding/KeyComboDisplay.vue +++ b/src/components/dialog/content/setting/keybinding/KeyComboDisplay.vue @@ -1,7 +1,10 @@