Compare commits

...

23 Commits

Author SHA1 Message Date
whatdreamscost
5b3b6aefc1 Merge branch 'whatdreamscost/fe-1096-advanced-menu' of https://github.com/Comfy-Org/ComfyUI_frontend into whatdreamscost/fe-1096-advanced-menu 2026-06-27 17:51:15 -05:00
whatdreamscost
7fb280885e fix: node footer layout styling and address review feedback 2026-06-27 17:49:45 -05:00
whatdreamscost
99835e765b Merge branch 'main' of https://github.com/Comfy-Org/ComfyUI_frontend into whatdreamscost/fe-1096-advanced-menu 2026-06-26 23:23:54 -05:00
Jonathan Watkins
d084a9c556 Merge branch 'main' into whatdreamscost/fe-1096-advanced-menu 2026-06-26 22:49:19 -05:00
Jonathan Watkins
409ddc88fa Merge branch 'main' into whatdreamscost/fe-1096-advanced-menu 2026-06-26 21:42:12 -05:00
whatdreamscost
dbbadbdc09 refactor: address review feedback 2026-06-26 20:53:23 -05:00
whatdreamscost
f1b24f308e Merge branch 'whatdreamscost/fe-1096-advanced-menu' of https://github.com/Comfy-Org/ComfyUI_frontend into whatdreamscost/fe-1096-advanced-menu 2026-06-26 20:02:56 -05:00
whatdreamscost
25fb1725e6 Merge remote-tracking branch 'origin/main' into whatdreamscost/fe-1096-advanced-menu 2026-06-26 19:59:06 -05:00
whatdreamscost
9d72802750 refactor: address review feedback 2026-06-26 19:56:06 -05:00
Jonathan Watkins
a4a4d7e527 Merge branch 'main' into whatdreamscost/fe-1096-advanced-menu 2026-06-26 14:41:26 -05:00
whatdreamscost
f9e0809ce7 refactor: address review feedback 2026-06-26 14:35:52 -05:00
whatdreamscost
7561d3f4bd Merge remote-tracking branch 'origin/main' into whatdreamscost/fe-1096-advanced-menu 2026-06-26 14:13:57 -05:00
whatdreamscost
b40b03c8ae refactor: address review feedback 2026-06-26 13:42:21 -05:00
whatdreamscost
b8fd3ec1dc refactor: address review feedback 2026-06-26 13:16:48 -05:00
whatdreamscost
93ec57e8d2 Merge remote-tracking branch 'origin/main' into whatdreamscost/fe-1096-advanced-menu 2026-06-25 23:04:35 -05:00
Alexander Brown
73e1fed080 Merge branch 'main' into whatdreamscost/fe-1096-advanced-menu 2026-06-25 17:51:33 -07:00
Jonathan Watkins
711aad704a Merge branch 'main' into whatdreamscost/fe-1096-advanced-menu 2026-06-25 04:34:47 -05:00
whatdreamscost
d10decbd96 refactor: address review feedback 2026-06-25 03:44:25 -05:00
Jonathan Watkins
cb8d3186f9 Merge branch 'main' into whatdreamscost/fe-1096-advanced-menu 2026-06-24 21:50:17 -05:00
whatdreamscost
83a412112e refactor: address CodeRabbit feedback 2026-06-24 21:46:59 -05:00
Jonathan Watkins
ecb2a745a5 Merge branch 'main' into whatdreamscost/fe-1096-advanced-menu 2026-06-24 20:57:39 -05:00
whatdreamscost
6262358361 Merge remote-tracking branch 'origin/main' into whatdreamscost/fe-1096-advanced-menu 2026-06-24 19:28:34 -05:00
whatdreamscost
0f04cec48c feat: add button to open advanced parameters global toggle (FE-1096) 2026-06-24 18:45:14 -05:00
11 changed files with 529 additions and 82 deletions

View File

@@ -29,4 +29,27 @@
position: absolute;
width: 100%;
}
@keyframes highlight-animation {
0% {
background-color: transparent;
box-shadow: 0 0 0 6px transparent;
border-radius: 4px;
}
6%,
63% {
background-color: color-mix(in srgb, var(--fg-color) 25%, transparent);
box-shadow: 0 0 0 6px color-mix(in srgb, var(--fg-color) 25%, transparent);
border-radius: 4px;
}
100% {
background-color: transparent;
box-shadow: 0 0 0 6px transparent;
border-radius: 4px;
}
}
.animate-highlight {
animation: highlight-animation 0.9s ease-out;
}
}

View File

@@ -6,13 +6,24 @@ import LayoutField from './LayoutField.vue'
defineProps<{
label: string
tooltip?: string
highlighted?: boolean
}>()
const emit = defineEmits<{
(e: 'animationend'): void
}>()
const modelValue = defineModel<boolean>({ default: false })
</script>
<template>
<LayoutField singleline :label :tooltip>
<LayoutField
singleline
:label
:tooltip
:class="{ 'animate-highlight': highlighted }"
@animationend="emit('animationend')"
>
<ToggleSwitch
v-model="modelValue"
class="transition-transform active:scale-90"

View File

@@ -0,0 +1,80 @@
import { cleanup, render, fireEvent, screen } from '@testing-library/vue'
import { describe, expect, it, vi, afterEach } from 'vitest'
import { createI18n } from 'vue-i18n'
import PrimeVue from 'primevue/config'
import { createTestingPinia } from '@pinia/testing'
import TabGlobalSettings from '@/components/rightSidePanel/settings/TabGlobalSettings.vue'
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
const i18n = createI18n({
legacy: false,
locale: 'en',
messages: {
en: {
rightSidePanel: {
globalSettings: {
nodes: 'Nodes',
showAdvanced: 'Show Advanced Parameters'
}
}
}
}
})
const { mockSettings } = vi.hoisted(() => {
const mockSettings: Record<string, unknown> = {
'Comfy.Node.AlwaysShowAdvancedWidgets': false,
'Comfy.Canvas.SelectionToolbox': false,
'Comfy.VueNodes.Enabled': false,
'Comfy.SnapToGrid.GridSize': 10,
'pysssss.SnapToGrid': false,
'Comfy.Graph.LinkMarkers': 'None',
'Comfy.LinkRenderMode': 'Spline'
}
return { mockSettings }
})
vi.mock('@/platform/settings/settingStore', () => ({
useSettingStore: () => ({
get: vi.fn((key: string) => mockSettings[key]),
set: vi.fn((key: string, val: unknown) => {
mockSettings[key] = val
})
})
}))
vi.mock('@/platform/settings/composables/useSettingsDialog', () => ({
useSettingsDialog: () => ({
show: vi.fn()
})
}))
describe('TabGlobalSettings', () => {
afterEach(() => {
cleanup()
})
it('adds and removes highlight class when highlight gets triggered and animation ends', async () => {
const pinia = createTestingPinia({ stubActions: false })
render(TabGlobalSettings, {
global: {
plugins: [i18n, pinia, PrimeVue]
}
})
const rightSidePanelStore = useRightSidePanelStore()
const switchEl = screen.getByTestId('advanced-widgets-switch')
expect(switchEl).not.toHaveClass('animate-highlight')
rightSidePanelStore.triggerHighlight('Comfy.Node.AlwaysShowAdvancedWidgets')
await new Promise((resolve) => setTimeout(resolve, 0))
expect(switchEl).toHaveClass('animate-highlight')
await fireEvent.animationEnd(switchEl)
expect(switchEl).not.toHaveClass('animate-highlight')
})
})

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import InputNumber from 'primevue/inputnumber'
import Select from 'primevue/select'
import { computed } from 'vue'
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import Button from '@/components/ui/button/Button.vue'
@@ -18,9 +18,29 @@ import PropertiesAccordionItem from '../layout/PropertiesAccordionItem.vue'
import FieldSwitch from './FieldSwitch.vue'
import LayoutField from './LayoutField.vue'
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
const { t } = useI18n()
const settingStore = useSettingStore()
const settingsDialog = useSettingsDialog()
const rightSidePanelStore = useRightSidePanelStore()
const isHighlightingAdvanced = ref(false)
watch(
() => rightSidePanelStore.highlightGlobalSetting,
(newVal) => {
if (
newVal &&
rightSidePanelStore.consumeHighlight(
'Comfy.Node.AlwaysShowAdvancedWidgets'
)
) {
isHighlightingAdvanced.value = true
}
},
{ immediate: true }
)
// NODES settings
const showAdvancedParameters = computed({
@@ -106,8 +126,11 @@ function openFullSettings() {
<div class="space-y-4 px-4 py-3">
<FieldSwitch
v-model="showAdvancedParameters"
data-testid="advanced-widgets-switch"
:label="t('rightSidePanel.globalSettings.showAdvanced')"
:tooltip="t('settings.Comfy_Node_AlwaysShowAdvancedWidgets.tooltip')"
:highlighted="isHighlightingAdvanced"
@animationend="isHighlightingAdvanced = false"
/>
<FieldSwitch
v-model="showToolbox"

View File

@@ -3816,6 +3816,7 @@
"parameters": "Parameters",
"nodes": "Nodes",
"info": "Info",
"advancedWidgetSettings": "Advanced widget settings",
"infoFor": "Info for {item}",
"color": "Node color",
"pinned": "Pinned",

View File

@@ -1,12 +1,50 @@
import { render, screen } from '@testing-library/vue'
import { cleanup, render, screen, within } from '@testing-library/vue'
import userEvent from '@testing-library/user-event'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import {
beforeEach,
describe,
expect,
it,
vi,
afterEach,
afterAll
} from 'vitest'
import { ref } from 'vue'
import { createI18n } from 'vue-i18n'
import PrimeVue from 'primevue/config'
import { createTestingPinia } from '@pinia/testing'
import { fromAny } from '@total-typescript/shoehorn'
import { RenderShape } from '@/lib/litegraph/src/litegraph'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import NodeFooter from '@/renderer/extensions/vueNodes/components/NodeFooter.vue'
vi.hoisted(() => {
const localStorageMock = (() => {
let store: Record<string, string> = {}
return {
getItem: (key: string) => store[key] || null,
setItem: (key: string, value: string) => {
store[key] = value.toString()
},
removeItem: (key: string) => {
delete store[key]
},
clear: () => {
store = {}
}
}
})()
vi.stubGlobal('localStorage', localStorageMock)
})
const mockSettingsDialogShow = vi.fn()
vi.mock('@/platform/settings/composables/useSettingsDialog', () => ({
useSettingsDialog: () => ({
show: mockSettingsDialogShow
})
}))
vi.mock('@/renderer/core/layout/store/layoutStore', () => {
const isDraggingVueNodes = ref(false)
return { layoutStore: { isDraggingVueNodes } }
@@ -51,24 +89,35 @@ const baseProps: Props = {
}
function renderFooter(overrides: Partial<Props> = {}) {
const pinia = createTestingPinia({ stubActions: false })
return render(NodeFooter, {
global: { plugins: [i18n] },
global: { plugins: [i18n, pinia, PrimeVue] },
props: { ...baseProps, ...overrides }
})
}
function allButtonClasses(): string {
return screen
.getAllByRole('button')
function allButtonClasses(container: Element): string {
return within(container as HTMLElement)
.queryAllByRole('button')
.map((b) => b.className)
.join(' ')
}
describe('NodeFooter', () => {
afterEach(() => {
cleanup()
})
afterAll(() => {
vi.unstubAllGlobals()
})
describe('rendering branches', () => {
it('renders nothing when no relevant flags are set', () => {
renderFooter()
expect(screen.queryAllByRole('button')).toHaveLength(0)
const { container } = renderFooter()
expect(
within(container as HTMLElement).queryAllByRole('button')
).toHaveLength(0)
})
it('renders error + enter tabs for subgraph with error (Case 1)', () => {
@@ -182,25 +231,34 @@ describe('NodeFooter', () => {
describe('shape-based radius classes (getBottomRadius)', () => {
it('BOX shape renders no rounded-b* class on the single-tab footer', () => {
renderFooter({ isSubgraph: true, shape: RenderShape.BOX })
expect(allButtonClasses()).not.toMatch(/rounded-b/)
const { container } = renderFooter({
isSubgraph: true,
shape: RenderShape.BOX
})
expect(allButtonClasses(container)).not.toMatch(/rounded-b/)
})
it('CARD shape emits rounded-br variant on the single-tab footer', () => {
renderFooter({ isSubgraph: true, shape: RenderShape.CARD })
const classes = allButtonClasses()
const { container } = renderFooter({
isSubgraph: true,
shape: RenderShape.CARD
})
const classes = allButtonClasses(container)
expect(classes).toMatch(/rounded-br-\[17px\]/)
expect(classes).not.toMatch(/rounded-b-\[/)
})
it('default shape emits rounded-b variant on the single-tab footer', () => {
renderFooter({ isSubgraph: true })
expect(allButtonClasses()).toMatch(/rounded-b-\[17px\]/)
const { container } = renderFooter({ isSubgraph: true })
expect(allButtonClasses(container)).toMatch(/rounded-b-\[17px\]/)
})
it('upgrades to 20px radius when the error tab is present', () => {
renderFooter({ hasAnyError: true, showErrorsTabEnabled: true })
expect(allButtonClasses()).toMatch(/rounded-b-\[20px\]/)
const { container } = renderFooter({
hasAnyError: true,
showErrorsTabEnabled: true
})
expect(allButtonClasses(container)).toMatch(/rounded-b-\[20px\]/)
})
it('enter tab uses right-only rounding in dual-tab mode (Case 1)', () => {
@@ -212,6 +270,53 @@ describe('NodeFooter', () => {
const enterBtn = screen.getByTestId('subgraph-enter-button')
expect(enterBtn.className).toMatch(/rounded-br-\[20px\]/)
})
it('error tab uses both-corner rounding in dual-tab mode (Case 1)', () => {
renderFooter({
isSubgraph: true,
hasAnyError: true,
showErrorsTabEnabled: true
})
const errorBtn = screen.getByRole('button', { name: /error/i })
expect(errorBtn.className).toMatch(/rounded-b-\[20px\]/)
})
it('error tab uses rounded-br rounding for CARD shape in dual-tab mode', () => {
renderFooter({
isSubgraph: true,
hasAnyError: true,
showErrorsTabEnabled: true,
shape: RenderShape.CARD
})
const errorBtn = screen.getByRole('button', { name: /error/i })
expect(errorBtn.className).toMatch(/rounded-br-\[20px\]/)
expect(errorBtn.className).not.toMatch(/rounded-bl-\[/)
expect(errorBtn.className).not.toMatch(/rounded-b-\[/)
})
it('enter tab uses rounded-br rounding for CARD shape in dual-tab mode', () => {
renderFooter({
isSubgraph: true,
hasAnyError: true,
showErrorsTabEnabled: true,
shape: RenderShape.CARD
})
const enterBtn = screen.getByTestId('subgraph-enter-button')
expect(enterBtn.className).toMatch(/rounded-br-\[20px\]/)
})
it('BOX shape renders no rounded corners in dual-tab mode', () => {
renderFooter({
isSubgraph: true,
hasAnyError: true,
showErrorsTabEnabled: true,
shape: RenderShape.BOX
})
const errorBtn = screen.getByRole('button', { name: /error/i })
const enterBtn = screen.getByTestId('subgraph-enter-button')
expect(errorBtn.className).not.toMatch(/rounded-b/)
expect(enterBtn.className).not.toMatch(/rounded-b/)
})
})
describe('headerColor style', () => {
@@ -231,4 +336,75 @@ describe('NodeFooter', () => {
)
})
})
describe('advanced settings button', () => {
let user: ReturnType<typeof userEvent.setup>
beforeEach(() => {
user = userEvent.setup()
vi.resetAllMocks()
})
it('renders the gear settings button next to advanced inputs button when expanded', () => {
renderFooter({ showAdvancedInputsButton: true, showAdvancedState: true })
expect(screen.getByTestId('advanced-settings-button')).toBeTruthy()
})
it('opens settings dialog when clicked in legacy menu mode', async () => {
renderFooter({ showAdvancedInputsButton: true, showAdvancedState: true })
const settingStore = (
await import('@/platform/settings/settingStore')
).useSettingStore()
vi.mocked(settingStore.get).mockImplementation((key) => {
if (key === 'Comfy.UseNewMenu') return 'Disabled'
return false
})
const settingsBtn = screen.getByTestId('advanced-settings-button')
await user.click(settingsBtn)
expect(mockSettingsDialogShow).toHaveBeenCalledWith(
undefined,
'Comfy.Node.AlwaysShowAdvancedWidgets'
)
})
it('opens right side settings panel when clicked in new menu mode', async () => {
const deselectAllSpy = vi.fn()
renderFooter({
showAdvancedInputsButton: true,
showAdvancedState: true
})
const canvasStore = useCanvasStore()
const originalCanvas = canvasStore.canvas
canvasStore.canvas = fromAny({
deselectAll: deselectAllSpy
})
try {
const settingStore = (
await import('@/platform/settings/settingStore')
).useSettingStore()
vi.mocked(settingStore.get).mockImplementation((key) => {
if (key === 'Comfy.UseNewMenu') return 'Top'
return false
})
const rightSidePanelStore = (
await import('@/stores/workspace/rightSidePanelStore')
).useRightSidePanelStore()
const openPanelSpy = vi.spyOn(rightSidePanelStore, 'openPanel')
const settingsBtn = screen.getByTestId('advanced-settings-button')
await user.click(settingsBtn)
expect(deselectAllSpy).toHaveBeenCalledOnce()
expect(openPanelSpy).toHaveBeenCalledWith('settings')
} finally {
canvasStore.canvas = originalCanvas
}
})
})
})

View File

@@ -28,7 +28,7 @@
:class="
cn(
tabStyles,
'-ml-5 box-border w-[calc(50%+20px)] rounded-none bg-node-component-header-surface pt-9 pb-4 pl-5',
'-ml-5 box-border w-[calc(50%+20px)] rounded-none bg-node-component-header-surface pt-9 pr-0 pb-4 pl-5',
enterRadiusClass
)
"
@@ -72,12 +72,17 @@
</Button>
<Button
as="div"
role="button"
tabindex="0"
variant="textonly"
data-testid="advanced-inputs-button"
:class="
cn(
tabStyles,
'-ml-5 box-border w-[calc(50%+20px)] rounded-none bg-node-component-header-surface pt-9 pb-4 pl-5',
'relative z-0 -ml-5 box-border w-[calc(50%+20px)] rounded-none bg-node-component-header-surface pt-9 pb-4 pl-5',
showAdvancedState ? 'pr-0' : 'pr-4',
'has-[.node-footer-settings-btn:hover]:hover:bg-node-component-header-surface',
enterRadiusClass
)
"
@@ -85,20 +90,31 @@
@pointerup="snapshotDragOnPointerUp"
@click.stop="emitIfNotDragged('toggleAdvanced')"
>
<div class="flex size-full items-center justify-center gap-2">
<span class="truncate">{{
<div
:class="
cn(
'flex size-full min-w-0 items-center justify-center gap-2',
showAdvancedState ? 'pr-10' : 'pr-0'
)
"
>
<span class="min-w-0 truncate">{{
showAdvancedState
? t('rightSidePanel.hideAdvancedShort')
: t('rightSidePanel.showAdvancedShort')
}}</span>
<i
:class="
showAdvancedState
? 'icon-[lucide--chevron-up] size-4 shrink-0'
: 'icon-[lucide--settings-2] size-4 shrink-0'
"
v-if="!showAdvancedState"
class="icon-[lucide--settings-2] size-4 shrink-0"
/>
</div>
<NodeFooterAdvancedSettingsButton
v-if="showAdvancedState"
class="border-interface-stroke-muted/20 absolute inset-y-0 right-0 flex h-full w-10 shrink-0 items-center justify-center rounded-none border-l bg-node-component-header-surface pt-9 pb-4"
:class="cn(tabStyles, enterRadiusClass)"
:style="headerColorStyle"
/>
</Button>
</div>
@@ -164,7 +180,8 @@
:class="
cn(
footerWrapperBase,
hasAnyError ? '-mx-1 -mb-2 w-[calc(100%+8px)] pb-1' : 'w-full'
hasAnyError ? '-mx-1 -mb-2 w-[calc(100%+8px)] pb-1' : 'w-full',
'relative flex items-center'
)
"
>
@@ -174,7 +191,7 @@
:class="
cn(
tabStyles,
'box-border w-full rounded-none bg-node-component-header-surface',
'box-border w-full rounded-none bg-node-component-header-surface px-0',
hasAnyError ? 'pt-9 pb-4' : 'pt-8 pb-4',
footerRadiusClass
)
@@ -183,21 +200,40 @@
@pointerup="snapshotDragOnPointerUp"
@click.stop="emitIfNotDragged('toggleAdvanced')"
>
<div class="flex size-full items-center justify-center gap-2">
<div
:class="
cn(
'flex size-full min-w-0 items-center justify-center gap-2',
showAdvancedState ? 'px-10' : 'px-4'
)
"
>
<template v-if="showAdvancedState">
<span class="truncate">{{
<span class="min-w-0 truncate">{{
t('rightSidePanel.hideAdvancedInputsButton')
}}</span>
<i class="icon-[lucide--chevron-up] size-4 shrink-0" />
</template>
<template v-else>
<span class="truncate">{{
<span class="min-w-0 truncate">{{
t('rightSidePanel.showAdvancedInputsButton')
}}</span>
<i class="icon-[lucide--settings-2] size-4 shrink-0" />
</template>
</div>
</Button>
<NodeFooterAdvancedSettingsButton
v-if="showAdvancedState"
class="border-interface-stroke-muted/20 absolute inset-y-0 right-0 flex h-full w-10 shrink-0 items-center justify-center rounded-none border-l bg-node-component-header-surface"
:class="
cn(
tabStyles,
footerRadiusRightClass,
hasAnyError ? 'pt-9 pb-4' : 'pt-8 pb-4'
)
"
:style="headerColorStyle"
/>
</div>
</template>
@@ -205,6 +241,7 @@
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import Button from '@/components/ui/button/Button.vue'
import NodeFooterAdvancedSettingsButton from './NodeFooterAdvancedSettingsButton.vue'
import { RenderShape } from '@/lib/litegraph/src/litegraph'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import { cn } from '@comfyorg/tailwind-utils'
@@ -258,19 +295,30 @@ const RADIUS_CLASS = {
'rounded-b-17': 'rounded-b-[17px]',
'rounded-b-20': 'rounded-b-[20px]',
'rounded-br-17': 'rounded-br-[17px]',
'rounded-br-20': 'rounded-br-[20px]'
'rounded-br-20': 'rounded-br-[20px]',
'rounded-bl-17': 'rounded-bl-[17px]',
'rounded-bl-20': 'rounded-bl-[20px]'
} as const
const SHAPE_CORNER_PREFIX_MAP: Record<
string | number,
Record<'both' | 'right' | 'left', string>
> = {
[RenderShape.BOX]: { both: '', right: '', left: '' },
[RenderShape.CARD]: { both: 'rounded-br', right: 'rounded-br', left: '' },
default: { both: 'rounded-b', right: 'rounded-br', left: 'rounded-bl' }
}
function getBottomRadius(
nodeShape: RenderShape | undefined,
size: '17px' | '20px',
corners: 'both' | 'right' = 'both'
corners: 'both' | 'right' | 'left' = 'both'
): string {
if (nodeShape === RenderShape.BOX) return ''
const prefix =
nodeShape === RenderShape.CARD || corners === 'right'
? 'rounded-br'
: 'rounded-b'
const map =
SHAPE_CORNER_PREFIX_MAP[nodeShape ?? 'default'] ??
SHAPE_CORNER_PREFIX_MAP.default
const prefix = map[corners]
if (!prefix) return ''
const key =
`${prefix}-${size === '17px' ? '17' : '20'}` as keyof typeof RADIUS_CLASS
return RADIUS_CLASS[key]
@@ -280,6 +328,10 @@ const footerRadiusClass = computed(() =>
getBottomRadius(shape, hasAnyError ? '20px' : '17px')
)
const footerRadiusRightClass = computed(() =>
getBottomRadius(shape, hasAnyError ? '20px' : '17px', 'right')
)
const errorRadiusClass = computed(() => getBottomRadius(shape, '20px'))
const enterRadiusClass = computed(() => getBottomRadius(shape, '20px', 'right'))

View File

@@ -0,0 +1,53 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import Button from '@/components/ui/button/Button.vue'
import { useTooltipConfig } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useSettingsDialog } from '@/platform/settings/composables/useSettingsDialog'
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
const { t } = useI18n()
const settingStore = useSettingStore()
const settingsDialog = useSettingsDialog()
const rightSidePanelStore = useRightSidePanelStore()
const canvasStore = useCanvasStore()
const { createTooltipConfig } = useTooltipConfig()
const tooltipConfig = computed(() =>
createTooltipConfig(t('rightSidePanel.advancedWidgetSettings'))
)
function openSettings() {
const isLegacyMenu = settingStore.get('Comfy.UseNewMenu') === 'Disabled'
if (isLegacyMenu) {
settingsDialog.show(undefined, 'Comfy.Node.AlwaysShowAdvancedWidgets')
} else {
if (canvasStore.canvas) {
canvasStore.canvas.deselectAll()
}
rightSidePanelStore.openPanel('settings')
rightSidePanelStore.triggerHighlight('Comfy.Node.AlwaysShowAdvancedWidgets')
}
}
</script>
<template>
<Button
v-tooltip.bottom="tooltipConfig"
variant="textonly"
class="node-footer-settings-btn group"
data-testid="advanced-settings-button"
:aria-label="t('rightSidePanel.advancedWidgetSettings')"
@pointerdown.stop
@pointerup.stop
@mousedown.stop
@mouseup.stop
@click.stop="openSettings"
>
<i
class="group-hover:text-foreground icon-[lucide--settings] size-4 text-muted-foreground"
/>
</Button>
</template>

View File

@@ -11,6 +11,10 @@ import { useNodeDefStore } from '@/stores/nodeDefStore'
import { useNodeTooltips } from './useNodeTooltips'
vi.mock('@/scripts/app', () => ({
app: {}
}))
const jsonTooltip =
'Positive point prompts as JSON [{"x": int, "y": int}, ...] (pixel coords)'

View File

@@ -82,22 +82,63 @@ function setupGlobalTooltipHiding() {
globalTooltipState.listenersSetup = true
}
/**
* Composable for creating the themed tooltip configuration object.
*/
export function useTooltipConfig() {
const settingStore = useSettingStore()
const tooltipsEnabled = computed(() =>
settingStore.get('Comfy.EnableTooltips')
)
const createTooltipConfig = (text: string): TooltipOptions => {
const tooltipDelay = settingStore.get('LiteGraph.Node.TooltipDelay')
const tooltipText = text || ''
return {
value: tooltipText,
showDelay: tooltipDelay as number,
hideDelay: 0, // Immediate hiding
disabled:
!tooltipsEnabled.value ||
!tooltipText ||
tooltipsTemporarilyDisabled.value, // this reactive value works but only on next mount,
// so if the tooltip is already visible changing this will not hide it
pt: {
text: {
class:
'border-node-component-tooltip-border bg-node-component-tooltip-surface border rounded-md px-4 py-2 text-node-component-tooltip text-sm font-normal leading-tight max-w-75 shadow-none'
},
arrow: ({ context }: TooltipPassThroughMethodOptions) => ({
class: cn(
context.top && 'border-t-node-component-tooltip-border',
context.bottom && 'border-b-node-component-tooltip-border',
context.left && 'border-l-node-component-tooltip-border',
context.right && 'border-r-node-component-tooltip-border'
)
})
}
}
}
return {
tooltipsEnabled,
createTooltipConfig
}
}
/**
* Composable for managing Vue node tooltips
* Provides tooltip text for node headers, slots, and widgets
*/
export function useNodeTooltips(nodeType: MaybeRef<string>) {
const nodeDefStore = useNodeDefStore()
const settingsStore = useSettingStore()
const { tooltipsEnabled, createTooltipConfig } = useTooltipConfig()
// Setup global pointerdown listener once
setupGlobalTooltipHiding()
// Check if tooltips are globally enabled
const tooltipsEnabled = computed(() =>
settingsStore.get('Comfy.EnableTooltips')
)
// Get node definition for tooltip data
const nodeDef = computed(() => nodeDefStore.nodeDefsByName[unref(nodeType)])
@@ -149,40 +190,6 @@ export function useNodeTooltips(nodeType: MaybeRef<string>) {
return stRaw(key, inputTooltip)
}
/**
* Create tooltip configuration object for v-tooltip directive
* Components wrap this in computed() for reactivity
*/
const createTooltipConfig = (text: string): TooltipOptions => {
const tooltipDelay = settingsStore.get('LiteGraph.Node.TooltipDelay')
const tooltipText = text || ''
return {
value: tooltipText,
showDelay: tooltipDelay as number,
hideDelay: 0, // Immediate hiding
disabled:
!tooltipsEnabled.value ||
!tooltipText ||
tooltipsTemporarilyDisabled.value, // this reactive value works but only on next mount,
// so if the tooltip is already visible changing this will not hide it
pt: {
text: {
class:
'border-node-component-tooltip-border bg-node-component-tooltip-surface border rounded-md px-4 py-2 text-node-component-tooltip text-sm font-normal leading-tight max-w-75 shadow-none'
},
arrow: ({ context }: TooltipPassThroughMethodOptions) => ({
class: cn(
context.top && 'border-t-node-component-tooltip-border',
context.bottom && 'border-b-node-component-tooltip-border',
context.left && 'border-l-node-component-tooltip-border',
context.right && 'border-r-node-component-tooltip-border'
)
})
}
}
}
return {
tooltipsEnabled,
getNodeDescription,

View File

@@ -1,5 +1,5 @@
import { defineStore } from 'pinia'
import { computed, ref, watch } from 'vue'
import { computed, readonly, ref, watch } from 'vue'
import { useSettingStore } from '@/platform/settings/settingStore'
@@ -81,6 +81,20 @@ export const useRightSidePanelStore = defineStore('rightSidePanel', () => {
focusedSection.value = null
}
const highlightGlobalSetting = ref<string | null>(null)
function triggerHighlight(settingName: string) {
highlightGlobalSetting.value = settingName
}
function consumeHighlight(settingName: string): boolean {
if (highlightGlobalSetting.value === settingName) {
highlightGlobalSetting.value = null
return true
}
return false
}
return {
isOpen,
activeTab,
@@ -88,10 +102,13 @@ export const useRightSidePanelStore = defineStore('rightSidePanel', () => {
focusedSection,
focusedErrorNodeId,
searchQuery,
highlightGlobalSetting: readonly(highlightGlobalSetting),
openPanel,
closePanel,
togglePanel,
focusSection,
clearFocusedSection
clearFocusedSection,
triggerHighlight,
consumeHighlight
}
})