From 48d928fc9ec4ef2c8496a6c41dba71194719fcbc Mon Sep 17 00:00:00 2001 From: Johnpaul Chiwetelu <49923152+Myestery@users.noreply.github.com> Date: Fri, 13 Mar 2026 14:43:33 +0100 Subject: [PATCH] feat: multi-keybinding support in settings panel (#9738) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Adds VS Code-style multi-keybinding support to the Keybinding settings panel. Commands can now have multiple keybindings displayed, expanded, and individually managed. - Fixes #1088 ## Changes ### Store (`keybindingStore.ts`) - `removeAllKeybindingsForCommand(commandId)` — unsets all bindings for a command - `updateSpecificKeybinding(old, new)` — replaces a single binding without affecting others - `resetKeybindingForCommand` — updated to restore **all** default bindings, not just the first - `isCommandKeybindingModified` — updated to compare full sorted sets of bindings ### UI (`KeybindingPanel.vue`) - **Data model**: `keybinding: KeybindingImpl | null` → `keybindings: KeybindingImpl[]` - **Multi-binding display**: shows up to 2 combos inline with `, ` separator, then `+ N more` badge - **Expand/collapse**: click any row with 2+ bindings to expand individual binding rows; chevron-right icon rotates on expand - **Per-binding actions**: edit (pencil), reset, trash on each expanded sub-row - **Parent row actions**: `+`/trash for 2+ bindings, pencil/reset/trash for 1, `+`/disabled for 0 - **Edit modes**: `edit` (replace specific binding via `updateSpecificKeybinding`) and `add` (append via `addUserKeybinding`) - **Right-click context menu**: Change keybinding, Add new, Reset to default, Remove keybinding — with proper disabled states and lucide icons - **Remove all dialog**: confirmation via `showSmallLayoutDialog` with `RemoveAllKeybindingsHeader`/`Content` components - **Reset all dialog**: confirmation via `showConfirmDialog` before resetting all keybindings to defaults - **Double-click**: 0 bindings → add, 1 → edit, 2+ → no-op (single click toggles expand) - **Consistent alignment**: commands without chevron get `pl-5` padding to align with those that have it ### Tests (`keybindingStore.test.ts`) - 7 new tests covering `removeAllKeybindingsForCommand`, `updateSpecificKeybinding`, multi-binding `isCommandKeybindingModified`, and multi-binding `resetKeybindingForCommand` ### i18n (`main.json`) - 11 new keys: removeAllKeybindingsTitle/Message, removeAll, changeKeybinding, addNewKeybinding, resetToDefault, removeKeybinding, nMoreKeybindings, resetAllKeybindingsTitle/Message, allKeybindingsReset ### New components - `RemoveAllKeybindingsHeader.vue` — dialog header - `RemoveAllKeybindingsContent.vue` — dialog body with Close/Remove all buttons ## Test plan - [x] `pnpm typecheck` passes - [x] `pnpm lint` passes (no new errors) - [x] `pnpm vitest run src/platform/keybindings/` — 45 tests pass - [x] CodeRabbit review — 0 findings - [ ] Manual: open Settings → Keybindings, verify multi-binding commands (e.g. Delete Selected Items, Zoom In) show multiple combos - [ ] Manual: click row to expand, verify per-binding actions work - [ ] Manual: right-click row, verify context menu actions - [ ] Manual: click trash on 2+ binding command, verify "Remove all" confirmation dialog - [ ] Manual: click "Reset All" button, verify confirmation dialog appears - [ ] Manual: add/edit/remove individual bindings, verify persistence ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9738-feat-multi-keybinding-support-in-settings-panel-3206d73d365081e9b08bd3cfe21495f1) by [Unito](https://www.unito.io) --- browser_tests/tests/dialog.spec.ts | 8 +- .../content/setting/KeybindingPanel.vue | 543 ++++++++++++++---- .../keybinding/EditKeybindingFooter.vue | 17 +- .../setting/keybinding/KeyComboDisplay.vue | 5 +- src/composables/useEditKeybindingDialog.ts | 9 +- src/locales/en/main.json | 11 + .../keybindings/keybindingStore.test.ts | 142 +++++ src/platform/keybindings/keybindingStore.ts | 73 ++- 8 files changed, 664 insertions(+), 144 deletions(-) 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') })" /> - - - - - - - - - - - - - - - - - - - + + + - - {{ slotProps.data.label }} - - - - - - + + + + + {{ slotProps.data.label }} + + + + + + + + , + + + + {{ + $t('g.nMoreKeybindings', { + count: slotProps.data.keybindings.length - 2 + }) + }} + + + - + + + + + {{ + slotProps.data.source || '-' + }} + + + + + + + + + + + + + + + + + + + + + + + + + {{ + slotProps.data.label + }} + + + + + + + + + + + + + + + + + + + - - - - - - - {{ - slotProps.data.source || '-' - }} - - - + @select="ctxChangeKeybinding" + > + + {{ $t('g.changeKeybinding') }} + + + + {{ $t('g.addNewKeybinding') }} + + + + + {{ $t('g.resetToDefault') }} + + + + {{ $t('g.removeKeybinding') }} + + + + - + {{ $t('g.resetAll') }} @@ -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 @@ - + {{ sequence }} + diff --git a/src/composables/useEditKeybindingDialog.ts b/src/composables/useEditKeybindingDialog.ts index 4cb367d891..82b7fff97c 100644 --- a/src/composables/useEditKeybindingDialog.ts +++ b/src/composables/useEditKeybindingDialog.ts @@ -4,6 +4,7 @@ import EditKeybindingContent from '@/components/dialog/content/setting/keybindin import EditKeybindingFooter from '@/components/dialog/content/setting/keybinding/EditKeybindingFooter.vue' import EditKeybindingHeader from '@/components/dialog/content/setting/keybinding/EditKeybindingHeader.vue' import type { KeyComboImpl } from '@/platform/keybindings/keyCombo' +import type { KeybindingImpl } from '@/platform/keybindings/keybinding' import { useKeybindingStore } from '@/platform/keybindings/keybindingStore' import { useDialogService } from '@/services/dialogService' @@ -13,6 +14,8 @@ export interface EditKeybindingDialogState { commandId: string newCombo: KeyComboImpl | null currentCombo: KeyComboImpl | null + mode: 'edit' | 'add' + existingBinding: KeybindingImpl | null } export function useEditKeybindingDialog() { @@ -23,11 +26,15 @@ export function useEditKeybindingDialog() { commandId: string commandLabel: string currentCombo: KeyComboImpl | null + mode?: 'edit' | 'add' + existingBinding?: KeybindingImpl | null }) { const dialogState = reactive({ commandId: options.commandId, newCombo: options.currentCombo, - currentCombo: options.currentCombo + currentCombo: options.currentCombo, + mode: options.mode ?? 'edit', + existingBinding: options.existingBinding ?? null }) const existingKeybindingOnCombo = computed(() => { diff --git a/src/locales/en/main.json b/src/locales/en/main.json index 0447fc85ea..6a280c636e 100644 --- a/src/locales/en/main.json +++ b/src/locales/en/main.json @@ -69,6 +69,17 @@ "resetAll": "Reset All", "clearFilters": "Clear Filters", "resetAllKeybindingsTooltip": "Reset all keybindings to default", + "resetAllKeybindingsTitle": "Reset all keybindings?", + "resetAllKeybindingsMessage": "All keybindings will be reset to their default values. This cannot be undone.", + "allKeybindingsReset": "All keybindings reset to default", + "removeAllKeybindingsTitle": "Remove all keybindings?", + "removeAllKeybindingsMessage": "All keybindings for this command will be removed", + "removeAll": "Remove all", + "changeKeybinding": "Change keybinding", + "addNewKeybinding": "Add new keybinding", + "resetToDefault": "Reset to default", + "removeKeybinding": "Remove keybinding", + "nMoreKeybindings": "+ {count} more", "customizeFolder": "Customize Folder", "icon": "Icon", "color": "Color", diff --git a/src/platform/keybindings/keybindingStore.test.ts b/src/platform/keybindings/keybindingStore.test.ts index 15f16444f5..3ce4c195ba 100644 --- a/src/platform/keybindings/keybindingStore.test.ts +++ b/src/platform/keybindings/keybindingStore.test.ts @@ -408,4 +408,146 @@ describe('useKeybindingStore', () => { defaultKeybinding ) }) + + describe('removeAllKeybindingsForCommand', () => { + it('should return false when command has no bindings', () => { + const store = useKeybindingStore() + expect(store.removeAllKeybindingsForCommand('nonexistent.command')).toBe( + false + ) + }) + + it('should remove all bindings for a command with multiple bindings', () => { + const store = useKeybindingStore() + const binding1 = new KeybindingImpl({ + commandId: 'test.command', + combo: { key: 'A', ctrl: true } + }) + const binding2 = new KeybindingImpl({ + commandId: 'test.command', + combo: { key: 'B', ctrl: true } + }) + + store.addDefaultKeybinding(binding1) + store.addUserKeybinding(binding2) + expect(store.getKeybindingsByCommandId('test.command')).toHaveLength(2) + + const result = store.removeAllKeybindingsForCommand('test.command') + + expect(result).toBe(true) + expect(store.getKeybindingsByCommandId('test.command')).toHaveLength(0) + }) + }) + + describe('updateSpecificKeybinding', () => { + it('should replace a specific binding with a new one', () => { + const store = useKeybindingStore() + const binding1 = new KeybindingImpl({ + commandId: 'test.command', + combo: { key: 'A', ctrl: true } + }) + const binding2 = new KeybindingImpl({ + commandId: 'test.command', + combo: { key: 'B', ctrl: true } + }) + + store.addUserKeybinding(binding1) + store.addUserKeybinding(binding2) + expect(store.getKeybindingsByCommandId('test.command')).toHaveLength(2) + + const newBinding = new KeybindingImpl({ + commandId: 'test.command', + combo: { key: 'C', alt: true } + }) + store.updateSpecificKeybinding(binding1, newBinding) + + const bindings = store.getKeybindingsByCommandId('test.command') + expect(bindings).toHaveLength(2) + expect(bindings.some((b) => b.combo.equals(newBinding.combo))).toBe(true) + expect(bindings.some((b) => b.combo.equals(binding1.combo))).toBe(false) + }) + }) + + describe('isCommandKeybindingModified (multi-binding)', () => { + it('should detect modification when binding count differs', () => { + const store = useKeybindingStore() + const defaultBinding = new KeybindingImpl({ + commandId: 'test.command', + combo: { key: 'A', ctrl: true } + }) + store.addDefaultKeybinding(defaultBinding) + + const extraBinding = new KeybindingImpl({ + commandId: 'test.command', + combo: { key: 'B', ctrl: true } + }) + store.addUserKeybinding(extraBinding) + + expect(store.isCommandKeybindingModified('test.command')).toBe(true) + }) + + it('should return false when multi-binding matches defaults', () => { + const store = useKeybindingStore() + const binding1 = new KeybindingImpl({ + commandId: 'test.command', + combo: { key: 'A', ctrl: true } + }) + const binding2 = new KeybindingImpl({ + commandId: 'test.command', + combo: { key: 'B', ctrl: true } + }) + + store.addDefaultKeybinding(binding1) + store.addDefaultKeybinding(binding2) + + expect(store.isCommandKeybindingModified('test.command')).toBe(false) + }) + }) + + describe('resetKeybindingForCommand (multi-binding)', () => { + it('should restore all default bindings when user has modified multi-binding command', () => { + const store = useKeybindingStore() + const defaultBinding1 = new KeybindingImpl({ + commandId: 'test.command', + combo: { key: 'Delete' } + }) + const defaultBinding2 = new KeybindingImpl({ + commandId: 'test.command', + combo: { key: 'Backspace' } + }) + + store.addDefaultKeybinding(defaultBinding1) + store.addDefaultKeybinding(defaultBinding2) + expect(store.getKeybindingsByCommandId('test.command')).toHaveLength(2) + + store.unsetKeybinding(defaultBinding1) + expect(store.getKeybindingsByCommandId('test.command')).toHaveLength(1) + + const result = store.resetKeybindingForCommand('test.command') + + expect(result).toBe(true) + expect(store.getKeybindingsByCommandId('test.command')).toHaveLength(2) + }) + + it('should remove all user bindings when no defaults exist for multi-binding', () => { + const store = useKeybindingStore() + const userBinding1 = new KeybindingImpl({ + commandId: 'test.command', + combo: { key: 'X', ctrl: true } + }) + const userBinding2 = new KeybindingImpl({ + commandId: 'test.command', + combo: { key: 'Y', ctrl: true } + }) + + store.addUserKeybinding(userBinding1) + store.addUserKeybinding(userBinding2) + expect(store.getKeybindingsByCommandId('test.command')).toHaveLength(2) + + const result = store.resetKeybindingForCommand('test.command') + + expect(result).toBe(true) + expect(store.getKeybindingsByCommandId('test.command')).toHaveLength(0) + }) + }) }) diff --git a/src/platform/keybindings/keybindingStore.ts b/src/platform/keybindings/keybindingStore.ts index 84b0f271b1..3c4c0910ad 100644 --- a/src/platform/keybindings/keybindingStore.ts +++ b/src/platform/keybindings/keybindingStore.ts @@ -144,47 +144,70 @@ export const useKeybindingStore = defineStore('keybinding', () => { userUnsetKeybindings.value = {} } - function resetKeybindingForCommand(commandId: string): boolean { - const currentKeybinding = getKeybindingByCommandId(commandId) - const defaultKeybinding = - defaultKeybindingsByCommandId.value[commandId]?.[0] + function removeAllKeybindingsForCommand(commandId: string): boolean { + const currentBindings = getKeybindingsByCommandId(commandId) + if (currentBindings.length === 0) return false + for (const binding of currentBindings) { + unsetKeybinding(binding) + } + return true + } - if (!defaultKeybinding) { - if (currentKeybinding) { - unsetKeybinding(currentKeybinding) + function updateSpecificKeybinding( + oldBinding: KeybindingImpl, + newBinding: KeybindingImpl + ) { + unsetKeybinding(oldBinding) + addUserKeybinding(newBinding) + } + + function resetKeybindingForCommand(commandId: string): boolean { + const currentBindings = getKeybindingsByCommandId(commandId) + const defaultBindings = defaultKeybindingsByCommandId.value[commandId] ?? [] + + if (defaultBindings.length === 0) { + if (currentBindings.length > 0) { + for (const binding of currentBindings) { + unsetKeybinding(binding) + } return true } return false } - if (currentKeybinding?.equals(defaultKeybinding)) { + if (!isCommandKeybindingModified(commandId)) { return false } - if (currentKeybinding) { - unsetKeybinding(currentKeybinding) + for (const binding of currentBindings) { + unsetKeybinding(binding) } - const serializedCombo = defaultKeybinding.combo.serialize() - if ( - userUnsetKeybindings.value[serializedCombo]?.equals(defaultKeybinding) - ) { - delete userUnsetKeybindings.value[serializedCombo] + for (const defaultBinding of defaultBindings) { + const serializedCombo = defaultBinding.combo.serialize() + if (userUnsetKeybindings.value[serializedCombo]?.equals(defaultBinding)) { + delete userUnsetKeybindings.value[serializedCombo] + } } return true } function isCommandKeybindingModified(commandId: string): boolean { - const currentKeybinding: KeybindingImpl | undefined = - getKeybindingByCommandId(commandId) - const defaultKeybinding: KeybindingImpl | undefined = - defaultKeybindingsByCommandId.value[commandId]?.[0] + const currentBindings = getKeybindingsByCommandId(commandId) + const defaultBindings = defaultKeybindingsByCommandId.value[commandId] ?? [] - return !( - (currentKeybinding === undefined && defaultKeybinding === undefined) || - currentKeybinding?.equals(defaultKeybinding) - ) + if (currentBindings.length !== defaultBindings.length) return true + if (currentBindings.length === 0) return false + + const sortedCurrent = [...currentBindings] + .map((b) => b.combo.serialize()) + .sort() + const sortedDefault = [...defaultBindings] + .map((b) => b.combo.serialize()) + .sort() + + return sortedCurrent.some((combo, i) => combo !== sortedDefault[i]) } return { @@ -200,6 +223,8 @@ export const useKeybindingStore = defineStore('keybinding', () => { updateKeybindingOnCommand, resetAllKeybindings, resetKeybindingForCommand, - isCommandKeybindingModified + isCommandKeybindingModified, + removeAllKeybindingsForCommand, + updateSpecificKeybinding } })