Add topbar dropdown menu (#937)

* Add basic menu

* Add workflows/edit to menu bar

* Add command store

* Fix z-index

* Fix beta menu setting switch

* nit

* Drop to center

* Fix command invocation
This commit is contained in:
Chenlei Hu
2024-09-24 09:51:57 +09:00
parent 6a158d46b8
commit 0d28c108d2
13 changed files with 298 additions and 106 deletions

View File

@@ -138,6 +138,7 @@ test.describe('Color Palette', () => {
test.afterEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.CustomColorPalettes', {})
await comfyPage.setSetting('Comfy.ColorPalette', 'dark')
await comfyPage.setSetting('Comfy.Node.Opacity', 1.0)
})
test('Can show custom color palette', async ({ comfyPage }) => {

View File

@@ -85,7 +85,21 @@ test.describe('Menu', () => {
const count = await comfyPage.getGraphNodesCount()
// Drag the node onto the canvas
const canvasSelector = '#graph-canvas'
await comfyPage.page.dragAndDrop(nodeSelector, canvasSelector)
// Get the bounding box of the canvas element
const canvasBoundingBox = (await comfyPage.page
.locator(canvasSelector)
.boundingBox())!
// Calculate the center position of the canvas
const targetPosition = {
x: canvasBoundingBox.x + canvasBoundingBox.width / 2,
y: canvasBoundingBox.y + canvasBoundingBox.height / 2
}
await comfyPage.page.dragAndDrop(nodeSelector, canvasSelector, {
targetPosition
})
// Verify the node is added to the canvas
expect(await comfyPage.getGraphNodesCount()).toBe(count + 1)

View File

@@ -7,7 +7,7 @@
<link rel="stylesheet" type="text/css" href="user.css" />
<link rel="stylesheet" type="text/css" href="materialdesignicons.min.css" />
</head>
<body class="litegraph">
<body class="litegraph grid">
<div id="vue-app"></div>
<div id="comfy-user-selection" class="comfy-user-selection" style="display: none;">
<main class="comfy-user-selection-inner">

View File

@@ -82,7 +82,8 @@ body {
grid-column: 1/-1;
/* Position at the first row */
grid-row: 1;
z-index: 10;
/* Top menu bar dropdown needs to be above of graph canvas splitter overlay which is z-index: 999 */
z-index: 1000;
display: flex;
flex-direction: column;
}

View File

@@ -65,7 +65,7 @@
icon="pi pi-times"
severity="secondary"
:disabled="!executingPrompt"
@click="actions.interrupt"
@click="commandStore.getCommand('Comfy.Interrupt')"
></Button>
<ButtonGroup>
@@ -73,25 +73,19 @@
v-tooltip.bottom="$t('menu.refresh')"
icon="pi pi-refresh"
severity="secondary"
@click="actions.refresh"
@click="commandStore.getCommand('Comfy.RefreshNodeDefinitions')"
/>
<Button
v-tooltip.bottom="$t('menu.clipspace')"
icon="pi pi-clipboard"
severity="secondary"
@click="actions.openClipspace"
@click="commandStore.getCommand('Comfy.OpenClipspace')"
/>
<Button
v-tooltip.bottom="$t('menu.resetView')"
icon="pi pi-expand"
severity="secondary"
@click="actions.resetView"
/>
<Button
v-tooltip.bottom="$t('menu.clear')"
icon="pi pi-ban"
severity="secondary"
@click="actions.clearWorkflow"
@click="commandStore.getCommand('Comfy.ResetView')"
/>
</ButtonGroup>
</div>
@@ -115,12 +109,12 @@ import {
useQueueSettingsStore
} from '@/stores/queueStore'
import { app } from '@/scripts/app'
import { api } from '@/scripts/api'
import { storeToRefs } from 'pinia'
import { useSettingStore } from '@/stores/settingStore'
import { useToastStore } from '@/stores/toastStore'
import { useCommandStore } from '@/stores/commandStore'
const settingsStore = useSettingStore()
const commandStore = useCommandStore()
const queueCountStore = storeToRefs(useQueuePendingTaskCountStore())
const { batchCount, mode: queueMode } = storeToRefs(useQueueSettingsStore())
@@ -147,31 +141,6 @@ const executingPrompt = computed(() => !!queueCountStore.count.value)
const queuePrompt = (e: MouseEvent) => {
app.queuePrompt(e.shiftKey ? -1 : 0, batchCount.value)
}
const actions = {
interrupt: async () => {
await api.interrupt()
useToastStore().add({
severity: 'info',
summary: 'Interrupted',
detail: 'Execution has been interrupted',
life: 1000
})
},
clearWorkflow: () => {
if (
!(settingsStore.get('Comfy.ComfirmClear') ?? true) ||
confirm('Clear workflow?')
) {
app.clean()
app.graph.clear()
api.dispatchEvent(new CustomEvent('graphCleared'))
}
},
resetView: () => app.resetView(),
openClipspace: () => app['openClipspace'](),
refresh: () => app.refreshComboInNodes()
}
</script>
<style scoped>

View File

@@ -6,27 +6,27 @@
icon="pi pi-th-large"
v-tooltip="$t('sideToolbar.browseTemplates')"
text
@click="browseTemplates"
@click="() => commandStore.getCommand('Comfy.BrowseTemplates')()"
/>
<Button
class="browse-workflows-button"
icon="pi pi-folder-open"
v-tooltip="'Browse for an image or exported workflow'"
text
@click="browse"
@click="() => commandStore.getCommand('Comfy.OpenWorkflow')()"
/>
<Button
class="new-default-workflow-button"
icon="pi pi-code"
v-tooltip="'Load default workflow'"
text
@click="loadDefault"
@click="() => commandStore.getCommand('Comfy.LoadDefaultWorkflow')()"
/>
<Button
class="new-blank-workflow-button"
icon="pi pi-plus"
v-tooltip="'Create a new blank workflow'"
@click="createBlank"
@click="() => commandStore.getCommand('Comfy.NewBlankWorkflow')()"
text
/>
</template>
@@ -114,12 +114,12 @@ import TextDivider from '@/components/common/TextDivider.vue'
import { app } from '@/scripts/app'
import { computed, nextTick, ref } from 'vue'
import { useWorkflowStore } from '@/stores/workflowStore'
import { useCommandStore } from '@/stores/commandStore'
import type { TreeNode } from 'primevue/treenode'
import { TreeExplorerNode } from '@/types/treeExplorerTypes'
import { ComfyWorkflow } from '@/scripts/workflows'
import { useI18n } from 'vue-i18n'
import { useTreeExpansion } from '@/hooks/treeHooks'
import { showTemplateWorkflowsDialog } from '@/services/dialogService'
const searchQuery = ref('')
const isSearching = computed(() => searchQuery.value.length > 0)
@@ -144,25 +144,7 @@ const handleSearch = (query: string) => {
})
}
const loadDefault = () => {
app.loadGraphData()
app.resetView()
}
const browse = () => {
app.ui.loadFile()
}
const browseTemplates = () => {
showTemplateWorkflowsDialog()
}
const createBlank = () => {
app.workflowManager.setWorkflow(null)
app.clean()
app.graph.clear()
app.workflowManager.activeWorkflow.track()
}
const commandStore = useCommandStore()
const workflowStore = useWorkflowStore()
const { t } = useI18n()

View File

@@ -0,0 +1,58 @@
<template>
<teleport to=".comfyui-body-top">
<div class="top-menubar comfyui-menu" v-if="betaMenuEnabled">
<h1 class="comfyui-logo mx-2">ComfyUI</h1>
<Menubar
:model="items"
class="border-none p-0 bg-transparent"
:pt="{
rootList: 'gap-0'
}"
/>
</div>
</teleport>
</template>
<script setup lang="ts">
import Menubar from 'primevue/menubar'
import { useCoreMenuItemStore } from '@/stores/coreMenuItemStore'
import { computed } from 'vue'
import { useSettingStore } from '@/stores/settingStore'
const settingStore = useSettingStore()
const betaMenuEnabled = computed(
() => settingStore.get('Comfy.UseNewMenu') !== 'Disabled'
)
const coreMenuItemsStore = useCoreMenuItemStore()
const items = coreMenuItemsStore.menuItems
</script>
<style scoped>
.comfyui-menu {
width: 100vw;
background: var(--comfy-menu-bg);
color: var(--fg-color);
font-family: Arial, Helvetica, sans-serif;
font-size: 0.8em;
display: flex;
align-items: center;
box-sizing: border-box;
z-index: 1000;
order: 0;
grid-column: 1/-1;
overflow: auto;
max-height: 90vh;
}
.comfyui-logo {
font-size: 1.2em;
user-select: none;
cursor: default;
}
</style>
<style>
.top-menubar .p-menubar-item-link svg {
display: none;
}
</style>

View File

@@ -0,0 +1,16 @@
<template>
<div class="workflow-tabs">
<SelectButton
v-model="workflowStore.activeWorkflow"
:options="workflowStore.openWorkflows"
aria-labelledby="basic"
/>
</div>
</template>
<script setup lang="ts">
import { useWorkflowStore } from '@/stores/workflowStore'
import SelectButton from 'primevue/selectbutton'
const workflowStore = useWorkflowStore()
</script>

View File

@@ -7,8 +7,8 @@ import { ComfyWorkflow } from './workflows'
export class ChangeTracker {
static MAX_HISTORY = 50
#app: ComfyApp
undo = []
redo = []
undoQueue = []
redoQueue = []
activeState = null
isOurLoad = false
workflow: ComfyWorkflow | null
@@ -54,12 +54,12 @@ export class ChangeTracker {
return
}
if (!ChangeTracker.graphEqual(this.activeState, currentState)) {
this.undo.push(this.activeState)
if (this.undo.length > ChangeTracker.MAX_HISTORY) {
this.undo.shift()
this.undoQueue.push(this.activeState)
if (this.undoQueue.length > ChangeTracker.MAX_HISTORY) {
this.undoQueue.shift()
}
this.activeState = clone(currentState)
this.redo.length = 0
this.redoQueue.length = 0
this.workflow.unsaved = true
api.dispatchEvent(
new CustomEvent('graphChanged', { detail: this.activeState })
@@ -80,13 +80,21 @@ export class ChangeTracker {
}
}
async undo() {
await this.updateState(this.undoQueue, this.redoQueue)
}
async redo() {
await this.updateState(this.redoQueue, this.undoQueue)
}
async undoRedo(e) {
if (e.ctrlKey || e.metaKey) {
if (e.key === 'y') {
this.updateState(this.redo, this.undo)
await this.redo()
return true
} else if (e.key === 'z') {
this.updateState(this.undo, this.redo)
await this.undo()
return true
}
}
@@ -276,4 +284,4 @@ export class ChangeTracker {
}
}
const globalTracker = new ChangeTracker({} as ComfyWorkflow)
export const globalTracker = new ChangeTracker({} as ComfyWorkflow)

View File

@@ -122,24 +122,6 @@
}
/* Menu */
.comfyui-menu {
width: 100vw;
background: var(--comfy-menu-bg);
color: var(--fg-color);
font-family: Arial, Helvetica, sans-serif;
font-size: 0.8em;
display: flex;
padding: 4px 8px;
align-items: center;
gap: 8px;
box-sizing: border-box;
z-index: 1000;
order: 0;
grid-column: 1/-1;
overflow: auto;
max-height: 90vh;
}
.comfyui-menu>* {
flex-shrink: 0;
}
@@ -183,13 +165,6 @@
flex: auto;
}
.comfyui-logo {
font-size: 1.2em;
margin: 0;
user-select: none;
cursor: default;
}
/** Send to workflow widget selection dialog */
.comfy-widget-selection-dialog {
border: none;

View File

@@ -0,0 +1,87 @@
import { app } from '@/scripts/app'
import { api } from '@/scripts/api'
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { globalTracker } from '@/scripts/changeTracker'
import { useSettingStore } from '@/stores/settingStore'
import { useToastStore } from '@/stores/toastStore'
import { showTemplateWorkflowsDialog } from '@/services/dialogService'
type Command = () => void | Promise<void>
const getTracker = () =>
app.workflowManager.activeWorkflow?.changeTracker ?? globalTracker
export const useCommandStore = defineStore('command', () => {
const settingStore = useSettingStore()
const commands = ref<Record<string, Command>>({
'Comfy.NewBlankWorkflow': () => {
app.workflowManager.setWorkflow(null)
app.clean()
app.graph.clear()
app.workflowManager.activeWorkflow.track()
},
'Comfy.OpenWorkflow': () => {
app.ui.loadFile()
},
'Comfy.LoadDefaultWorkflow': async () => {
await app.loadGraphData()
},
'Comfy.SaveWorkflow': () => {
app.workflowManager.activeWorkflow.save()
},
'Comfy.SaveWorkflowAs': () => {
app.workflowManager.activeWorkflow.save(true)
},
'Comfy.ExportWorkflow': () => {
app.menu.exportWorkflow('workflow', 'workflow')
},
'Comfy.ExportWorkflowAPI': () => {
app.menu.exportWorkflow('workflow_api', 'output')
},
'Comfy.Undo': async () => {
await getTracker().undo()
},
'Comfy.Redo': async () => {
await getTracker().redo()
},
'Comfy.ClearWorkflow': () => {
if (
!settingStore.get('Comfy.ComfirmClear') ||
confirm('Clear workflow?')
) {
app.clean()
app.graph.clear()
api.dispatchEvent(new CustomEvent('graphCleared'))
}
},
'Comfy.ResetView': () => {
app.resetView()
},
'Comfy.OpenClipspace': () => {
app['openClipspace']?.()
},
'Comfy.RefreshNodeDefinitions': async () => {
await app.refreshComboInNodes()
},
'Comfy.Interrupt': async () => {
await api.interrupt()
useToastStore().add({
severity: 'info',
summary: 'Interrupted',
detail: 'Execution has been interrupted',
life: 1000
})
},
'Comfy.BrowseTemplates': showTemplateWorkflowsDialog
})
const getCommand = (command: string) => {
return commands.value[command] ?? (() => {})
}
return {
commands,
getCommand
}
})

View File

@@ -0,0 +1,85 @@
import { defineStore } from 'pinia'
import type { MenuItem } from 'primevue/menuitem'
import { computed } from 'vue'
import { useCommandStore } from './commandStore'
export const useCoreMenuItemStore = defineStore('coreMenuItem', () => {
const commandStore = useCommandStore()
const menuItems = computed<MenuItem[]>(() => {
return [
{
label: 'Workflow',
items: [
{
label: 'New',
icon: 'pi pi-plus',
command: commandStore.commands['Comfy.NewBlankWorkflow']
},
{
separator: true
},
{
label: 'Open',
icon: 'pi pi-folder-open',
command: commandStore.commands['Comfy.OpenWorkflow']
},
{
label: 'Browse Templates',
icon: 'pi pi-th-large',
command: commandStore.commands['Comfy.BrowseTemplates']
},
{
separator: true
},
{
label: 'Save',
icon: 'pi pi-save',
command: commandStore.commands['Comfy.SaveWorkflow']
},
{
label: 'Save As',
icon: 'pi pi-save',
command: commandStore.commands['Comfy.SaveWorkflowAs']
},
{
label: 'Export',
icon: 'pi pi-download',
command: commandStore.commands['Comfy.ExportWorkflow']
},
{
label: 'Export (API Format)',
icon: 'pi pi-download',
command: commandStore.commands['Comfy.ExportWorkflowAPI']
}
]
},
{
label: 'Edit',
items: [
{
label: 'Undo',
icon: 'pi pi-undo',
command: commandStore.commands['Comfy.Undo']
},
{
label: 'Redo',
icon: 'pi pi-refresh',
command: commandStore.commands['Comfy.Redo']
},
{
separator: true
},
{
label: 'Clear Workflow',
icon: 'pi pi-trash',
command: commandStore.commands['Comfy.ClearWorkflow']
}
]
}
]
})
return {
menuItems
}
})

View File

@@ -4,6 +4,7 @@
<UnloadWindowConfirmDialog />
<BrowserTabTitle />
<AppMenu />
<TopMenubar />
</template>
<script setup lang="ts">
@@ -40,6 +41,7 @@ import UnloadWindowConfirmDialog from '@/components/dialog/UnloadWindowConfirmDi
import BrowserTabTitle from '@/components/BrowserTabTitle.vue'
import AppMenu from '@/components/appMenu/AppMenu.vue'
import WorkflowsSidebarTab from '@/components/sidebar/tabs/WorkflowsSidebarTab.vue'
import TopMenubar from '@/components/topbar/TopMenubar.vue'
import { setupAutoQueueHandler } from '@/services/autoQueueService'
setupAutoQueueHandler()
@@ -91,16 +93,10 @@ watchEffect(() => {
watchEffect(() => {
const useNewMenu = settingStore.get('Comfy.UseNewMenu')
if (useNewMenu === 'Disabled') {
app.ui.menuContainer.style.removeProperty('display')
app.ui.restoreMenuPosition()
document.body.style.removeProperty('display')
if (app.ui.menuContainer) {
app.ui.menuContainer.style.removeProperty('display')
}
} else {
document.body.style.setProperty('display', 'grid')
if (app.ui.menuContainer) {
app.ui.menuContainer.style.setProperty('display', 'none')
}
app.ui.menuContainer.style.setProperty('display', 'none')
}
})