mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 03:01:54 +00:00
Show keybinding on topbar dropdown menus (#1127)
* Show keybinding on topbar dropdown menus, resolve #1092 * Add text-muted to tailwind config * Add Playwright test * Preserve Primevue classes in menu item template * Extend MenuItem * Revert adding undo/redo to core keybindings * Change test selector * refactor * Extract as component * refactor * nit * fix extension API --------- Co-authored-by: huchenlei <huchenlei@proton.me>
This commit is contained in:
@@ -499,6 +499,16 @@ test.describe('Menu', () => {
|
|||||||
})
|
})
|
||||||
expect(isTextCutoff).toBe(false)
|
expect(isTextCutoff).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('Displays keybinding next to item', async ({ comfyPage }) => {
|
||||||
|
const workflowMenuItem =
|
||||||
|
await comfyPage.menu.topbar.getMenuItem('Workflow')
|
||||||
|
await workflowMenuItem.click()
|
||||||
|
const exportTag = comfyPage.page.locator('.keybinding-tag', {
|
||||||
|
hasText: 'Ctrl + s'
|
||||||
|
})
|
||||||
|
expect(await exportTag.count()).toBe(1)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Only test 'Top' to reduce test time.
|
// Only test 'Top' to reduce test time.
|
||||||
|
|||||||
@@ -47,6 +47,7 @@
|
|||||||
import SplitButton from 'primevue/splitbutton'
|
import SplitButton from 'primevue/splitbutton'
|
||||||
import Button from 'primevue/button'
|
import Button from 'primevue/button'
|
||||||
import BatchCountEdit from './BatchCountEdit.vue'
|
import BatchCountEdit from './BatchCountEdit.vue'
|
||||||
|
import ButtonGroup from 'primevue/buttongroup'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import {
|
import {
|
||||||
AutoQueueMode,
|
AutoQueueMode,
|
||||||
|
|||||||
55
src/components/topbar/CommandMenubar.vue
Normal file
55
src/components/topbar/CommandMenubar.vue
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<template>
|
||||||
|
<Menubar
|
||||||
|
:model="items"
|
||||||
|
class="top-menubar border-none p-0 bg-transparent"
|
||||||
|
:pt="{
|
||||||
|
rootList: 'gap-0 flex-nowrap w-auto',
|
||||||
|
submenu: `dropdown-direction-${dropdownDirection}`,
|
||||||
|
item: 'relative'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #item="{ item, props }">
|
||||||
|
<a class="p-menubar-item-link" v-bind="props.action">
|
||||||
|
<span v-if="item.icon" class="p-menubar-item-icon" :class="item.icon" />
|
||||||
|
<span class="p-menubar-item-label">{{ item.label }}</span>
|
||||||
|
<span
|
||||||
|
v-if="item?.comfyCommand?.keybinding"
|
||||||
|
class="ml-auto border border-surface rounded text-muted text-xs p-1 keybinding-tag"
|
||||||
|
>
|
||||||
|
{{ item.comfyCommand.keybinding.combo.toString() }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
</Menubar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useMenuItemStore } from '@/stores/menuItemStore'
|
||||||
|
import { useSettingStore } from '@/stores/settingStore'
|
||||||
|
import Menubar from 'primevue/menubar'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
const settingStore = useSettingStore()
|
||||||
|
const dropdownDirection = computed(() =>
|
||||||
|
settingStore.get('Comfy.UseNewMenu') === 'Top' ? 'down' : 'up'
|
||||||
|
)
|
||||||
|
|
||||||
|
const menuItemsStore = useMenuItemStore()
|
||||||
|
const items = menuItemsStore.menuItems
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.top-menubar :deep(.p-menubar-item-link) svg {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.p-menubar-submenu.dropdown-direction-up) {
|
||||||
|
@apply top-auto bottom-full flex-col-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keybinding-tag {
|
||||||
|
background: var(--p-content-hover-background);
|
||||||
|
border-color: var(--p-content-border-color);
|
||||||
|
border-style: solid;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -7,15 +7,7 @@
|
|||||||
:class="{ dropzone: isDropZone, 'dropzone-active': isDroppable }"
|
:class="{ dropzone: isDropZone, 'dropzone-active': isDroppable }"
|
||||||
>
|
>
|
||||||
<h1 class="comfyui-logo mx-2">ComfyUI</h1>
|
<h1 class="comfyui-logo mx-2">ComfyUI</h1>
|
||||||
<Menubar
|
<CommandMenubar />
|
||||||
:model="items"
|
|
||||||
class="top-menubar border-none p-0 bg-transparent"
|
|
||||||
:pt="{
|
|
||||||
rootList: 'gap-0 flex-nowrap w-auto',
|
|
||||||
submenu: `dropdown-direction-${dropdownDirection}`,
|
|
||||||
item: 'relative'
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
<Divider layout="vertical" class="mx-2" />
|
<Divider layout="vertical" class="mx-2" />
|
||||||
<div class="flex-grow">
|
<div class="flex-grow">
|
||||||
<WorkflowTabs v-if="workflowTabsPosition === 'Topbar'" />
|
<WorkflowTabs v-if="workflowTabsPosition === 'Topbar'" />
|
||||||
@@ -27,11 +19,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Menubar from 'primevue/menubar'
|
|
||||||
import Divider from 'primevue/divider'
|
import Divider from 'primevue/divider'
|
||||||
import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
|
import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
|
||||||
|
import CommandMenubar from '@/components/topbar/CommandMenubar.vue'
|
||||||
import Actionbar from '@/components/actionbar/ComfyActionbar.vue'
|
import Actionbar from '@/components/actionbar/ComfyActionbar.vue'
|
||||||
import { useMenuItemStore } from '@/stores/menuItemStore'
|
|
||||||
import { computed, onMounted, provide, ref } from 'vue'
|
import { computed, onMounted, provide, ref } from 'vue'
|
||||||
import { useSettingStore } from '@/stores/settingStore'
|
import { useSettingStore } from '@/stores/settingStore'
|
||||||
import { app } from '@/scripts/app'
|
import { app } from '@/scripts/app'
|
||||||
@@ -49,12 +40,6 @@ const teleportTarget = computed(() =>
|
|||||||
? '.comfyui-body-top'
|
? '.comfyui-body-top'
|
||||||
: '.comfyui-body-bottom'
|
: '.comfyui-body-bottom'
|
||||||
)
|
)
|
||||||
const dropdownDirection = computed(() =>
|
|
||||||
settingStore.get('Comfy.UseNewMenu') === 'Top' ? 'down' : 'up'
|
|
||||||
)
|
|
||||||
|
|
||||||
const menuItemsStore = useMenuItemStore()
|
|
||||||
const items = menuItemsStore.menuItems
|
|
||||||
|
|
||||||
const menuRight = ref<HTMLDivElement | null>(null)
|
const menuRight = ref<HTMLDivElement | null>(null)
|
||||||
// Menu-right holds legacy topbar elements attached by custom scripts
|
// Menu-right holds legacy topbar elements attached by custom scripts
|
||||||
@@ -105,13 +90,3 @@ eventBus.on((event: string, payload: any) => {
|
|||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style>
|
|
||||||
.top-menubar .p-menubar-item-link svg {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.p-menubar-submenu.dropdown-direction-up {
|
|
||||||
@apply top-auto bottom-full flex-col-reverse;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { LGraphGroup } from '@comfyorg/litegraph'
|
|||||||
import { useTitleEditorStore } from './graphStore'
|
import { useTitleEditorStore } from './graphStore'
|
||||||
import { useErrorHandling } from '@/hooks/errorHooks'
|
import { useErrorHandling } from '@/hooks/errorHooks'
|
||||||
import { useWorkflowStore } from './workflowStore'
|
import { useWorkflowStore } from './workflowStore'
|
||||||
|
import { type KeybindingImpl, useKeybindingStore } from './keybindingStore'
|
||||||
|
|
||||||
export interface ComfyCommand {
|
export interface ComfyCommand {
|
||||||
id: string
|
id: string
|
||||||
@@ -24,23 +25,67 @@ export interface ComfyCommand {
|
|||||||
label?: string | (() => string)
|
label?: string | (() => string)
|
||||||
icon?: string | (() => string)
|
icon?: string | (() => string)
|
||||||
tooltip?: string | (() => string)
|
tooltip?: string | (() => string)
|
||||||
|
/** Menubar item label, if different from command label */
|
||||||
|
menubarLabel?: string | (() => string)
|
||||||
versionAdded?: string
|
versionAdded?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class ComfyCommandImpl implements ComfyCommand {
|
||||||
|
id: string
|
||||||
|
function: () => void | Promise<void>
|
||||||
|
_label?: string | (() => string)
|
||||||
|
_icon?: string | (() => string)
|
||||||
|
_tooltip?: string | (() => string)
|
||||||
|
_menubarLabel?: string | (() => string)
|
||||||
|
versionAdded?: string
|
||||||
|
|
||||||
|
constructor(command: ComfyCommand) {
|
||||||
|
this.id = command.id
|
||||||
|
this.function = command.function
|
||||||
|
this._label = command.label
|
||||||
|
this._icon = command.icon
|
||||||
|
this._tooltip = command.tooltip
|
||||||
|
this._menubarLabel = command.menubarLabel ?? command.label
|
||||||
|
this.versionAdded = command.versionAdded
|
||||||
|
}
|
||||||
|
|
||||||
|
get label() {
|
||||||
|
return typeof this._label === 'function' ? this._label() : this._label
|
||||||
|
}
|
||||||
|
|
||||||
|
get icon() {
|
||||||
|
return typeof this._icon === 'function' ? this._icon() : this._icon
|
||||||
|
}
|
||||||
|
|
||||||
|
get tooltip() {
|
||||||
|
return typeof this._tooltip === 'function' ? this._tooltip() : this._tooltip
|
||||||
|
}
|
||||||
|
|
||||||
|
get menubarLabel() {
|
||||||
|
return typeof this._menubarLabel === 'function'
|
||||||
|
? this._menubarLabel()
|
||||||
|
: this._menubarLabel
|
||||||
|
}
|
||||||
|
|
||||||
|
get keybinding(): KeybindingImpl | null {
|
||||||
|
return useKeybindingStore().getKeybindingByCommandId(this.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const getTracker = () =>
|
const getTracker = () =>
|
||||||
app.workflowManager.activeWorkflow?.changeTracker ?? globalTracker
|
app.workflowManager.activeWorkflow?.changeTracker ?? globalTracker
|
||||||
|
|
||||||
export const useCommandStore = defineStore('command', () => {
|
export const useCommandStore = defineStore('command', () => {
|
||||||
const settingStore = useSettingStore()
|
const settingStore = useSettingStore()
|
||||||
|
|
||||||
const commandsById = ref<Record<string, ComfyCommand>>({})
|
const commandsById = ref<Record<string, ComfyCommandImpl>>({})
|
||||||
const commands = computed(() => Object.values(commandsById.value))
|
const commands = computed(() => Object.values(commandsById.value))
|
||||||
|
|
||||||
const registerCommand = (command: ComfyCommand) => {
|
const registerCommand = (command: ComfyCommand) => {
|
||||||
if (commandsById.value[command.id]) {
|
if (commandsById.value[command.id]) {
|
||||||
console.warn(`Command ${command.id} already registered`)
|
console.warn(`Command ${command.id} already registered`)
|
||||||
}
|
}
|
||||||
commandsById.value[command.id] = command
|
commandsById.value[command.id] = new ComfyCommandImpl(command)
|
||||||
}
|
}
|
||||||
|
|
||||||
const commandDefinitions: ComfyCommand[] = [
|
const commandDefinitions: ComfyCommand[] = [
|
||||||
@@ -48,6 +93,7 @@ export const useCommandStore = defineStore('command', () => {
|
|||||||
id: 'Comfy.NewBlankWorkflow',
|
id: 'Comfy.NewBlankWorkflow',
|
||||||
icon: 'pi pi-plus',
|
icon: 'pi pi-plus',
|
||||||
label: 'New Blank Workflow',
|
label: 'New Blank Workflow',
|
||||||
|
menubarLabel: 'New',
|
||||||
function: () => {
|
function: () => {
|
||||||
app.workflowManager.setWorkflow(null)
|
app.workflowManager.setWorkflow(null)
|
||||||
app.clean()
|
app.clean()
|
||||||
@@ -59,6 +105,7 @@ export const useCommandStore = defineStore('command', () => {
|
|||||||
id: 'Comfy.OpenWorkflow',
|
id: 'Comfy.OpenWorkflow',
|
||||||
icon: 'pi pi-folder-open',
|
icon: 'pi pi-folder-open',
|
||||||
label: 'Open Workflow',
|
label: 'Open Workflow',
|
||||||
|
menubarLabel: 'Open',
|
||||||
function: () => {
|
function: () => {
|
||||||
app.ui.loadFile()
|
app.ui.loadFile()
|
||||||
}
|
}
|
||||||
@@ -75,6 +122,7 @@ export const useCommandStore = defineStore('command', () => {
|
|||||||
id: 'Comfy.SaveWorkflow',
|
id: 'Comfy.SaveWorkflow',
|
||||||
icon: 'pi pi-save',
|
icon: 'pi pi-save',
|
||||||
label: 'Save Workflow',
|
label: 'Save Workflow',
|
||||||
|
menubarLabel: 'Save',
|
||||||
function: () => {
|
function: () => {
|
||||||
app.workflowManager.activeWorkflow.save()
|
app.workflowManager.activeWorkflow.save()
|
||||||
}
|
}
|
||||||
@@ -83,6 +131,7 @@ export const useCommandStore = defineStore('command', () => {
|
|||||||
id: 'Comfy.SaveWorkflowAs',
|
id: 'Comfy.SaveWorkflowAs',
|
||||||
icon: 'pi pi-save',
|
icon: 'pi pi-save',
|
||||||
label: 'Save Workflow As',
|
label: 'Save Workflow As',
|
||||||
|
menubarLabel: 'Save As',
|
||||||
function: () => {
|
function: () => {
|
||||||
app.workflowManager.activeWorkflow.save(true)
|
app.workflowManager.activeWorkflow.save(true)
|
||||||
}
|
}
|
||||||
@@ -91,6 +140,7 @@ export const useCommandStore = defineStore('command', () => {
|
|||||||
id: 'Comfy.ExportWorkflow',
|
id: 'Comfy.ExportWorkflow',
|
||||||
icon: 'pi pi-download',
|
icon: 'pi pi-download',
|
||||||
label: 'Export Workflow',
|
label: 'Export Workflow',
|
||||||
|
menubarLabel: 'Export',
|
||||||
function: () => {
|
function: () => {
|
||||||
app.menu.exportWorkflow('workflow', 'workflow')
|
app.menu.exportWorkflow('workflow', 'workflow')
|
||||||
}
|
}
|
||||||
@@ -99,6 +149,7 @@ export const useCommandStore = defineStore('command', () => {
|
|||||||
id: 'Comfy.ExportWorkflowAPI',
|
id: 'Comfy.ExportWorkflowAPI',
|
||||||
icon: 'pi pi-download',
|
icon: 'pi pi-download',
|
||||||
label: 'Export Workflow (API Format)',
|
label: 'Export Workflow (API Format)',
|
||||||
|
menubarLabel: 'Export (API)',
|
||||||
function: () => {
|
function: () => {
|
||||||
app.menu.exportWorkflow('workflow_api', 'output')
|
app.menu.exportWorkflow('workflow_api', 'output')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,61 +51,44 @@ export const useMenuItemStore = defineStore('menuItem', () => {
|
|||||||
commandStore.registerCommand(command)
|
commandStore.registerCommand(command)
|
||||||
}
|
}
|
||||||
|
|
||||||
const items = commands.map(
|
const items = commands
|
||||||
(command) =>
|
// Convert command to commandImpl
|
||||||
({
|
.map((command) => commandStore.getCommand(command.id))
|
||||||
...command,
|
.map(
|
||||||
command: command.function
|
(command) =>
|
||||||
}) as MenuItem
|
({
|
||||||
)
|
command: command.function,
|
||||||
|
label: command.menubarLabel,
|
||||||
|
icon: command.icon,
|
||||||
|
tooltip: command.tooltip,
|
||||||
|
comfyCommand: command
|
||||||
|
}) as MenuItem
|
||||||
|
)
|
||||||
registerMenuGroup(path, items)
|
registerMenuGroup(path, items)
|
||||||
}
|
}
|
||||||
|
|
||||||
const workflowMenuGroup: MenuItem[] = [
|
registerCommands(
|
||||||
{
|
['Workflow'],
|
||||||
label: 'New',
|
[commandStore.getCommand('Comfy.NewBlankWorkflow')]
|
||||||
icon: 'pi pi-plus',
|
)
|
||||||
command: () => commandStore.execute('Comfy.NewBlankWorkflow')
|
|
||||||
},
|
registerCommands(
|
||||||
{
|
['Workflow'],
|
||||||
separator: true
|
[
|
||||||
},
|
commandStore.getCommand('Comfy.OpenWorkflow'),
|
||||||
{
|
commandStore.getCommand('Comfy.BrowseTemplates')
|
||||||
label: 'Open',
|
]
|
||||||
icon: 'pi pi-folder-open',
|
)
|
||||||
command: () => commandStore.execute('Comfy.OpenWorkflow')
|
registerCommands(
|
||||||
},
|
['Workflow'],
|
||||||
{
|
[
|
||||||
label: 'Browse Templates',
|
commandStore.getCommand('Comfy.SaveWorkflow'),
|
||||||
icon: 'pi pi-th-large',
|
commandStore.getCommand('Comfy.SaveWorkflowAs'),
|
||||||
command: () => commandStore.execute('Comfy.BrowseTemplates')
|
commandStore.getCommand('Comfy.ExportWorkflow'),
|
||||||
},
|
commandStore.getCommand('Comfy.ExportWorkflowAPI')
|
||||||
{
|
]
|
||||||
separator: true
|
)
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Save',
|
|
||||||
icon: 'pi pi-save',
|
|
||||||
command: () => commandStore.execute('Comfy.SaveWorkflow')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Save As',
|
|
||||||
icon: 'pi pi-save',
|
|
||||||
command: () => commandStore.execute('Comfy.SaveWorkflowAs')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Export',
|
|
||||||
icon: 'pi pi-download',
|
|
||||||
command: () => commandStore.execute('Comfy.ExportWorkflow')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Export (API Format)',
|
|
||||||
icon: 'pi pi-download',
|
|
||||||
command: () => commandStore.execute('Comfy.ExportWorkflowAPI')
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
registerMenuGroup(['Workflow'], workflowMenuGroup)
|
|
||||||
registerCommands(
|
registerCommands(
|
||||||
['Edit'],
|
['Edit'],
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -174,6 +174,10 @@ export default {
|
|||||||
900: '#7b341e',
|
900: '#7b341e',
|
||||||
950: '#431407'
|
950: '#431407'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
textColor: {
|
||||||
|
muted: 'var(--p-text-muted-color)'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user