Compare commits

...

2 Commits

Author SHA1 Message Date
bymyself
a476be3933 [refactor] Extract keybinding functionality into @comfyorg/keybinding package
Create framework-agnostic keybinding package following domain-driven design patterns.
Move pure business logic to package while keeping Vue integration in workbench layer.

Changes:
- Add @comfyorg/keybinding package with KeyComboImpl and KeybindingImpl classes
- Move core keybindings and reserved key constants to package
- Update workbench layer to import from package with backward compatibility
- Update all imports across codebase to use package exports
- Maintain existing API surface for consumers
2025-10-12 20:51:31 -07:00
bymyself
42ffdb2141 refactor keybinding files 2025-09-30 11:55:27 -07:00
34 changed files with 509 additions and 145 deletions

View File

@@ -1,3 +1,4 @@
import type { KeyCombo } from '@comfyorg/keybinding'
import type { APIRequestContext, Locator, Page } from '@playwright/test'
import { expect } from '@playwright/test'
import { test as base } from '@playwright/test'
@@ -6,7 +7,6 @@ import * as fs from 'fs'
import type { LGraphNode } from '../../src/lib/litegraph/src/litegraph'
import type { NodeId } from '../../src/platform/workflow/validation/schemas/workflowSchema'
import type { KeyCombo } from '../../src/schemas/keyBindingSchema'
import type { useWorkspaceStore } from '../../src/stores/workspaceStore'
import { NodeBadgeMode } from '../../src/types/nodeSource'
import { ComfyActionbar } from '../helpers/actionbar'

View File

@@ -1,7 +1,7 @@
import type { Keybinding } from '@comfyorg/keybinding'
import type { Locator } from '@playwright/test'
import { expect } from '@playwright/test'
import type { Keybinding } from '../../src/schemas/keyBindingSchema'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {

View File

@@ -0,0 +1,134 @@
# Keybinding Domain DDD Refactoring
## Overview
This document outlines the refactoring of ComfyUI's keybinding functionality from technical layers to domain-driven design (DDD) following VSCode's proven three-layer architecture.
## Architecture
### Three-Layer Design
Following VSCode's enterprise-grade architecture:
1. **Base Layer** - Foundational utilities (no dependencies)
2. **Platform Layer** - Core abstractions (reusable across frontends)
3. **Workbench Layer** - UI-specific implementation
### Final Structure
```
src/
├── base/keybinding/ # Base Layer
│ └── reservedKeyCombos.ts # Reserved key combinations
├── platform/keybinding/ # Platform Layer
│ ├── constants/
│ │ └── coreKeybindings.ts # Core keybinding definitions
│ └── types/
│ └── keybinding.ts # Schema types and interfaces
└── workbench/keybindings/ # Workbench Layer
├── components/ # UI Components
│ ├── KeybindingPanel.vue # Settings panel
│ ├── KeyComboDisplay.vue # Key display component
│ └── shortcuts/ # Shortcuts panel
│ ├── EssentialsPanel.vue
│ ├── ShortcutsList.vue
│ └── ViewControlsPanel.vue
├── composables/
│ └── useShortcutsTab.ts # UI logic
├── services/
│ └── keybindingService.ts # Service implementation
└── stores/
└── keybindingStore.ts # State management
```
## Migration Summary
### Before (Technical Layers)
- `services/keybindingService.ts`
- `stores/keybindingStore.ts`
- `constants/coreKeybindings.ts`
- `schemas/keyBindingSchema.ts`
- `components/dialog/content/setting/KeybindingPanel.vue`
- `components/bottomPanel/tabs/shortcuts/`
- `composables/bottomPanelTabs/useShortcutsTab.ts`
### After (Domain-Driven)
All keybinding functionality organized by architectural layer and domain responsibility.
## Key Import Paths
```typescript
// Platform types and constants
import type { Keybinding, KeyCombo } from '@/platform/keybinding/types/keybinding'
import { CORE_KEYBINDINGS } from '@/platform/keybinding/constants/coreKeybindings'
// Base utilities
import { RESERVED_BY_TEXT_INPUT } from '@/base/keybinding/reservedKeyCombos'
// Workbench services
import { useKeybindingService } from '@/workbench/keybindings/services/keybindingService'
import {
KeyComboImpl,
KeybindingImpl,
useKeybindingStore
} from '@/workbench/keybindings/stores/keybindingStore'
// UI components
import KeybindingPanel from '@/workbench/keybindings/components/KeybindingPanel.vue'
import { useShortcutsTab } from '@/workbench/keybindings/composables/useShortcutsTab'
```
## Benefits
### 1. Clear Architectural Boundaries
- **Base**: Reusable across any JavaScript environment
- **Platform**: Reusable across any UI framework
- **Workbench**: Vue/PrimeVue specific implementation
### 2. Frontend Flexibility
The base + platform layers enable building alternative frontends (React, Angular, etc.) while reusing core keybinding logic.
### 3. VSCode Alignment
Follows the same proven patterns used by one of the most successful code editors, ensuring scalability and maintainability.
### 4. Domain Cohesion
All keybinding-related functionality is now located together, making it easier to:
- Find related code
- Make changes across the domain
- Test domain functionality
- Understand feature scope
### 5. Dependency Management
Clear dependency flow: `base/``platform/``workbench/`
## Migration Process
1.**Analysis**: Identified all keybinding-related files
2.**Structure Creation**: Built three-layer directory structure
3.**File Migration**: Moved files to appropriate layers
4.**Import Updates**: Updated all import paths using `@` aliases
5.**Testing**: Verified TypeScript compilation and linting
6.**Cleanup**: Removed old files and empty directories
## Quality Assurance
- **TypeScript**: `pnpm typecheck` passes
- **Code Quality**: `pnpm lint --fix` applied
- **Testing**: All test imports updated
- **No Breaking Changes**: All functionality preserved
## Future Considerations
### Potential Enhancements
- Add platform-level abstractions as needed
- Create additional keybinding editor components
- Consider splitting large components into smaller, focused ones
### Extension Points
- New UI components can be added to `workbench/keybindings/components/`
- Platform-level services can be extended in `platform/keybinding/`
- Base utilities can be enhanced in `base/keybinding/`
This refactoring establishes a solid foundation for future keybinding feature development while maintaining backward compatibility and improving code organization.

View File

@@ -107,6 +107,7 @@
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
"@comfyorg/comfyui-electron-types": "0.4.73-0",
"@comfyorg/design-system": "workspace:*",
"@comfyorg/keybinding": "workspace:*",
"@comfyorg/registry-types": "workspace:*",
"@comfyorg/tailwind-utils": "workspace:*",
"@iconify/json": "^2.2.380",

View File

@@ -0,0 +1,29 @@
{
"name": "@comfyorg/keybinding",
"version": "1.0.0",
"type": "module",
"description": "Framework-agnostic keybinding system for ComfyUI",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": {
"import": "./src/index.ts",
"types": "./src/index.ts"
}
},
"scripts": {
"typecheck": "tsc --noEmit"
},
"nx": {
"tags": [
"scope:shared",
"type:lib"
]
},
"dependencies": {
"zod": "^3.22.4"
},
"devDependencies": {
"typescript": "^5.4.5"
}
}

View File

@@ -1,4 +1,4 @@
import type { Keybinding } from '@/schemas/keyBindingSchema'
import type { Keybinding } from '../types/keybinding'
export const CORE_KEYBINDINGS: Keybinding[] = [
{

View File

@@ -0,0 +1,11 @@
// Types
export type * from './types/keybinding'
export { zKeybinding } from './types/keybinding'
// Models (Implementation classes)
export { KeyComboImpl } from './models/KeyCombo'
export { KeybindingImpl } from './models/Keybinding'
// Constants
export { CORE_KEYBINDINGS } from './constants/coreKeybindings'
export { RESERVED_BY_TEXT_INPUT } from './constants/reservedKeyCombos'

View File

@@ -0,0 +1,83 @@
import { RESERVED_BY_TEXT_INPUT } from '../constants/reservedKeyCombos'
import type { KeyCombo } from '../types/keybinding'
export class KeyComboImpl implements KeyCombo {
key: string
// ctrl or meta(cmd on mac)
ctrl: boolean
alt: boolean
shift: boolean
constructor(obj: KeyCombo) {
this.key = obj.key
this.ctrl = obj.ctrl ?? false
this.alt = obj.alt ?? false
this.shift = obj.shift ?? false
}
static fromEvent(event: KeyboardEvent) {
return new KeyComboImpl({
key: event.key,
ctrl: event.ctrlKey || event.metaKey,
alt: event.altKey,
shift: event.shiftKey
})
}
equals(other: unknown): boolean {
return other instanceof KeyComboImpl
? this.key.toUpperCase() === other.key.toUpperCase() &&
this.ctrl === other.ctrl &&
this.alt === other.alt &&
this.shift === other.shift
: false
}
serialize(): string {
return `${this.key.toUpperCase()}:${this.ctrl}:${this.alt}:${this.shift}`
}
toString(): string {
return this.getKeySequences().join(' + ')
}
get hasModifier(): boolean {
return this.ctrl || this.alt || this.shift
}
get isModifier(): boolean {
return ['Control', 'Meta', 'Alt', 'Shift'].includes(this.key)
}
get modifierCount(): number {
const modifiers = [this.ctrl, this.alt, this.shift]
return modifiers.reduce((acc, cur) => acc + Number(cur), 0)
}
get isShiftOnly(): boolean {
return this.shift && this.modifierCount === 1
}
get isReservedByTextInput(): boolean {
return (
!this.hasModifier ||
this.isShiftOnly ||
RESERVED_BY_TEXT_INPUT.has(this.toString())
)
}
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
}
}

View File

@@ -0,0 +1,22 @@
import type { Keybinding } from '../types/keybinding'
import { KeyComboImpl } from './KeyCombo'
export class KeybindingImpl implements Keybinding {
commandId: string
combo: KeyComboImpl
targetElementId?: string
constructor(obj: Keybinding) {
this.commandId = obj.commandId
this.combo = new KeyComboImpl(obj.combo)
this.targetElementId = obj.targetElementId
}
equals(other: unknown): boolean {
return other instanceof KeybindingImpl
? this.commandId === other.commandId &&
this.combo.equals(other.combo) &&
this.targetElementId === other.targetElementId
: false
}
}

View File

@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
"declaration": true,
"declarationMap": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

13
pnpm-lock.yaml generated
View File

@@ -20,6 +20,9 @@ importers:
'@comfyorg/design-system':
specifier: workspace:*
version: link:packages/design-system
'@comfyorg/keybinding':
specifier: workspace:*
version: link:packages/keybinding
'@comfyorg/registry-types':
specifier: workspace:*
version: link:packages/registry-types
@@ -371,6 +374,16 @@ importers:
specifier: ^5.4.5
version: 5.9.2
packages/keybinding:
dependencies:
zod:
specifier: ^3.22.4
version: 3.24.1
devDependencies:
typescript:
specifier: ^5.4.5
version: 5.9.2
packages/registry-types: {}
packages/shared-frontend-utils:

View File

@@ -38,10 +38,10 @@ import ExtensionSlot from '@/components/common/ExtensionSlot.vue'
import SidebarBottomPanelToggleButton from '@/components/sidebar/SidebarBottomPanelToggleButton.vue'
import SidebarShortcutsToggleButton from '@/components/sidebar/SidebarShortcutsToggleButton.vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useKeybindingStore } from '@/stores/keybindingStore'
import { useUserStore } from '@/stores/userStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import type { SidebarTabExtension } from '@/types/extensionTypes'
import { useKeybindingStore } from '@/workbench/keybindings/stores/keybindingStore'
import SidebarHelpCenterIcon from './SidebarHelpCenterIcon.vue'
import SidebarIcon from './SidebarIcon.vue'

View File

@@ -101,7 +101,7 @@ export function useSettingUI(
children: []
},
component: defineAsyncComponent(
() => import('@/components/dialog/content/setting/KeybindingPanel.vue')
() => import('@/workbench/keybindings/components/KeybindingPanel.vue')
)
}

View File

@@ -1,8 +1,9 @@
import type { Keybinding } from '@comfyorg/keybinding'
import { LinkMarkerShape, LiteGraph } from '@/lib/litegraph/src/litegraph'
import { useSettingStore } from '@/platform/settings/settingStore'
import type { SettingParams } from '@/platform/settings/types'
import type { ColorPalettes } from '@/schemas/colorPaletteSchema'
import type { Keybinding } from '@/schemas/keyBindingSchema'
import { NodeBadgeMode } from '@/types/nodeSource'
import { LinkReleaseTriggerAction } from '@/types/searchBoxTypes'

View File

@@ -1,3 +1,4 @@
import { zKeybinding } from '@comfyorg/keybinding'
import { z } from 'zod'
import { LinkMarkerShape } from '@/lib/litegraph/src/litegraph'
@@ -6,7 +7,6 @@ import {
zNodeId
} from '@/platform/workflow/validation/schemas/workflowSchema'
import { colorPalettesSchema } from '@/schemas/colorPaletteSchema'
import { zKeybinding } from '@/schemas/keyBindingSchema'
import { NodeBadgeMode } from '@/types/nodeSource'
import { LinkReleaseTriggerAction } from '@/types/searchBoxTypes'

View File

@@ -1,3 +1,4 @@
import { KeyComboImpl } from '@comfyorg/keybinding'
import _ from 'es-toolkit/compat'
import type { ToastMessageOptions } from 'primevue/toast'
import { reactive } from 'vue'
@@ -53,7 +54,6 @@ import { useExecutionStore } from '@/stores/executionStore'
import { useExtensionStore } from '@/stores/extensionStore'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
import { KeyComboImpl, useKeybindingStore } from '@/stores/keybindingStore'
import { useModelStore } from '@/stores/modelStore'
import { SYSTEM_NODE_DEFS, useNodeDefStore } from '@/stores/nodeDefStore'
import { useSubgraphStore } from '@/stores/subgraphStore'
@@ -81,6 +81,7 @@ import {
} from '@/utils/migration/migrateReroute'
import { getSelectedModelsMetadata } from '@/utils/modelMetadataUtil'
import { deserialiseAndCreate } from '@/utils/vintageClipboard'
import { useKeybindingStore } from '@/workbench/keybindings/stores/keybindingStore'
import { type ComfyApi, PromptExecutionError, api } from './api'
import { defaultGraph } from './defaultGraph'

View File

@@ -5,11 +5,14 @@ import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { useCommandStore } from '@/stores/commandStore'
import { useExtensionStore } from '@/stores/extensionStore'
import { KeybindingImpl, useKeybindingStore } from '@/stores/keybindingStore'
import { useMenuItemStore } from '@/stores/menuItemStore'
import { useWidgetStore } from '@/stores/widgetStore'
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
import type { ComfyExtension } from '@/types/comfy'
import {
KeybindingImpl,
useKeybindingStore
} from '@/workbench/keybindings/stores/keybindingStore'
export const useExtensionService = () => {
const extensionStore = useExtensionStore()

View File

@@ -3,8 +3,10 @@ import { computed, ref } from 'vue'
import { useErrorHandling } from '@/composables/useErrorHandling'
import type { ComfyExtension } from '@/types/comfy'
import { type KeybindingImpl, useKeybindingStore } from './keybindingStore'
import {
type KeybindingImpl,
useKeybindingStore
} from '@/workbench/keybindings/stores/keybindingStore'
export interface ComfyCommand {
id: string

View File

@@ -1,7 +1,6 @@
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import { useShortcutsTab } from '@/composables/bottomPanelTabs/useShortcutsTab'
import {
useCommandTerminalTab,
useLogsTerminalTab
@@ -10,6 +9,7 @@ import { useCommandStore } from '@/stores/commandStore'
import type { ComfyExtension } from '@/types/comfy'
import type { BottomPanelExtension } from '@/types/extensionTypes'
import { isElectron } from '@/utils/envUtil'
import { useShortcutsTab } from '@/workbench/keybindings/composables/useShortcutsTab'
type PanelType = 'terminal' | 'shortcuts'

View File

@@ -1,8 +1,9 @@
import type { Keybinding } from '@comfyorg/keybinding'
import type { Positionable } from '@/lib/litegraph/src/interfaces'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { SettingParams } from '@/platform/settings/types'
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
import type { Keybinding } from '@/schemas/keyBindingSchema'
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
import type { ComfyApp } from '@/scripts/app'
import type { ComfyWidgetConstructor } from '@/scripts/widgets'

View File

@@ -53,7 +53,6 @@ import type { StatusWsMessageStatus } from '@/schemas/apiSchema'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { setupAutoQueueHandler } from '@/services/autoQueueService'
import { useKeybindingService } from '@/services/keybindingService'
import { useCommandStore } from '@/stores/commandStore'
import { useExecutionStore } from '@/stores/executionStore'
import { useMenuItemStore } from '@/stores/menuItemStore'
@@ -69,6 +68,7 @@ import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { electronAPI, isElectron } from '@/utils/envUtil'
import { useKeybindingService } from '@/workbench/keybindings/services/keybindingService'
setupAutoQueueHandler()
useProgressFavicon()

View File

@@ -13,7 +13,7 @@
import Tag from 'primevue/tag'
import { computed } from 'vue'
import type { KeyComboImpl } from '@/stores/keybindingStore'
import type { KeyComboImpl } from '@/workbench/keybindings/stores/keybindingStore'
const { keyCombo, isModified = false } = defineProps<{
keyCombo: KeyComboImpl

View File

@@ -142,17 +142,17 @@ import { computed, ref, watchEffect } from 'vue'
import { useI18n } from 'vue-i18n'
import SearchBox from '@/components/common/SearchBox.vue'
import { useKeybindingService } from '@/services/keybindingService'
import PanelTemplate from '@/components/dialog/content/setting/PanelTemplate.vue'
import { useCommandStore } from '@/stores/commandStore'
import { normalizeI18nKey } from '@/utils/formatUtil'
import { useKeybindingService } from '@/workbench/keybindings/services/keybindingService'
import {
KeyComboImpl,
KeybindingImpl,
useKeybindingStore
} from '@/stores/keybindingStore'
import { normalizeI18nKey } from '@/utils/formatUtil'
} from '@/workbench/keybindings/stores/keybindingStore'
import PanelTemplate from './PanelTemplate.vue'
import KeyComboDisplay from './keybinding/KeyComboDisplay.vue'
import KeyComboDisplay from './KeyComboDisplay.vue'
const filters = ref({
global: { value: '', matchMode: FilterMatchMode.CONTAINS }

View File

@@ -0,0 +1,33 @@
<template>
<div class="h-full flex flex-col p-4">
<div class="flex-1 min-h-0 overflow-auto">
<ShortcutsList
:commands="essentialsCommands"
:subcategories="essentialsSubcategories"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import {
ESSENTIALS_CONFIG,
useCommandSubcategories
} from '@/composables/bottomPanelTabs/useCommandSubcategories'
import { useCommandStore } from '@/stores/commandStore'
import ShortcutsList from './ShortcutsList.vue'
const commandStore = useCommandStore()
const essentialsCommands = computed(() =>
commandStore.commands.filter((cmd) => cmd.category === 'essentials')
)
const { subcategories: essentialsSubcategories } = useCommandSubcategories(
essentialsCommands,
ESSENTIALS_CONFIG
)
</script>

View File

@@ -0,0 +1,120 @@
<template>
<div class="shortcuts-list flex justify-center">
<div class="grid gap-4 md:gap-24 h-full grid-cols-1 md:grid-cols-3 w-[90%]">
<div
v-for="(subcategoryCommands, subcategory) in filteredSubcategories"
:key="subcategory"
class="flex flex-col"
>
<h3
class="subcategory-title text-xs font-bold uppercase tracking-wide text-surface-600 dark-theme:text-surface-400 mb-4"
>
{{ getSubcategoryTitle(subcategory) }}
</h3>
<div class="flex flex-col gap-1">
<div
v-for="command in subcategoryCommands"
:key="command.id"
class="shortcut-item flex justify-between items-center py-2 rounded hover:bg-surface-100 dark-theme:hover:bg-surface-700 transition-colors duration-200"
>
<div class="shortcut-info grow pr-4">
<div class="shortcut-name text-sm font-medium">
{{ t(`commands.${normalizeI18nKey(command.id)}.label`) }}
</div>
</div>
<div class="keybinding-display shrink-0">
<div
class="keybinding-combo flex gap-1"
:aria-label="`Keyboard shortcut: ${command.keybinding!.combo.getKeySequences().join(' + ')}`"
>
<span
v-for="key in command.keybinding!.combo.getKeySequences()"
:key="key"
class="key-badge px-2 py-1 text-xs font-mono bg-surface-200 dark-theme:bg-surface-600 rounded border min-w-6 text-center"
>
{{ formatKey(key) }}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import type { ComfyCommandImpl } from '@/stores/commandStore'
import { normalizeI18nKey } from '@/utils/formatUtil'
const { t } = useI18n()
const { subcategories } = defineProps<{
commands: ComfyCommandImpl[]
subcategories: Record<string, ComfyCommandImpl[]>
}>()
const filteredSubcategories = computed(() => {
const result: Record<string, ComfyCommandImpl[]> = {}
for (const [subcategory, commands] of Object.entries(subcategories)) {
result[subcategory] = commands.filter((cmd) => !!cmd.keybinding)
}
return result
})
const getSubcategoryTitle = (subcategory: string): string => {
const titleMap: Record<string, string> = {
workflow: t('shortcuts.subcategories.workflow'),
node: t('shortcuts.subcategories.node'),
queue: t('shortcuts.subcategories.queue'),
view: t('shortcuts.subcategories.view'),
'panel-controls': t('shortcuts.subcategories.panelControls')
}
return titleMap[subcategory] || subcategory
}
const formatKey = (key: string): string => {
const keyMap: Record<string, string> = {
Control: 'Ctrl',
Meta: 'Cmd',
ArrowUp: '↑',
ArrowDown: '↓',
ArrowLeft: '←',
ArrowRight: '→',
Backspace: '⌫',
Delete: '⌦',
Enter: '↵',
Escape: 'Esc',
Tab: '⇥',
' ': 'Space'
}
return keyMap[key] || key
}
</script>
<style scoped>
.subcategory-title {
color: var(--p-text-muted-color);
}
.key-badge {
background-color: var(--p-surface-200);
border: 1px solid var(--p-surface-300);
min-width: 1.5rem;
text-align: center;
}
.dark-theme .key-badge {
background-color: var(--p-surface-600);
border-color: var(--p-surface-500);
}
</style>

View File

@@ -1,9 +1,9 @@
import { markRaw } from 'vue'
import { useI18n } from 'vue-i18n'
import EssentialsPanel from '@/components/bottomPanel/tabs/shortcuts/EssentialsPanel.vue'
import ViewControlsPanel from '@/components/bottomPanel/tabs/shortcuts/ViewControlsPanel.vue'
import type { BottomPanelExtension } from '@/types/extensionTypes'
import EssentialsPanel from '@/workbench/keybindings/components/shortcuts/EssentialsPanel.vue'
import ViewControlsPanel from '@/workbench/keybindings/components/shortcuts/ViewControlsPanel.vue'
export const useShortcutsTab = (): BottomPanelExtension[] => {
const { t } = useI18n()

View File

@@ -1,13 +1,14 @@
import { CORE_KEYBINDINGS } from '@/constants/coreKeybindings'
import {
CORE_KEYBINDINGS,
KeyComboImpl,
KeybindingImpl
} from '@comfyorg/keybinding'
import { useSettingStore } from '@/platform/settings/settingStore'
import { app } from '@/scripts/app'
import { useCommandStore } from '@/stores/commandStore'
import { useDialogStore } from '@/stores/dialogStore'
import {
KeyComboImpl,
KeybindingImpl,
useKeybindingStore
} from '@/stores/keybindingStore'
import { useKeybindingStore } from '@/workbench/keybindings/stores/keybindingStore'
export const useKeybindingService = () => {
const keybindingStore = useKeybindingStore()

View File

@@ -1,115 +1,11 @@
import { KeyComboImpl, KeybindingImpl } from '@comfyorg/keybinding'
import _ from 'es-toolkit/compat'
import { defineStore } from 'pinia'
import type { Ref } from 'vue'
import { computed, ref, toRaw } from 'vue'
import { computed, ref } from 'vue'
import { RESERVED_BY_TEXT_INPUT } from '@/constants/reservedKeyCombos'
import type { KeyCombo, Keybinding } from '@/schemas/keyBindingSchema'
export class KeybindingImpl implements Keybinding {
commandId: string
combo: KeyComboImpl
targetElementId?: string
constructor(obj: Keybinding) {
this.commandId = obj.commandId
this.combo = new KeyComboImpl(obj.combo)
this.targetElementId = obj.targetElementId
}
equals(other: unknown): boolean {
const raw = toRaw(other)
return raw instanceof KeybindingImpl
? this.commandId === raw.commandId &&
this.combo.equals(raw.combo) &&
this.targetElementId === raw.targetElementId
: false
}
}
export class KeyComboImpl implements KeyCombo {
key: string
// ctrl or meta(cmd on mac)
ctrl: boolean
alt: boolean
shift: boolean
constructor(obj: KeyCombo) {
this.key = obj.key
this.ctrl = obj.ctrl ?? false
this.alt = obj.alt ?? false
this.shift = obj.shift ?? false
}
static fromEvent(event: KeyboardEvent) {
return new KeyComboImpl({
key: event.key,
ctrl: event.ctrlKey || event.metaKey,
alt: event.altKey,
shift: event.shiftKey
})
}
equals(other: unknown): boolean {
const raw = toRaw(other)
return raw instanceof KeyComboImpl
? this.key.toUpperCase() === raw.key.toUpperCase() &&
this.ctrl === raw.ctrl &&
this.alt === raw.alt &&
this.shift === raw.shift
: false
}
serialize(): string {
return `${this.key.toUpperCase()}:${this.ctrl}:${this.alt}:${this.shift}`
}
toString(): string {
return this.getKeySequences().join(' + ')
}
get hasModifier(): boolean {
return this.ctrl || this.alt || this.shift
}
get isModifier(): boolean {
return ['Control', 'Meta', 'Alt', 'Shift'].includes(this.key)
}
get modifierCount(): number {
const modifiers = [this.ctrl, this.alt, this.shift]
return modifiers.reduce((acc, cur) => acc + Number(cur), 0)
}
get isShiftOnly(): boolean {
return this.shift && this.modifierCount === 1
}
get isReservedByTextInput(): boolean {
return (
!this.hasModifier ||
this.isShiftOnly ||
RESERVED_BY_TEXT_INPUT.has(this.toString())
)
}
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
}
}
// Re-export classes from package for backward compatibility
export { KeybindingImpl, KeyComboImpl }
export const useKeybindingStore = defineStore('keybinding', () => {
/**

View File

@@ -1,15 +1,15 @@
import {
CORE_KEYBINDINGS,
KeyComboImpl,
KeybindingImpl
} from '@comfyorg/keybinding'
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { CORE_KEYBINDINGS } from '@/constants/coreKeybindings'
import { useKeybindingService } from '@/services/keybindingService'
import { useCommandStore } from '@/stores/commandStore'
import { useDialogStore } from '@/stores/dialogStore'
import {
KeyComboImpl,
KeybindingImpl,
useKeybindingStore
} from '@/stores/keybindingStore'
import { useKeybindingService } from '@/workbench/keybindings/services/keybindingService'
import { useKeybindingStore } from '@/workbench/keybindings/stores/keybindingStore'
// Mock stores
vi.mock('@/platform/settings/settingStore', () => ({

View File

@@ -2,9 +2,9 @@ import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { app } from '@/scripts/app'
import { useKeybindingService } from '@/services/keybindingService'
import { useCommandStore } from '@/stores/commandStore'
import { useDialogStore } from '@/stores/dialogStore'
import { useKeybindingService } from '@/workbench/keybindings/services/keybindingService'
// Mock the app and canvas using factory functions
vi.mock('@/scripts/app', () => {

View File

@@ -1,7 +1,8 @@
import { KeybindingImpl } from '@comfyorg/keybinding'
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it } from 'vitest'
import { KeybindingImpl, useKeybindingStore } from '@/stores/keybindingStore'
import { useKeybindingStore } from '@/workbench/keybindings/stores/keybindingStore'
describe('useKeybindingStore', () => {
beforeEach(() => {