Add bottom panel support (#1294)

* Add bottom panel

* Bottom panel store

* Extract ExtensionSlot component

* Tab rendering

* Add toggle button on top menu bar

* nit

* Add toggle button tooltip

* Add command
This commit is contained in:
Chenlei Hu
2024-10-24 21:15:19 +02:00
committed by GitHub
parent 957a767ed0
commit d142893244
11 changed files with 240 additions and 58 deletions

View File

@@ -1,5 +1,8 @@
<template>
<Splitter class="splitter-overlay" :pt:gutter="gutterClass">
<Splitter
class="splitter-overlay-root splitter-overlay"
:pt:gutter="sidebarPanelVisible ? '' : 'hidden'"
>
<SplitterPanel
class="side-bar-panel"
:minSize="10"
@@ -9,9 +12,22 @@
>
<slot name="side-bar-panel"></slot>
</SplitterPanel>
<SplitterPanel class="graph-canvas-panel relative" :size="100">
<slot name="graph-canvas-panel"></slot>
<SplitterPanel :size="100">
<Splitter
class="splitter-overlay"
layout="vertical"
:pt:gutter="bottomPanelVisible ? '' : 'hidden'"
>
<SplitterPanel class="graph-canvas-panel relative">
<slot name="graph-canvas-panel"></slot>
</SplitterPanel>
<SplitterPanel class="bottom-panel" v-show="bottomPanelVisible">
<slot name="bottom-panel"></slot>
</SplitterPanel>
</Splitter>
</SplitterPanel>
<SplitterPanel
class="side-bar-panel"
:minSize="10"
@@ -26,7 +42,8 @@
<script setup lang="ts">
import { useSettingStore } from '@/stores/settingStore'
import { useWorkspaceStore } from '@/stores/workspaceStateStore'
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
import Splitter from 'primevue/splitter'
import SplitterPanel from 'primevue/splitterpanel'
import { computed } from 'vue'
@@ -37,42 +54,39 @@ const sidebarLocation = computed<'left' | 'right'>(() =>
)
const sidebarPanelVisible = computed(
() => useWorkspaceStore().sidebarTab.activeSidebarTab !== null
() => useSidebarTabStore().activeSidebarTab !== null
)
const bottomPanelVisible = computed(
() => useBottomPanelStore().bottomPanelVisible
)
const gutterClass = computed(() => {
return sidebarPanelVisible.value ? '' : 'gutter-hidden'
})
</script>
<style>
.p-splitter-gutter {
<style scoped>
:deep(.p-splitter-gutter) {
pointer-events: auto;
}
.gutter-hidden {
display: none !important;
}
</style>
<style scoped>
.side-bar-panel {
background-color: var(--bg-color);
pointer-events: auto;
}
.bottom-panel {
background-color: var(--bg-color);
pointer-events: auto;
}
.splitter-overlay {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
background-color: transparent;
pointer-events: none;
@apply bg-transparent pointer-events-none border-none;
}
.splitter-overlay-root {
@apply w-full h-full absolute top-0 left-0;
/* Set it the same as the ComfyUI menu */
/* Note: Lite-graph DOM widgets have the same z-index as the node id, so
999 should be sufficient to make sure splitter overlays on node's DOM
widgets */
z-index: 999;
border: none;
}
</style>

View File

@@ -0,0 +1,43 @@
<template>
<Tabs v-model:value="bottomPanelStore.activeBottomPanelTabId">
<TabList pt:tabList="border-none">
<div class="w-full flex justify-between">
<div class="tabs-container">
<Tab
v-for="tab in bottomPanelStore.bottomPanelTabs"
:key="tab.id"
:value="tab.id"
class="p-3 border-none"
>
<span class="font-bold">
{{ tab.title.toUpperCase() }}
</span>
</Tab>
</div>
<Button
class="justify-self-end"
icon="pi pi-times"
severity="secondary"
size="small"
text
@click="bottomPanelStore.bottomPanelVisible = false"
/>
</div>
</TabList>
</Tabs>
<ExtensionSlot
v-if="bottomPanelStore.activeBottomPanelTab"
:extension="bottomPanelStore.activeBottomPanelTab"
/>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import Tabs from 'primevue/tabs'
import TabList from 'primevue/tablist'
import Tab from 'primevue/tab'
import ExtensionSlot from '@/components/common/ExtensionSlot.vue'
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
const bottomPanelStore = useBottomPanelStore()
</script>

View File

@@ -0,0 +1,34 @@
<template>
<component v-if="extension.type === 'vue'" :is="extension.component" />
<div
v-else
:ref="
(el) => {
if (el)
mountCustomExtension(
props.extension as CustomExtension,
el as HTMLElement
)
}
"
></div>
</template>
<script setup lang="ts">
import { CustomExtension, VueExtension } from '@/types/extensionTypes'
import { onBeforeUnmount } from 'vue'
const props = defineProps<{
extension: VueExtension | CustomExtension
}>()
const mountCustomExtension = (extension: CustomExtension, el: HTMLElement) => {
extension.render(el)
}
onBeforeUnmount(() => {
if (props.extension.type === 'custom' && props.extension.destroy) {
props.extension.destroy()
}
})
</script>

View File

@@ -4,6 +4,9 @@
<template #side-bar-panel>
<SideToolbar />
</template>
<template #bottom-panel>
<BottomPanel />
</template>
<template #graph-canvas-panel>
<GraphCanvasMenu v-if="canvasMenuEnabled" />
</template>
@@ -19,6 +22,7 @@
<script setup lang="ts">
import TitleEditor from '@/components/graph/TitleEditor.vue'
import SideToolbar from '@/components/sidebar/SideToolbar.vue'
import BottomPanel from '@/components/bottomPanel/BottomPanel.vue'
import LiteGraphCanvasSplitterOverlay from '@/components/LiteGraphCanvasSplitterOverlay.vue'
import NodeSearchboxPopover from '@/components/searchbox/NodeSearchBoxPopover.vue'
import NodeTooltip from '@/components/graph/NodeTooltip.vue'

View File

@@ -21,19 +21,7 @@
v-if="selectedTab"
class="sidebar-content-container h-full overflow-y-auto overflow-x-hidden"
>
<component v-if="selectedTab.type === 'vue'" :is="selectedTab.component" />
<div
v-else
:ref="
(el) => {
if (el)
mountCustomTab(
selectedTab as CustomSidebarTabExtension,
el as HTMLElement
)
}
"
></div>
<ExtensionSlot :extension="selectedTab" />
</div>
</template>
@@ -41,13 +29,11 @@
import SidebarIcon from './SidebarIcon.vue'
import SidebarThemeToggleIcon from './SidebarThemeToggleIcon.vue'
import SidebarSettingsToggleIcon from './SidebarSettingsToggleIcon.vue'
import { computed, onBeforeUnmount } from 'vue'
import ExtensionSlot from '@/components/common/ExtensionSlot.vue'
import { computed } from 'vue'
import { useWorkspaceStore } from '@/stores/workspaceStateStore'
import { useSettingStore } from '@/stores/settingStore'
import {
CustomSidebarTabExtension,
SidebarTabExtension
} from '@/types/extensionTypes'
import type { SidebarTabExtension } from '@/types/extensionTypes'
import { useKeybindingStore } from '@/stores/keybindingStore'
const workspaceStore = useWorkspaceStore()
@@ -65,20 +51,9 @@ const isSmall = computed(
const tabs = computed(() => workspaceStore.getSidebarTabs())
const selectedTab = computed(() => workspaceStore.sidebarTab.activeSidebarTab)
const mountCustomTab = (tab: CustomSidebarTabExtension, el: HTMLElement) => {
tab.render(el)
}
const onTabClick = (item: SidebarTabExtension) => {
workspaceStore.sidebarTab.toggleSidebarTab(item.id)
}
onBeforeUnmount(() => {
tabs.value.forEach((tab) => {
if (tab.type === 'custom' && tab.destroy) {
tab.destroy()
}
})
})
const keybindingStore = useKeybindingStore()
const getTabTooltipSuffix = (tab: SidebarTabExtension) => {
const keybinding = keybindingStore.getKeybindingByCommandId(

View File

@@ -0,0 +1,23 @@
<template>
<Button
v-show="bottomPanelStore.bottomPanelTabs.length > 0"
severity="secondary"
text
@click="bottomPanelStore.toggleBottomPanel"
v-tooltip="{ value: $t('menu.toggleBottomPanel'), showDelay: 300 }"
>
<template #icon>
<i-material-symbols:dock-to-bottom
v-if="bottomPanelStore.bottomPanelVisible"
/>
<i-material-symbols:dock-to-bottom-outline v-else />
</template>
</Button>
</template>
<script setup lang="ts">
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
import Button from 'primevue/button'
const bottomPanelStore = useBottomPanelStore()
</script>

View File

@@ -14,6 +14,7 @@
</div>
<div class="comfyui-menu-right" ref="menuRight"></div>
<Actionbar />
<BottomPanelToggleButton />
</div>
</teleport>
</template>
@@ -23,6 +24,7 @@ import Divider from 'primevue/divider'
import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
import CommandMenubar from '@/components/topbar/CommandMenubar.vue'
import Actionbar from '@/components/actionbar/ComfyActionbar.vue'
import BottomPanelToggleButton from '@/components/topbar/BottomPanelToggleButton.vue'
import { computed, onMounted, provide, ref } from 'vue'
import { useSettingStore } from '@/stores/settingStore'
import { app } from '@/scripts/app'

View File

@@ -91,7 +91,8 @@ const messages = {
refresh: 'Refresh node definitions',
clipspace: 'Open Clipspace',
resetView: 'Reset canvas view',
clear: 'Clear workflow'
clear: 'Clear workflow',
toggleBottomPanel: 'Toggle Bottom Panel'
},
templateWorkflows: {
title: 'Get Started with a Template',
@@ -200,7 +201,8 @@ const messages = {
refresh: '刷新节点',
clipspace: '打开剪贴板',
resetView: '重置画布视图',
clear: '清空工作流'
clear: '清空工作流',
toggleBottomPanel: '底部面板'
},
templateWorkflows: {
title: '从模板开始',

View File

@@ -18,6 +18,7 @@ import { useTitleEditorStore } from './graphStore'
import { useErrorHandling } from '@/hooks/errorHooks'
import { useWorkflowStore } from './workflowStore'
import { type KeybindingImpl, useKeybindingStore } from './keybindingStore'
import { useBottomPanelStore } from './workspace/bottomPanelStore'
import { LGraphNode } from '@comfyorg/litegraph'
export interface ComfyCommand {
@@ -458,6 +459,15 @@ export const useCommandStore = defineStore('command', () => {
}
}
})()
},
{
id: 'Workspace.ToggleBottomPanel',
icon: 'pi pi-list',
label: 'Toggle Bottom Panel',
versionAdded: '1.3.22',
function: () => {
useBottomPanelStore().toggleBottomPanel()
}
}
]

View File

@@ -0,0 +1,59 @@
import type { BottomPanelExtension } from '@/types/extensionTypes'
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import { useCommandStore } from '@/stores/commandStore'
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
}
bottomPanelVisible.value = !bottomPanelVisible.value
}
const bottomPanelTabs = ref<BottomPanelExtension[]>([])
const activeBottomPanelTabId = ref<string | null>(null)
const activeBottomPanelTab = computed<BottomPanelExtension | null>(() => {
return (
bottomPanelTabs.value.find(
(tab) => tab.id === activeBottomPanelTabId.value
) ?? null
)
})
const setActiveTab = (tabId: string) => {
activeBottomPanelTabId.value = tabId
}
const toggleBottomPanelTab = (tabId: string) => {
if (activeBottomPanelTabId.value === tabId) {
bottomPanelVisible.value = false
} else {
activeBottomPanelTabId.value = tabId
bottomPanelVisible.value = true
}
}
const registerBottomPanelTab = (tab: BottomPanelExtension) => {
bottomPanelTabs.value = [...bottomPanelTabs.value, tab]
if (bottomPanelTabs.value.length === 1) {
activeBottomPanelTabId.value = tab.id
}
useCommandStore().registerCommand({
id: `Workspace.ToggleBottomPanelTab.${tab.id}`,
icon: 'pi pi-list',
label: tab.title,
function: () => toggleBottomPanelTab(tab.id)
})
}
return {
bottomPanelVisible,
toggleBottomPanel,
bottomPanelTabs,
activeBottomPanelTab,
activeBottomPanelTabId,
setActiveTab,
toggleBottomPanelTab,
registerBottomPanelTab
}
})

View File

@@ -6,25 +6,41 @@ export interface BaseSidebarTabExtension {
title: string
icon?: string
iconBadge?: string | (() => string | null)
order?: number
tooltip?: string
}
export interface VueSidebarTabExtension extends BaseSidebarTabExtension {
export interface BaseBottomPanelExtension {
id: string
title: string
}
export interface VueExtension {
id: string
type: 'vue'
component: Component
}
export interface CustomSidebarTabExtension extends BaseSidebarTabExtension {
export interface CustomExtension {
id: string
type: 'custom'
render: (container: HTMLElement) => void
destroy?: () => void
}
export type VueSidebarTabExtension = BaseSidebarTabExtension & VueExtension
export type CustomSidebarTabExtension = BaseSidebarTabExtension &
CustomExtension
export type SidebarTabExtension =
| VueSidebarTabExtension
| CustomSidebarTabExtension
export type VueBottomPanelExtension = BaseBottomPanelExtension & VueExtension
export type CustomBottomPanelExtension = BaseBottomPanelExtension &
CustomExtension
export type BottomPanelExtension =
| VueBottomPanelExtension
| CustomBottomPanelExtension
export type ToastManager = {
add(message: ToastMessageOptions): void
remove(message: ToastMessageOptions): void