mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-11 10:30:10 +00:00
[Desktop] Native window in graph view (#2216)
This commit is contained in:
@@ -765,6 +765,7 @@ audio.comfy-audio.empty-audio-widget {
|
||||
padding: var(--comfy-tree-explorer-item-padding) !important;
|
||||
}
|
||||
|
||||
/* [Desktop] Electron window specific styles */
|
||||
.app-drag {
|
||||
app-region: drag;
|
||||
}
|
||||
@@ -772,3 +773,8 @@ audio.comfy-audio.empty-audio-widget {
|
||||
.no-drag {
|
||||
app-region: no-drag;
|
||||
}
|
||||
|
||||
.window-actions-spacer {
|
||||
width: calc(100vw - env(titlebar-area-width, 100vw));
|
||||
}
|
||||
/* End of [Desktop] Electron window specific styles */
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
<template>
|
||||
<Button
|
||||
<div
|
||||
v-show="workspaceState.focusMode"
|
||||
class="comfy-menu-hamburger"
|
||||
class="comfy-menu-hamburger no-drag"
|
||||
:style="positionCSS"
|
||||
icon="pi pi-bars"
|
||||
severity="secondary"
|
||||
text
|
||||
size="large"
|
||||
v-tooltip="{ value: $t('menu.showMenu'), showDelay: 300 }"
|
||||
:aria-label="$t('menu.showMenu')"
|
||||
aria-live="assertive"
|
||||
@click="exitFocusMode"
|
||||
@contextmenu="showNativeMenu"
|
||||
/>
|
||||
>
|
||||
<Button
|
||||
icon="pi pi-bars"
|
||||
severity="secondary"
|
||||
text
|
||||
size="large"
|
||||
v-tooltip="{ value: $t('menu.showMenu'), showDelay: 300 }"
|
||||
:aria-label="$t('menu.showMenu')"
|
||||
aria-live="assertive"
|
||||
@click="exitFocusMode"
|
||||
@contextmenu="showNativeMenu"
|
||||
/>
|
||||
<div v-show="menuSetting !== 'Bottom'" class="window-actions-spacer" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -53,8 +57,6 @@ const positionCSS = computed<CSSProperties>(() =>
|
||||
|
||||
<style scoped>
|
||||
.comfy-menu-hamburger {
|
||||
pointer-events: auto;
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
@apply pointer-events-auto fixed z-[9999] flex flex-row;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
<div
|
||||
ref="topMenuRef"
|
||||
class="comfyui-menu flex items-center"
|
||||
v-show="betaMenuEnabled && !workspaceState.focusMode"
|
||||
v-show="showTopMenu"
|
||||
:class="{ dropzone: isDropZone, 'dropzone-active': isDroppable }"
|
||||
>
|
||||
<h1 class="comfyui-logo mx-2">ComfyUI</h1>
|
||||
<h1 class="comfyui-logo mx-2 app-drag">ComfyUI</h1>
|
||||
<CommandMenubar />
|
||||
<Divider layout="vertical" class="mx-2" />
|
||||
<div class="flex-grow min-w-0">
|
||||
@@ -25,8 +25,18 @@
|
||||
@click="workspaceState.focusMode = true"
|
||||
@contextmenu="showNativeMenu"
|
||||
/>
|
||||
<div
|
||||
v-show="menuSetting !== 'Bottom'"
|
||||
class="window-actions-spacer flex-shrink-0"
|
||||
/>
|
||||
</div>
|
||||
</teleport>
|
||||
|
||||
<!-- Virtual top menu for native window (drag handle) -->
|
||||
<div
|
||||
v-show="isNativeWindow && !showTopMenu"
|
||||
class="fixed top-0 left-0 app-drag w-full h-[var(--comfy-topbar-height)]"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -49,14 +59,20 @@ const settingStore = useSettingStore()
|
||||
const workflowTabsPosition = computed(() =>
|
||||
settingStore.get('Comfy.Workflow.WorkflowTabsPosition')
|
||||
)
|
||||
const betaMenuEnabled = computed(
|
||||
() => settingStore.get('Comfy.UseNewMenu') !== 'Disabled'
|
||||
)
|
||||
const menuSetting = computed(() => settingStore.get('Comfy.UseNewMenu'))
|
||||
const betaMenuEnabled = computed(() => menuSetting.value !== 'Disabled')
|
||||
const teleportTarget = computed(() =>
|
||||
settingStore.get('Comfy.UseNewMenu') === 'Top'
|
||||
? '.comfyui-body-top'
|
||||
: '.comfyui-body-bottom'
|
||||
)
|
||||
const isNativeWindow = computed(
|
||||
() =>
|
||||
isElectron() && settingStore.get('Comfy-Desktop.WindowStyle') === 'custom'
|
||||
)
|
||||
const showTopMenu = computed(
|
||||
() => betaMenuEnabled.value && !workspaceState.focusMode
|
||||
)
|
||||
|
||||
const menuRight = ref<HTMLDivElement | null>(null)
|
||||
// Menu-right holds legacy topbar elements attached by custom scripts
|
||||
|
||||
@@ -411,223 +411,231 @@ export class ComfyUI {
|
||||
}
|
||||
})
|
||||
|
||||
this.menuContainer = $el('div.comfy-menu', { parent: containerElement }, [
|
||||
$el(
|
||||
'div.drag-handle.comfy-menu-header',
|
||||
{
|
||||
style: {
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
cursor: 'default'
|
||||
}
|
||||
},
|
||||
[
|
||||
$el('span.drag-handle'),
|
||||
$el('span.comfy-menu-queue-size', { $: (q) => (this.queueSize = q) }),
|
||||
$el('div.comfy-menu-actions', [
|
||||
$el('button.comfy-settings-btn', {
|
||||
textContent: '⚙️',
|
||||
onclick: () => {
|
||||
useDialogService().showSettingsDialog()
|
||||
}
|
||||
this.menuContainer = $el(
|
||||
'div.comfy-menu.no-drag',
|
||||
{ parent: containerElement },
|
||||
[
|
||||
$el(
|
||||
'div.drag-handle.comfy-menu-header',
|
||||
{
|
||||
style: {
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
cursor: 'default'
|
||||
}
|
||||
},
|
||||
[
|
||||
$el('span.drag-handle'),
|
||||
$el('span.comfy-menu-queue-size', {
|
||||
$: (q) => (this.queueSize = q)
|
||||
}),
|
||||
$el('button.comfy-close-menu-btn', {
|
||||
textContent: '\u00d7',
|
||||
onclick: () => {
|
||||
useWorkspaceStore().focusMode = true
|
||||
$el('div.comfy-menu-actions', [
|
||||
$el('button.comfy-settings-btn', {
|
||||
textContent: '⚙️',
|
||||
onclick: () => {
|
||||
useDialogService().showSettingsDialog()
|
||||
}
|
||||
}),
|
||||
$el('button.comfy-close-menu-btn', {
|
||||
textContent: '\u00d7',
|
||||
onclick: () => {
|
||||
useWorkspaceStore().focusMode = true
|
||||
}
|
||||
})
|
||||
])
|
||||
]
|
||||
),
|
||||
$el('button.comfy-queue-btn', {
|
||||
id: 'queue-button',
|
||||
textContent: 'Queue Prompt',
|
||||
onclick: () => app.queuePrompt(0, this.batchCount)
|
||||
}),
|
||||
$el('div', {}, [
|
||||
$el('label', { innerHTML: 'Extra options' }, [
|
||||
$el('input', {
|
||||
type: 'checkbox',
|
||||
onchange: (i) => {
|
||||
document.getElementById('extraOptions').style.display = i
|
||||
.srcElement.checked
|
||||
? 'block'
|
||||
: 'none'
|
||||
this.batchCount = i.srcElement.checked
|
||||
? Number.parseInt(
|
||||
(
|
||||
document.getElementById(
|
||||
'batchCountInputRange'
|
||||
) as HTMLInputElement
|
||||
).value
|
||||
)
|
||||
: 1
|
||||
;(
|
||||
document.getElementById(
|
||||
'autoQueueCheckbox'
|
||||
) as HTMLInputElement
|
||||
).checked = false
|
||||
this.autoQueueEnabled = false
|
||||
}
|
||||
})
|
||||
])
|
||||
]
|
||||
),
|
||||
$el('button.comfy-queue-btn', {
|
||||
id: 'queue-button',
|
||||
textContent: 'Queue Prompt',
|
||||
onclick: () => app.queuePrompt(0, this.batchCount)
|
||||
}),
|
||||
$el('div', {}, [
|
||||
$el('label', { innerHTML: 'Extra options' }, [
|
||||
$el('input', {
|
||||
type: 'checkbox',
|
||||
onchange: (i) => {
|
||||
document.getElementById('extraOptions').style.display = i
|
||||
.srcElement.checked
|
||||
? 'block'
|
||||
: 'none'
|
||||
this.batchCount = i.srcElement.checked
|
||||
? Number.parseInt(
|
||||
(
|
||||
document.getElementById(
|
||||
'batchCountInputRange'
|
||||
) as HTMLInputElement
|
||||
).value
|
||||
)
|
||||
: 1
|
||||
;(
|
||||
document.getElementById('autoQueueCheckbox') as HTMLInputElement
|
||||
).checked = false
|
||||
this.autoQueueEnabled = false
|
||||
}
|
||||
})
|
||||
])
|
||||
]),
|
||||
$el(
|
||||
'div',
|
||||
{ id: 'extraOptions', style: { width: '100%', display: 'none' } },
|
||||
[
|
||||
$el('div', [
|
||||
$el('label', { innerHTML: 'Batch count' }),
|
||||
$el('input', {
|
||||
id: 'batchCountInputNumber',
|
||||
type: 'number',
|
||||
value: this.batchCount,
|
||||
min: '1',
|
||||
style: { width: '35%', marginLeft: '0.4em' },
|
||||
oninput: (i) => {
|
||||
this.batchCount = i.target.value
|
||||
/* Even though an <input> element with a type of range logically represents a number (since
|
||||
]),
|
||||
$el(
|
||||
'div',
|
||||
{ id: 'extraOptions', style: { width: '100%', display: 'none' } },
|
||||
[
|
||||
$el('div', [
|
||||
$el('label', { innerHTML: 'Batch count' }),
|
||||
$el('input', {
|
||||
id: 'batchCountInputNumber',
|
||||
type: 'number',
|
||||
value: this.batchCount,
|
||||
min: '1',
|
||||
style: { width: '35%', marginLeft: '0.4em' },
|
||||
oninput: (i) => {
|
||||
this.batchCount = i.target.value
|
||||
/* Even though an <input> element with a type of range logically represents a number (since
|
||||
it's used for numeric input), the value it holds is still treated as a string in HTML and
|
||||
JavaScript. This behavior is consistent across all <input> elements regardless of their type
|
||||
(like text, number, or range), where the .value property is always a string. */
|
||||
;(
|
||||
document.getElementById(
|
||||
'batchCountInputRange'
|
||||
) as HTMLInputElement
|
||||
).value = this.batchCount.toString()
|
||||
}
|
||||
}),
|
||||
$el('input', {
|
||||
id: 'batchCountInputRange',
|
||||
type: 'range',
|
||||
min: '1',
|
||||
max: '100',
|
||||
value: this.batchCount,
|
||||
oninput: (i) => {
|
||||
this.batchCount = i.srcElement.value
|
||||
// Note
|
||||
;(
|
||||
document.getElementById(
|
||||
'batchCountInputNumber'
|
||||
) as HTMLInputElement
|
||||
).value = i.srcElement.value
|
||||
}
|
||||
})
|
||||
]),
|
||||
$el('div', [
|
||||
$el('label', {
|
||||
for: 'autoQueueCheckbox',
|
||||
innerHTML: 'Auto Queue'
|
||||
}),
|
||||
$el('input', {
|
||||
id: 'autoQueueCheckbox',
|
||||
type: 'checkbox',
|
||||
checked: false,
|
||||
title: 'Automatically queue prompt when the queue size hits 0',
|
||||
onchange: (e) => {
|
||||
this.autoQueueEnabled = e.target.checked
|
||||
autoQueueModeEl.style.display = this.autoQueueEnabled
|
||||
? ''
|
||||
: 'none'
|
||||
}
|
||||
}),
|
||||
autoQueueModeEl
|
||||
])
|
||||
]
|
||||
),
|
||||
$el('div.comfy-menu-btns', [
|
||||
;(
|
||||
document.getElementById(
|
||||
'batchCountInputRange'
|
||||
) as HTMLInputElement
|
||||
).value = this.batchCount.toString()
|
||||
}
|
||||
}),
|
||||
$el('input', {
|
||||
id: 'batchCountInputRange',
|
||||
type: 'range',
|
||||
min: '1',
|
||||
max: '100',
|
||||
value: this.batchCount,
|
||||
oninput: (i) => {
|
||||
this.batchCount = i.srcElement.value
|
||||
// Note
|
||||
;(
|
||||
document.getElementById(
|
||||
'batchCountInputNumber'
|
||||
) as HTMLInputElement
|
||||
).value = i.srcElement.value
|
||||
}
|
||||
})
|
||||
]),
|
||||
$el('div', [
|
||||
$el('label', {
|
||||
for: 'autoQueueCheckbox',
|
||||
innerHTML: 'Auto Queue'
|
||||
}),
|
||||
$el('input', {
|
||||
id: 'autoQueueCheckbox',
|
||||
type: 'checkbox',
|
||||
checked: false,
|
||||
title: 'Automatically queue prompt when the queue size hits 0',
|
||||
onchange: (e) => {
|
||||
this.autoQueueEnabled = e.target.checked
|
||||
autoQueueModeEl.style.display = this.autoQueueEnabled
|
||||
? ''
|
||||
: 'none'
|
||||
}
|
||||
}),
|
||||
autoQueueModeEl
|
||||
])
|
||||
]
|
||||
),
|
||||
$el('div.comfy-menu-btns', [
|
||||
$el('button', {
|
||||
id: 'queue-front-button',
|
||||
textContent: 'Queue Front',
|
||||
onclick: () => app.queuePrompt(-1, this.batchCount)
|
||||
}),
|
||||
$el('button', {
|
||||
$: (b) => (this.queue.button = b as HTMLButtonElement),
|
||||
id: 'comfy-view-queue-button',
|
||||
textContent: 'View Queue',
|
||||
onclick: () => {
|
||||
this.history.hide()
|
||||
this.queue.toggle()
|
||||
}
|
||||
}),
|
||||
$el('button', {
|
||||
$: (b) => (this.history.button = b as HTMLButtonElement),
|
||||
id: 'comfy-view-history-button',
|
||||
textContent: 'View History',
|
||||
onclick: () => {
|
||||
this.queue.hide()
|
||||
this.history.toggle()
|
||||
}
|
||||
})
|
||||
]),
|
||||
this.queue.element,
|
||||
this.history.element,
|
||||
$el('button', {
|
||||
id: 'queue-front-button',
|
||||
textContent: 'Queue Front',
|
||||
onclick: () => app.queuePrompt(-1, this.batchCount)
|
||||
}),
|
||||
$el('button', {
|
||||
$: (b) => (this.queue.button = b as HTMLButtonElement),
|
||||
id: 'comfy-view-queue-button',
|
||||
textContent: 'View Queue',
|
||||
id: 'comfy-save-button',
|
||||
textContent: 'Save',
|
||||
onclick: () => {
|
||||
this.history.hide()
|
||||
this.queue.toggle()
|
||||
useCommandStore().execute('Comfy.ExportWorkflow')
|
||||
}
|
||||
}),
|
||||
$el('button', {
|
||||
$: (b) => (this.history.button = b as HTMLButtonElement),
|
||||
id: 'comfy-view-history-button',
|
||||
textContent: 'View History',
|
||||
id: 'comfy-dev-save-api-button',
|
||||
textContent: 'Save (API Format)',
|
||||
style: { width: '100%', display: 'none' },
|
||||
onclick: () => {
|
||||
this.queue.hide()
|
||||
this.history.toggle()
|
||||
useCommandStore().execute('Comfy.ExportWorkflowAPI')
|
||||
}
|
||||
}),
|
||||
$el('button', {
|
||||
id: 'comfy-load-button',
|
||||
textContent: 'Load',
|
||||
onclick: () => fileInput.click()
|
||||
}),
|
||||
$el('button', {
|
||||
id: 'comfy-refresh-button',
|
||||
textContent: 'Refresh',
|
||||
onclick: () => app.refreshComboInNodes()
|
||||
}),
|
||||
$el('button', {
|
||||
id: 'comfy-clipspace-button',
|
||||
textContent: 'Clipspace',
|
||||
onclick: () => app.openClipspace()
|
||||
}),
|
||||
$el('button', {
|
||||
id: 'comfy-clear-button',
|
||||
textContent: 'Clear',
|
||||
onclick: () => {
|
||||
if (
|
||||
!useSettingStore().get('Comfy.ConfirmClear') ||
|
||||
confirm('Clear workflow?')
|
||||
) {
|
||||
app.clean()
|
||||
app.graph.clear()
|
||||
app.resetView()
|
||||
api.dispatchCustomEvent('graphCleared')
|
||||
}
|
||||
}
|
||||
}),
|
||||
$el('button', {
|
||||
id: 'comfy-load-default-button',
|
||||
textContent: 'Load Default',
|
||||
onclick: async () => {
|
||||
if (
|
||||
!useSettingStore().get('Comfy.ConfirmClear') ||
|
||||
confirm('Load default workflow?')
|
||||
) {
|
||||
app.resetView()
|
||||
await app.loadGraphData()
|
||||
}
|
||||
}
|
||||
}),
|
||||
$el('button', {
|
||||
id: 'comfy-reset-view-button',
|
||||
textContent: 'Reset View',
|
||||
onclick: async () => {
|
||||
app.resetView()
|
||||
}
|
||||
})
|
||||
]),
|
||||
this.queue.element,
|
||||
this.history.element,
|
||||
$el('button', {
|
||||
id: 'comfy-save-button',
|
||||
textContent: 'Save',
|
||||
onclick: () => {
|
||||
useCommandStore().execute('Comfy.ExportWorkflow')
|
||||
}
|
||||
}),
|
||||
$el('button', {
|
||||
id: 'comfy-dev-save-api-button',
|
||||
textContent: 'Save (API Format)',
|
||||
style: { width: '100%', display: 'none' },
|
||||
onclick: () => {
|
||||
useCommandStore().execute('Comfy.ExportWorkflowAPI')
|
||||
}
|
||||
}),
|
||||
$el('button', {
|
||||
id: 'comfy-load-button',
|
||||
textContent: 'Load',
|
||||
onclick: () => fileInput.click()
|
||||
}),
|
||||
$el('button', {
|
||||
id: 'comfy-refresh-button',
|
||||
textContent: 'Refresh',
|
||||
onclick: () => app.refreshComboInNodes()
|
||||
}),
|
||||
$el('button', {
|
||||
id: 'comfy-clipspace-button',
|
||||
textContent: 'Clipspace',
|
||||
onclick: () => app.openClipspace()
|
||||
}),
|
||||
$el('button', {
|
||||
id: 'comfy-clear-button',
|
||||
textContent: 'Clear',
|
||||
onclick: () => {
|
||||
if (
|
||||
!useSettingStore().get('Comfy.ConfirmClear') ||
|
||||
confirm('Clear workflow?')
|
||||
) {
|
||||
app.clean()
|
||||
app.graph.clear()
|
||||
app.resetView()
|
||||
api.dispatchCustomEvent('graphCleared')
|
||||
}
|
||||
}
|
||||
}),
|
||||
$el('button', {
|
||||
id: 'comfy-load-default-button',
|
||||
textContent: 'Load Default',
|
||||
onclick: async () => {
|
||||
if (
|
||||
!useSettingStore().get('Comfy.ConfirmClear') ||
|
||||
confirm('Load default workflow?')
|
||||
) {
|
||||
app.resetView()
|
||||
await app.loadGraphData()
|
||||
}
|
||||
}
|
||||
}),
|
||||
$el('button', {
|
||||
id: 'comfy-reset-view-button',
|
||||
textContent: 'Reset View',
|
||||
onclick: async () => {
|
||||
app.resetView()
|
||||
}
|
||||
})
|
||||
]) as HTMLDivElement
|
||||
]
|
||||
) as HTMLDivElement
|
||||
// Hide by default on construction so it does not interfere with other views.
|
||||
this.menuContainer.style.display = 'none'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user