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:
bymyself
2024-10-06 09:08:16 -07:00
committed by GitHub
parent 1b3cc4de1a
commit 3c70c1e463
7 changed files with 159 additions and 80 deletions

View File

@@ -499,6 +499,16 @@ test.describe('Menu', () => {
})
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.

View File

@@ -47,6 +47,7 @@
import SplitButton from 'primevue/splitbutton'
import Button from 'primevue/button'
import BatchCountEdit from './BatchCountEdit.vue'
import ButtonGroup from 'primevue/buttongroup'
import { useI18n } from 'vue-i18n'
import {
AutoQueueMode,

View 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>

View File

@@ -7,15 +7,7 @@
:class="{ dropzone: isDropZone, 'dropzone-active': isDroppable }"
>
<h1 class="comfyui-logo mx-2">ComfyUI</h1>
<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'
}"
/>
<CommandMenubar />
<Divider layout="vertical" class="mx-2" />
<div class="flex-grow">
<WorkflowTabs v-if="workflowTabsPosition === 'Topbar'" />
@@ -27,11 +19,10 @@
</template>
<script setup lang="ts">
import Menubar from 'primevue/menubar'
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 { useMenuItemStore } from '@/stores/menuItemStore'
import { computed, onMounted, provide, ref } from 'vue'
import { useSettingStore } from '@/stores/settingStore'
import { app } from '@/scripts/app'
@@ -49,12 +40,6 @@ const teleportTarget = computed(() =>
? '.comfyui-body-top'
: '.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)
// Menu-right holds legacy topbar elements attached by custom scripts
@@ -105,13 +90,3 @@ eventBus.on((event: string, payload: any) => {
cursor: default;
}
</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>

View File

@@ -16,6 +16,7 @@ import { LGraphGroup } from '@comfyorg/litegraph'
import { useTitleEditorStore } from './graphStore'
import { useErrorHandling } from '@/hooks/errorHooks'
import { useWorkflowStore } from './workflowStore'
import { type KeybindingImpl, useKeybindingStore } from './keybindingStore'
export interface ComfyCommand {
id: string
@@ -24,23 +25,67 @@ export interface ComfyCommand {
label?: string | (() => string)
icon?: string | (() => string)
tooltip?: string | (() => string)
/** Menubar item label, if different from command label */
menubarLabel?: string | (() => 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 = () =>
app.workflowManager.activeWorkflow?.changeTracker ?? globalTracker
export const useCommandStore = defineStore('command', () => {
const settingStore = useSettingStore()
const commandsById = ref<Record<string, ComfyCommand>>({})
const commandsById = ref<Record<string, ComfyCommandImpl>>({})
const commands = computed(() => Object.values(commandsById.value))
const registerCommand = (command: ComfyCommand) => {
if (commandsById.value[command.id]) {
console.warn(`Command ${command.id} already registered`)
}
commandsById.value[command.id] = command
commandsById.value[command.id] = new ComfyCommandImpl(command)
}
const commandDefinitions: ComfyCommand[] = [
@@ -48,6 +93,7 @@ export const useCommandStore = defineStore('command', () => {
id: 'Comfy.NewBlankWorkflow',
icon: 'pi pi-plus',
label: 'New Blank Workflow',
menubarLabel: 'New',
function: () => {
app.workflowManager.setWorkflow(null)
app.clean()
@@ -59,6 +105,7 @@ export const useCommandStore = defineStore('command', () => {
id: 'Comfy.OpenWorkflow',
icon: 'pi pi-folder-open',
label: 'Open Workflow',
menubarLabel: 'Open',
function: () => {
app.ui.loadFile()
}
@@ -75,6 +122,7 @@ export const useCommandStore = defineStore('command', () => {
id: 'Comfy.SaveWorkflow',
icon: 'pi pi-save',
label: 'Save Workflow',
menubarLabel: 'Save',
function: () => {
app.workflowManager.activeWorkflow.save()
}
@@ -83,6 +131,7 @@ export const useCommandStore = defineStore('command', () => {
id: 'Comfy.SaveWorkflowAs',
icon: 'pi pi-save',
label: 'Save Workflow As',
menubarLabel: 'Save As',
function: () => {
app.workflowManager.activeWorkflow.save(true)
}
@@ -91,6 +140,7 @@ export const useCommandStore = defineStore('command', () => {
id: 'Comfy.ExportWorkflow',
icon: 'pi pi-download',
label: 'Export Workflow',
menubarLabel: 'Export',
function: () => {
app.menu.exportWorkflow('workflow', 'workflow')
}
@@ -99,6 +149,7 @@ export const useCommandStore = defineStore('command', () => {
id: 'Comfy.ExportWorkflowAPI',
icon: 'pi pi-download',
label: 'Export Workflow (API Format)',
menubarLabel: 'Export (API)',
function: () => {
app.menu.exportWorkflow('workflow_api', 'output')
}

View File

@@ -51,61 +51,44 @@ export const useMenuItemStore = defineStore('menuItem', () => {
commandStore.registerCommand(command)
}
const items = commands.map(
(command) =>
({
...command,
command: command.function
}) as MenuItem
)
const items = commands
// Convert command to commandImpl
.map((command) => commandStore.getCommand(command.id))
.map(
(command) =>
({
command: command.function,
label: command.menubarLabel,
icon: command.icon,
tooltip: command.tooltip,
comfyCommand: command
}) as MenuItem
)
registerMenuGroup(path, items)
}
const workflowMenuGroup: MenuItem[] = [
{
label: 'New',
icon: 'pi pi-plus',
command: () => commandStore.execute('Comfy.NewBlankWorkflow')
},
{
separator: true
},
{
label: 'Open',
icon: 'pi pi-folder-open',
command: () => commandStore.execute('Comfy.OpenWorkflow')
},
{
label: 'Browse Templates',
icon: 'pi pi-th-large',
command: () => commandStore.execute('Comfy.BrowseTemplates')
},
{
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')
}
]
registerCommands(
['Workflow'],
[commandStore.getCommand('Comfy.NewBlankWorkflow')]
)
registerCommands(
['Workflow'],
[
commandStore.getCommand('Comfy.OpenWorkflow'),
commandStore.getCommand('Comfy.BrowseTemplates')
]
)
registerCommands(
['Workflow'],
[
commandStore.getCommand('Comfy.SaveWorkflow'),
commandStore.getCommand('Comfy.SaveWorkflowAs'),
commandStore.getCommand('Comfy.ExportWorkflow'),
commandStore.getCommand('Comfy.ExportWorkflowAPI')
]
)
registerMenuGroup(['Workflow'], workflowMenuGroup)
registerCommands(
['Edit'],
[

View File

@@ -174,6 +174,10 @@ export default {
900: '#7b341e',
950: '#431407'
}
},
textColor: {
muted: 'var(--p-text-muted-color)'
}
}
},