Keyboard Shortcut Bottom Panel (#4635)

This commit is contained in:
Johnpaul Chiwetelu
2025-08-07 19:51:23 +01:00
committed by GitHub
parent f4482eb35a
commit 70c06d10bb
21 changed files with 1251 additions and 37 deletions

View File

@@ -11,18 +11,33 @@
class="p-3 border-none"
>
<span class="font-bold">
{{ tab.title.toUpperCase() }}
{{
shouldCapitalizeTab(tab.id)
? tab.title.toUpperCase()
: tab.title
}}
</span>
</Tab>
</div>
<Button
class="justify-self-end"
icon="pi pi-times"
severity="secondary"
size="small"
text
@click="bottomPanelStore.bottomPanelVisible = false"
/>
<div class="flex items-center gap-2">
<Button
v-if="isShortcutsTabActive"
:label="$t('shortcuts.manageShortcuts')"
icon="pi pi-cog"
severity="secondary"
size="small"
text
@click="openKeybindingSettings"
/>
<Button
class="justify-self-end"
icon="pi pi-times"
severity="secondary"
size="small"
text
@click="closeBottomPanel"
/>
</div>
</div>
</TabList>
</Tabs>
@@ -44,9 +59,32 @@ import Button from 'primevue/button'
import Tab from 'primevue/tab'
import TabList from 'primevue/tablist'
import Tabs from 'primevue/tabs'
import { computed } from 'vue'
import ExtensionSlot from '@/components/common/ExtensionSlot.vue'
import { useDialogService } from '@/services/dialogService'
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
const bottomPanelStore = useBottomPanelStore()
const dialogService = useDialogService()
const isShortcutsTabActive = computed(() => {
const activeTabId = bottomPanelStore.activeBottomPanelTabId
return (
activeTabId === 'shortcuts-essentials' ||
activeTabId === 'shortcuts-view-controls'
)
})
const shouldCapitalizeTab = (tabId: string): boolean => {
return tabId !== 'shortcuts-essentials' && tabId !== 'shortcuts-view-controls'
}
const openKeybindingSettings = async () => {
dialogService.showSettingsDialog('keybinding')
}
const closeBottomPanel = () => {
bottomPanelStore.activePanel = null
}
</script>

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,119 @@
<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 flex-grow pr-4">
<div class="shortcut-name text-sm font-medium">
{{ command.label || command.id }}
</div>
</div>
<div class="keybinding-display flex-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'
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

@@ -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="viewControlsCommands"
:subcategories="viewControlsSubcategories"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import {
VIEW_CONTROLS_CONFIG,
useCommandSubcategories
} from '@/composables/bottomPanelTabs/useCommandSubcategories'
import { useCommandStore } from '@/stores/commandStore'
import ShortcutsList from './ShortcutsList.vue'
const commandStore = useCommandStore()
const viewControlsCommands = computed(() =>
commandStore.commands.filter((cmd) => cmd.category === 'view-controls')
)
const { subcategories: viewControlsSubcategories } = useCommandSubcategories(
viewControlsCommands,
VIEW_CONTROLS_CONFIG
)
</script>

View File

@@ -2,7 +2,11 @@
<div
v-if="visible && initialized"
ref="containerRef"
class="litegraph-minimap absolute bottom-[20px] right-[90px] z-[1000]"
class="litegraph-minimap absolute right-[90px] z-[1000]"
:class="{
'bottom-[20px]': !bottomPanelStore.bottomPanelVisible,
'bottom-[280px]': bottomPanelStore.bottomPanelVisible
}"
:style="containerStyles"
@pointerdown="handlePointerDown"
@pointermove="handlePointerMove"
@@ -25,9 +29,11 @@ import { onMounted, onUnmounted, watch } from 'vue'
import { useMinimap } from '@/composables/useMinimap'
import { useCanvasStore } from '@/stores/graphStore'
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
const minimap = useMinimap()
const canvasStore = useCanvasStore()
const bottomPanelStore = useBottomPanelStore()
const {
initialized,

View File

@@ -16,6 +16,7 @@
<SidebarLogoutIcon v-if="userStore.isMultiUserServer" />
<SidebarHelpCenterIcon />
<SidebarBottomPanelToggleButton />
<SidebarShortcutsToggleButton />
</div>
</nav>
</teleport>
@@ -32,6 +33,7 @@ import { computed } from 'vue'
import ExtensionSlot from '@/components/common/ExtensionSlot.vue'
import SidebarBottomPanelToggleButton from '@/components/sidebar/SidebarBottomPanelToggleButton.vue'
import SidebarShortcutsToggleButton from '@/components/sidebar/SidebarShortcutsToggleButton.vue'
import { useKeybindingStore } from '@/stores/keybindingStore'
import { useSettingStore } from '@/stores/settingStore'
import { useUserStore } from '@/stores/userStore'

View File

@@ -1,7 +1,7 @@
<template>
<SidebarIcon
:tooltip="$t('menu.toggleBottomPanel')"
:selected="bottomPanelStore.bottomPanelVisible"
:selected="bottomPanelStore.activePanel == 'terminal'"
@click="bottomPanelStore.toggleBottomPanel"
>
<template #icon>

View File

@@ -0,0 +1,44 @@
<template>
<SidebarIcon
:tooltip="
$t('shortcuts.keyboardShortcuts') +
' (' +
formatKeySequence(command.keybinding!.combo.getKeySequences()) +
')'
"
:selected="isShortcutsPanelVisible"
@click="toggleShortcutsPanel"
>
<template #icon>
<i-lucide:keyboard />
</template>
</SidebarIcon>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useCommandStore } from '@/stores/commandStore'
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
import SidebarIcon from './SidebarIcon.vue'
const bottomPanelStore = useBottomPanelStore()
const command = useCommandStore().getCommand(
'Workspace.ToggleBottomPanel.Shortcuts'
)
const isShortcutsPanelVisible = computed(
() => bottomPanelStore.activePanel === 'shortcuts'
)
const toggleShortcutsPanel = () => {
bottomPanelStore.togglePanel('shortcuts')
}
const formatKeySequence = (sequences: string[]): string => {
return sequences
.map((seq) => seq.replace(/Control/g, 'Ctrl').replace(/Shift/g, 'Shift'))
.join(' + ')
}
</script>

View File

@@ -0,0 +1,78 @@
import { type ComputedRef, computed } from 'vue'
import { type ComfyCommandImpl } from '@/stores/commandStore'
export type SubcategoryRule = {
pattern: string | RegExp
subcategory: string
}
export type SubcategoryConfig = {
defaultSubcategory: string
rules: SubcategoryRule[]
}
/**
* Composable for grouping commands by subcategory based on configurable rules
*/
export function useCommandSubcategories(
commands: ComputedRef<ComfyCommandImpl[]>,
config: SubcategoryConfig
) {
const subcategories = computed(() => {
const result: Record<string, ComfyCommandImpl[]> = {}
for (const command of commands.value) {
let subcategory = config.defaultSubcategory
// Find the first matching rule
for (const rule of config.rules) {
const matches =
typeof rule.pattern === 'string'
? command.id.includes(rule.pattern)
: rule.pattern.test(command.id)
if (matches) {
subcategory = rule.subcategory
break
}
}
if (!result[subcategory]) {
result[subcategory] = []
}
result[subcategory].push(command)
}
return result
})
return {
subcategories
}
}
/**
* Predefined configuration for view controls subcategories
*/
export const VIEW_CONTROLS_CONFIG: SubcategoryConfig = {
defaultSubcategory: 'view',
rules: [
{ pattern: 'Zoom', subcategory: 'view' },
{ pattern: 'Fit', subcategory: 'view' },
{ pattern: 'Panel', subcategory: 'panel-controls' },
{ pattern: 'Sidebar', subcategory: 'panel-controls' }
]
}
/**
* Predefined configuration for essentials subcategories
*/
export const ESSENTIALS_CONFIG: SubcategoryConfig = {
defaultSubcategory: 'workflow',
rules: [
{ pattern: 'Workflow', subcategory: 'workflow' },
{ pattern: 'Node', subcategory: 'node' },
{ pattern: 'Queue', subcategory: 'queue' }
]
}

View File

@@ -0,0 +1,27 @@
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 { BottomPanelExtension } from '@/types/extensionTypes'
export const useShortcutsTab = (): BottomPanelExtension[] => {
const { t } = useI18n()
return [
{
id: 'shortcuts-essentials',
title: t('shortcuts.essentials'),
component: markRaw(EssentialsPanel),
type: 'vue',
targetPanel: 'shortcuts'
},
{
id: 'shortcuts-view-controls',
title: t('shortcuts.viewControls'),
component: markRaw(ViewControlsPanel),
type: 'vue',
targetPanel: 'shortcuts'
}
]
}

View File

@@ -46,6 +46,9 @@ export function useCoreCommands(): ComfyCommand[] {
const toastStore = useToastStore()
const canvasStore = useCanvasStore()
const executionStore = useExecutionStore()
const bottomPanelStore = useBottomPanelStore()
const { getSelectedNodes, toggleSelectedNodesMode } =
useSelectedLiteGraphItems()
const getTracker = () => workflowStore.activeWorkflow?.changeTracker
@@ -70,6 +73,7 @@ export function useCoreCommands(): ComfyCommand[] {
icon: 'pi pi-plus',
label: 'New Blank Workflow',
menubarLabel: 'New',
category: 'essentials' as const,
function: () => workflowService.loadBlankWorkflow()
},
{
@@ -77,6 +81,7 @@ export function useCoreCommands(): ComfyCommand[] {
icon: 'pi pi-folder-open',
label: 'Open Workflow',
menubarLabel: 'Open',
category: 'essentials' as const,
function: () => {
app.ui.loadFile()
}
@@ -92,6 +97,7 @@ export function useCoreCommands(): ComfyCommand[] {
icon: 'pi pi-save',
label: 'Save Workflow',
menubarLabel: 'Save',
category: 'essentials' as const,
function: async () => {
const workflow = useWorkflowStore().activeWorkflow as ComfyWorkflow
if (!workflow) return
@@ -104,6 +110,7 @@ export function useCoreCommands(): ComfyCommand[] {
icon: 'pi pi-save',
label: 'Save Workflow As',
menubarLabel: 'Save As',
category: 'essentials' as const,
function: async () => {
const workflow = useWorkflowStore().activeWorkflow as ComfyWorkflow
if (!workflow) return
@@ -116,6 +123,7 @@ export function useCoreCommands(): ComfyCommand[] {
icon: 'pi pi-download',
label: 'Export Workflow',
menubarLabel: 'Export',
category: 'essentials' as const,
function: async () => {
await workflowService.exportWorkflow('workflow', 'workflow')
}
@@ -133,6 +141,7 @@ export function useCoreCommands(): ComfyCommand[] {
id: 'Comfy.Undo',
icon: 'pi pi-undo',
label: 'Undo',
category: 'essentials' as const,
function: async () => {
await getTracker()?.undo?.()
}
@@ -141,6 +150,7 @@ export function useCoreCommands(): ComfyCommand[] {
id: 'Comfy.Redo',
icon: 'pi pi-refresh',
label: 'Redo',
category: 'essentials' as const,
function: async () => {
await getTracker()?.redo?.()
}
@@ -149,6 +159,7 @@ export function useCoreCommands(): ComfyCommand[] {
id: 'Comfy.ClearWorkflow',
icon: 'pi pi-trash',
label: 'Clear Workflow',
category: 'essentials' as const,
function: () => {
const settingStore = useSettingStore()
if (
@@ -190,6 +201,7 @@ export function useCoreCommands(): ComfyCommand[] {
id: 'Comfy.RefreshNodeDefinitions',
icon: 'pi pi-refresh',
label: 'Refresh Node Definitions',
category: 'essentials' as const,
function: async () => {
await app.refreshComboInNodes()
}
@@ -198,6 +210,7 @@ export function useCoreCommands(): ComfyCommand[] {
id: 'Comfy.Interrupt',
icon: 'pi pi-stop',
label: 'Interrupt',
category: 'essentials' as const,
function: async () => {
await api.interrupt(executionStore.activePromptId)
toastStore.add({
@@ -212,6 +225,7 @@ export function useCoreCommands(): ComfyCommand[] {
id: 'Comfy.ClearPendingTasks',
icon: 'pi pi-stop',
label: 'Clear Pending Tasks',
category: 'essentials' as const,
function: async () => {
await useQueueStore().clear(['queue'])
toastStore.add({
@@ -234,6 +248,7 @@ export function useCoreCommands(): ComfyCommand[] {
id: 'Comfy.Canvas.ZoomIn',
icon: 'pi pi-plus',
label: 'Zoom In',
category: 'view-controls' as const,
function: () => {
const ds = app.canvas.ds
ds.changeScale(
@@ -247,6 +262,7 @@ export function useCoreCommands(): ComfyCommand[] {
id: 'Comfy.Canvas.ZoomOut',
icon: 'pi pi-minus',
label: 'Zoom Out',
category: 'view-controls' as const,
function: () => {
const ds = app.canvas.ds
ds.changeScale(
@@ -260,6 +276,7 @@ export function useCoreCommands(): ComfyCommand[] {
id: 'Comfy.Canvas.FitView',
icon: 'pi pi-expand',
label: 'Fit view to selected nodes',
category: 'view-controls' as const,
function: () => {
if (app.canvas.empty) {
toastStore.add({
@@ -325,6 +342,7 @@ export function useCoreCommands(): ComfyCommand[] {
icon: 'pi pi-play',
label: 'Queue Prompt',
versionAdded: '1.3.7',
category: 'essentials' as const,
function: async () => {
const batchCount = useQueueSettingsStore().batchCount
await app.queuePrompt(0, batchCount)
@@ -335,6 +353,7 @@ export function useCoreCommands(): ComfyCommand[] {
icon: 'pi pi-play',
label: 'Queue Prompt (Front)',
versionAdded: '1.3.7',
category: 'essentials' as const,
function: async () => {
const batchCount = useQueueSettingsStore().batchCount
await app.queuePrompt(-1, batchCount)
@@ -371,6 +390,7 @@ export function useCoreCommands(): ComfyCommand[] {
icon: 'pi pi-cog',
label: 'Show Settings Dialog',
versionAdded: '1.3.7',
category: 'view-controls' as const,
function: () => {
dialogService.showSettingsDialog()
}
@@ -380,6 +400,7 @@ export function useCoreCommands(): ComfyCommand[] {
icon: 'pi pi-sitemap',
label: 'Group Selected Nodes',
versionAdded: '1.3.7',
category: 'essentials' as const,
function: () => {
const { canvas } = app
if (!canvas.selectedItems?.size) {
@@ -423,6 +444,7 @@ export function useCoreCommands(): ComfyCommand[] {
icon: 'pi pi-volume-off',
label: 'Mute/Unmute Selected Nodes',
versionAdded: '1.3.11',
category: 'essentials' as const,
function: () => {
toggleSelectedNodesMode(LGraphEventMode.NEVER)
app.canvas.setDirty(true, true)
@@ -433,6 +455,7 @@ export function useCoreCommands(): ComfyCommand[] {
icon: 'pi pi-shield',
label: 'Bypass/Unbypass Selected Nodes',
versionAdded: '1.3.11',
category: 'essentials' as const,
function: () => {
toggleSelectedNodesMode(LGraphEventMode.BYPASS)
app.canvas.setDirty(true, true)
@@ -443,6 +466,7 @@ export function useCoreCommands(): ComfyCommand[] {
icon: 'pi pi-pin',
label: 'Pin/Unpin Selected Nodes',
versionAdded: '1.3.11',
category: 'essentials' as const,
function: () => {
getSelectedNodes().forEach((node) => {
node.pin(!node.pinned)
@@ -516,8 +540,9 @@ export function useCoreCommands(): ComfyCommand[] {
icon: 'pi pi-list',
label: 'Toggle Bottom Panel',
versionAdded: '1.3.22',
category: 'view-controls' as const,
function: () => {
useBottomPanelStore().toggleBottomPanel()
bottomPanelStore.toggleBottomPanel()
}
},
{
@@ -525,6 +550,7 @@ export function useCoreCommands(): ComfyCommand[] {
icon: 'pi pi-eye',
label: 'Toggle Focus Mode',
versionAdded: '1.3.27',
category: 'view-controls' as const,
function: () => {
useWorkspaceStore().toggleFocusMode()
}
@@ -750,6 +776,7 @@ export function useCoreCommands(): ComfyCommand[] {
icon: 'pi pi-sitemap',
label: 'Convert Selection to Subgraph',
versionAdded: '1.20.1',
category: 'essentials' as const,
function: () => {
const canvas = canvasStore.getCanvas()
const graph = canvas.subgraph ?? canvas.graph
@@ -768,6 +795,16 @@ export function useCoreCommands(): ComfyCommand[] {
const { node } = res
canvas.select(node)
}
},
{
id: 'Workspace.ToggleBottomPanel.Shortcuts',
icon: 'pi pi-key',
label: 'Show Keybindings Dialog',
versionAdded: '1.24.1',
category: 'view-controls' as const,
function: () => {
bottomPanelStore.togglePanel('shortcuts')
}
}
]

View File

@@ -182,5 +182,13 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
alt: true
},
commandId: 'Comfy.Canvas.ToggleMinimap'
},
{
combo: {
ctrl: true,
shift: true,
key: 'k'
},
commandId: 'Workspace.ToggleBottomPanel.Shortcuts'
}
]

View File

@@ -1630,5 +1630,19 @@
"clearWorkflow": "Clear Workflow",
"deleteWorkflow": "Delete Workflow",
"enterNewName": "Enter new name"
},
"shortcuts": {
"essentials": "Essential",
"viewControls": "View Controls",
"manageShortcuts": "Manage Shortcuts",
"noKeybinding": "No keybinding",
"keyboardShortcuts": "Keyboard Shortcuts",
"subcategories": {
"workflow": "Workflow",
"node": "Node",
"queue": "Queue",
"view": "View",
"panelControls": "Panel Controls"
}
}
}

View File

@@ -17,6 +17,7 @@ export interface ComfyCommand {
versionAdded?: string
confirmation?: string // If non-nullish, this command will prompt for confirmation
source?: string
category?: 'essentials' | 'view-controls' // For shortcuts panel organization
}
export class ComfyCommandImpl implements ComfyCommand {
@@ -29,6 +30,7 @@ export class ComfyCommandImpl implements ComfyCommand {
versionAdded?: string
confirmation?: string
source?: string
category?: 'essentials' | 'view-controls'
constructor(command: ComfyCommand) {
this.id = command.id
@@ -40,6 +42,7 @@ export class ComfyCommandImpl implements ComfyCommand {
this.versionAdded = command.versionAdded
this.confirmation = command.confirmation
this.source = command.source
this.category = command.category
}
get label() {

View File

@@ -1,6 +1,7 @@
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import { useShortcutsTab } from '@/composables/bottomPanelTabs/useShortcutsTab'
import {
useCommandTerminalTab,
useLogsTerminalTab
@@ -10,45 +11,110 @@ import { ComfyExtension } from '@/types/comfy'
import type { BottomPanelExtension } from '@/types/extensionTypes'
import { isElectron } from '@/utils/envUtil'
type PanelType = 'terminal' | 'shortcuts'
interface PanelState {
tabs: BottomPanelExtension[]
activeTabId: string
visible: boolean
}
export const useBottomPanelStore = defineStore('bottomPanel', () => {
const bottomPanelVisible = ref(false)
const toggleBottomPanel = () => {
// If there are no tabs, don't show the bottom panel
if (bottomPanelTabs.value.length === 0) {
return
// Multi-panel state
const panels = ref<Record<PanelType, PanelState>>({
terminal: { tabs: [], activeTabId: '', visible: false },
shortcuts: { tabs: [], activeTabId: '', visible: false }
})
const activePanel = ref<PanelType | null>(null)
// Computed properties for active panel
const activePanelState = computed(() =>
activePanel.value ? panels.value[activePanel.value] : null
)
const activeBottomPanelTab = computed<BottomPanelExtension | null>(() => {
const state = activePanelState.value
if (!state) return null
return state.tabs.find((tab) => tab.id === state.activeTabId) ?? null
})
const bottomPanelVisible = computed({
get: () => !!activePanel.value,
set: (visible: boolean) => {
if (!visible) {
activePanel.value = null
}
}
})
const bottomPanelTabs = computed(() => activePanelState.value?.tabs ?? [])
const activeBottomPanelTabId = computed({
get: () => activePanelState.value?.activeTabId ?? '',
set: (tabId: string) => {
const state = activePanelState.value
if (state) {
state.activeTabId = tabId
}
}
})
const togglePanel = (panelType: PanelType) => {
const panel = panels.value[panelType]
if (panel.tabs.length === 0) return
if (activePanel.value === panelType) {
// Hide current panel
activePanel.value = null
} else {
// Show target panel
activePanel.value = panelType
if (!panel.activeTabId && panel.tabs.length > 0) {
panel.activeTabId = panel.tabs[0].id
}
}
bottomPanelVisible.value = !bottomPanelVisible.value
}
const bottomPanelTabs = ref<BottomPanelExtension[]>([])
const activeBottomPanelTabId = ref<string>('')
const activeBottomPanelTab = computed<BottomPanelExtension | null>(() => {
return (
bottomPanelTabs.value.find(
(tab) => tab.id === activeBottomPanelTabId.value
) ?? null
)
})
const setActiveTab = (tabId: string) => {
activeBottomPanelTabId.value = tabId
const toggleBottomPanel = () => {
// Legacy method - toggles terminal panel
togglePanel('terminal')
}
const setActiveTab = (tabId: string) => {
const state = activePanelState.value
if (state) {
state.activeTabId = tabId
}
}
const toggleBottomPanelTab = (tabId: string) => {
if (activeBottomPanelTabId.value === tabId && bottomPanelVisible.value) {
bottomPanelVisible.value = false
} else {
activeBottomPanelTabId.value = tabId
bottomPanelVisible.value = true
// Find which panel contains this tab
for (const [panelType, panel] of Object.entries(panels.value)) {
const tab = panel.tabs.find((t) => t.id === tabId)
if (tab) {
if (activePanel.value === panelType && panel.activeTabId === tabId) {
activePanel.value = null
} else {
activePanel.value = panelType as PanelType
panel.activeTabId = tabId
}
return
}
}
}
const registerBottomPanelTab = (tab: BottomPanelExtension) => {
bottomPanelTabs.value = [...bottomPanelTabs.value, tab]
if (bottomPanelTabs.value.length === 1) {
activeBottomPanelTabId.value = tab.id
const targetPanel = tab.targetPanel ?? 'terminal'
const panel = panels.value[targetPanel]
panel.tabs = [...panel.tabs, tab]
if (panel.tabs.length === 1) {
panel.activeTabId = tab.id
}
useCommandStore().registerCommand({
id: `Workspace.ToggleBottomPanelTab.${tab.id}`,
icon: 'pi pi-list',
label: `Toggle ${tab.title} Bottom Panel`,
category: 'view-controls' as const,
function: () => toggleBottomPanelTab(tab.id),
source: 'System'
})
@@ -59,6 +125,7 @@ export const useBottomPanelStore = defineStore('bottomPanel', () => {
if (isElectron()) {
registerBottomPanelTab(useCommandTerminalTab())
}
useShortcutsTab().forEach(registerBottomPanelTab)
}
const registerExtensionBottomPanelTabs = (extension: ComfyExtension) => {
@@ -68,6 +135,11 @@ export const useBottomPanelStore = defineStore('bottomPanel', () => {
}
return {
// Multi-panel API
panels,
activePanel,
togglePanel,
bottomPanelVisible,
toggleBottomPanel,
bottomPanelTabs,

View File

@@ -44,6 +44,7 @@ export const useSidebarTabStore = defineStore('sidebarTab', () => {
label: labelFunction,
tooltip: tooltipFunction,
versionAdded: '1.3.9',
category: 'view-controls' as const,
function: () => {
toggleSidebarTab(tab.id)
},

View File

@@ -14,6 +14,7 @@ export interface BaseSidebarTabExtension {
export interface BaseBottomPanelExtension {
id: string
title: string
targetPanel?: 'terminal' | 'shortcuts'
}
export interface VueExtension {