mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-26 09:44:06 +00:00
Add support for node/input/output tooltips (#287)
* Add support for node/input/output tooltips * pr feedback * Remove
This commit is contained in:
@@ -8,12 +8,14 @@
|
||||
<canvas ref="canvasRef" id="graph-canvas" tabindex="1" />
|
||||
</teleport>
|
||||
<NodeSearchboxPopover v-if="nodeSearchEnabled" />
|
||||
<NodeTooltip />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SideToolBar from '@/components/sidebar/SideToolBar.vue'
|
||||
import LiteGraphCanvasSplitterOverlay from '@/components/LiteGraphCanvasSplitterOverlay.vue'
|
||||
import NodeSearchboxPopover from '@/components/NodeSearchBoxPopover.vue'
|
||||
import NodeTooltip from '@/components/graph/NodeTooltip.vue'
|
||||
import { ref, computed, onUnmounted, watch, onMounted } from 'vue'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
168
src/components/graph/NodeTooltip.vue
Normal file
168
src/components/graph/NodeTooltip.vue
Normal file
@@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="tooltipText"
|
||||
ref="tooltipRef"
|
||||
class="node-tooltip"
|
||||
:style="{ left, top }"
|
||||
>
|
||||
{{ tooltipText }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, ref, onBeforeUnmount, watch } from 'vue'
|
||||
import { LiteGraph } from '@comfyorg/litegraph'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
let idleTimeout: number
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const settingStore = useSettingStore()
|
||||
const tooltipRef = ref<HTMLDivElement>()
|
||||
const tooltipText = ref('')
|
||||
const left = ref<string>()
|
||||
const top = ref<string>()
|
||||
|
||||
const getHoveredWidget = () => {
|
||||
const node = comfyApp.canvas.node_over
|
||||
if (!node.widgets) return
|
||||
|
||||
const graphPos = comfyApp.canvas.graph_mouse
|
||||
const x = graphPos[0] - node.pos[0]
|
||||
const y = graphPos[1] - node.pos[1]
|
||||
|
||||
for (const w of node.widgets) {
|
||||
let widgetWidth: number, widgetHeight: number
|
||||
if (w.computeSize) {
|
||||
;[widgetWidth, widgetHeight] = w.computeSize(node.size[0])
|
||||
} else {
|
||||
widgetWidth = (w as { width?: number }).width || node.size[0]
|
||||
widgetHeight = LiteGraph.NODE_WIDGET_HEIGHT
|
||||
}
|
||||
|
||||
if (
|
||||
w.last_y !== undefined &&
|
||||
x >= 6 &&
|
||||
x <= widgetWidth - 12 &&
|
||||
y >= w.last_y &&
|
||||
y <= w.last_y + widgetHeight
|
||||
) {
|
||||
return w
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const hideTooltip = () => (tooltipText.value = null)
|
||||
|
||||
const showTooltip = async (tooltip: string | null | undefined) => {
|
||||
if (!tooltip) return
|
||||
|
||||
left.value = comfyApp.canvas.mouse[0] + 'px'
|
||||
top.value = comfyApp.canvas.mouse[1] + 'px'
|
||||
tooltipText.value = tooltip
|
||||
|
||||
await nextTick()
|
||||
|
||||
const rect = tooltipRef.value.getBoundingClientRect()
|
||||
if (rect.right > window.innerWidth) {
|
||||
left.value = comfyApp.canvas.mouse[0] - rect.width + 'px'
|
||||
}
|
||||
|
||||
if (rect.top < 0) {
|
||||
top.value = comfyApp.canvas.mouse[1] + rect.height + 'px'
|
||||
}
|
||||
}
|
||||
|
||||
const onIdle = () => {
|
||||
const { canvas } = comfyApp
|
||||
const node = canvas.node_over
|
||||
if (!node) return
|
||||
|
||||
const ctor = node.constructor as { title_mode?: 0 | 1 | 2 | 3 }
|
||||
const nodeDef = nodeDefStore.nodeDefsByName[node.type]
|
||||
|
||||
if (
|
||||
ctor.title_mode !== LiteGraph.NO_TITLE &&
|
||||
canvas.graph_mouse[1] < node.pos[1] // If we are over a node, but not within the node then we are on its title
|
||||
) {
|
||||
return showTooltip(nodeDef.description)
|
||||
}
|
||||
|
||||
if (node.flags?.collapsed) return
|
||||
|
||||
const inputSlot = canvas.isOverNodeInput(
|
||||
node,
|
||||
canvas.graph_mouse[0],
|
||||
canvas.graph_mouse[1],
|
||||
[0, 0]
|
||||
)
|
||||
if (inputSlot !== -1) {
|
||||
const inputName = node.inputs[inputSlot].name
|
||||
return showTooltip(nodeDef.input.getInput(inputName)?.tooltip)
|
||||
}
|
||||
|
||||
const outputSlot = canvas.isOverNodeOutput(
|
||||
node,
|
||||
canvas.graph_mouse[0],
|
||||
canvas.graph_mouse[1],
|
||||
[0, 0]
|
||||
)
|
||||
if (outputSlot !== -1) {
|
||||
return showTooltip(nodeDef.output.all?.[outputSlot].tooltip)
|
||||
}
|
||||
|
||||
const widget = getHoveredWidget()
|
||||
// Dont show for DOM widgets, these use native browser tooltips as we dont get proper mouse events on these
|
||||
if (widget && !widget.element) {
|
||||
return showTooltip(
|
||||
widget.tooltip ?? nodeDef.input.getInput(widget.name)?.tooltip
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
hideTooltip()
|
||||
clearTimeout(idleTimeout)
|
||||
|
||||
if ((e.target as Node).nodeName !== 'CANVAS') return
|
||||
idleTimeout = window.setTimeout(onIdle, 500)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => settingStore.get<boolean>('Comfy.EnableTooltips'),
|
||||
(enabled) => {
|
||||
if (enabled) {
|
||||
window.addEventListener('mousemove', onMouseMove)
|
||||
window.addEventListener('click', hideTooltip)
|
||||
} else {
|
||||
window.removeEventListener('mousemove', onMouseMove)
|
||||
window.removeEventListener('click', hideTooltip)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('mousemove', onMouseMove)
|
||||
window.removeEventListener('click', hideTooltip)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.node-tooltip {
|
||||
background: var(--comfy-input-bg);
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.4);
|
||||
color: var(--input-text);
|
||||
font-family: sans-serif;
|
||||
left: 0;
|
||||
max-width: 30vw;
|
||||
padding: 4px 8px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
transform: translate(5px, calc(-100% - 5px));
|
||||
white-space: pre-wrap;
|
||||
z-index: 99999;
|
||||
}
|
||||
</style>
|
||||
@@ -571,7 +571,8 @@ export function mergeIfValid(
|
||||
k !== 'forceInput' &&
|
||||
k !== 'defaultInput' &&
|
||||
k !== 'control_after_generate' &&
|
||||
k !== 'multiline'
|
||||
k !== 'multiline' &&
|
||||
k !== 'tooltip'
|
||||
) {
|
||||
let v1 = config1[1][k]
|
||||
let v2 = config2[1]?.[k]
|
||||
|
||||
@@ -226,11 +226,8 @@ LGraphCanvas.prototype.computeVisibleNodes = function (): LGraphNode[] {
|
||||
if (elementWidgets.has(node)) {
|
||||
const hidden = visibleNodes.indexOf(node) === -1
|
||||
for (const w of node.widgets) {
|
||||
// @ts-expect-error
|
||||
if (w.element) {
|
||||
// @ts-expect-error
|
||||
w.element.hidden = hidden
|
||||
// @ts-expect-error
|
||||
w.element.style.display = hidden ? 'none' : undefined
|
||||
if (hidden) {
|
||||
w.options.onHide?.(w)
|
||||
@@ -282,6 +279,13 @@ LGraphNode.prototype.addDOMWidget = function (
|
||||
document.addEventListener('mousedown', mouseDownHandler)
|
||||
}
|
||||
|
||||
const { nodeData } = this.constructor
|
||||
const tooltip = (nodeData?.input.required?.[name] ??
|
||||
nodeData?.input.optional?.[name])?.[1]?.tooltip
|
||||
if (tooltip && !element.title) {
|
||||
element.title = tooltip
|
||||
}
|
||||
|
||||
const widget: DOMWidget = {
|
||||
type,
|
||||
name,
|
||||
|
||||
@@ -428,6 +428,13 @@ export class ComfyUI {
|
||||
defaultValue: 'default'
|
||||
})
|
||||
|
||||
this.settings.addSetting({
|
||||
id: 'Comfy.EnableTooltips',
|
||||
name: 'Enable Tooltips',
|
||||
type: 'boolean',
|
||||
defaultValue: true
|
||||
})
|
||||
|
||||
const fileInput = $el('input', {
|
||||
id: 'comfy-file-input',
|
||||
type: 'file',
|
||||
@@ -437,7 +444,7 @@ export class ComfyUI {
|
||||
onchange: () => {
|
||||
app.handleFile(fileInput.files[0])
|
||||
}
|
||||
}) as HTMLInputElement
|
||||
})
|
||||
|
||||
this.loadFile = () => fileInput.click()
|
||||
|
||||
|
||||
@@ -113,6 +113,8 @@ export function addValueControlWidgets(
|
||||
serialize: false // Don't include this in prompt.
|
||||
}
|
||||
)
|
||||
valueControl.tooltip =
|
||||
'Allows the linked widget to be changed automatically, for example randomizing the noise seed.'
|
||||
valueControl[IS_CONTROL_WIDGET] = true
|
||||
updateControlWidgetLabel(valueControl)
|
||||
widgets.push(valueControl)
|
||||
@@ -133,6 +135,8 @@ export function addValueControlWidgets(
|
||||
}
|
||||
)
|
||||
updateControlWidgetLabel(comboFilter)
|
||||
comboFilter.tooltip =
|
||||
"Allows for filtering the list of values when changing the value via the control generate mode. Allows for RegEx matches in the format /abc/ to only filter to values containing 'abc'."
|
||||
|
||||
widgets.push(comboFilter)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { TreeNode } from 'primevue/treenode'
|
||||
export class BaseInputSpec<T = any> {
|
||||
name: string
|
||||
type: string
|
||||
|
||||
tooltip?: string
|
||||
default?: T
|
||||
|
||||
@Type(() => Boolean)
|
||||
@@ -131,6 +131,10 @@ export class ComfyInputsSpec {
|
||||
get all() {
|
||||
return [...Object.values(this.required), ...Object.values(this.optional)]
|
||||
}
|
||||
|
||||
getInput(name: string): BaseInputSpec | undefined {
|
||||
return this.required[name] ?? this.optional[name]
|
||||
}
|
||||
}
|
||||
|
||||
export class ComfyOutputSpec {
|
||||
@@ -140,7 +144,8 @@ export class ComfyOutputSpec {
|
||||
public name: string,
|
||||
public type: string,
|
||||
public is_list: boolean,
|
||||
public comboOptions?: any[]
|
||||
public comboOptions?: any[],
|
||||
public tooltip?: string
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -166,7 +171,7 @@ export class ComfyNodeDefImpl {
|
||||
output: ComfyOutputsSpec
|
||||
|
||||
private static transformOutputSpec(obj: any): ComfyOutputsSpec {
|
||||
const { output, output_is_list, output_name } = obj
|
||||
const { output, output_is_list, output_name, output_tooltips } = obj
|
||||
const result = output.map((type: string | any[], index: number) => {
|
||||
const typeString = Array.isArray(type) ? 'COMBO' : type
|
||||
|
||||
@@ -175,7 +180,8 @@ export class ComfyNodeDefImpl {
|
||||
output_name[index],
|
||||
typeString,
|
||||
output_is_list[index],
|
||||
Array.isArray(type) ? type : undefined
|
||||
Array.isArray(type) ? type : undefined,
|
||||
output_tooltips?.[index]
|
||||
)
|
||||
})
|
||||
return new ComfyOutputsSpec(result)
|
||||
|
||||
@@ -32,7 +32,7 @@ export const useSettingStore = defineStore('setting', {
|
||||
app.ui.settings.setSettingValue(key, value)
|
||||
},
|
||||
|
||||
get(key: string) {
|
||||
get<T = any>(key: string): T {
|
||||
return (
|
||||
this.settingValues[key] ?? app.ui.settings.getSettingDefaultValue(key)
|
||||
)
|
||||
|
||||
@@ -272,6 +272,7 @@ const zComfyNodeDef = z.object({
|
||||
output: zComfyOutputTypesSpec,
|
||||
output_is_list: z.array(z.boolean()),
|
||||
output_name: z.array(z.string()),
|
||||
output_tooltips: z.array(z.string()).optional(),
|
||||
name: z.string(),
|
||||
display_name: z.string(),
|
||||
description: z.string(),
|
||||
|
||||
24
src/types/litegraph-augmentation.d.ts
vendored
24
src/types/litegraph-augmentation.d.ts
vendored
@@ -21,6 +21,13 @@ declare module '@comfyorg/litegraph' {
|
||||
* Allows for additional cleanup when removing a widget when converting to input.
|
||||
*/
|
||||
onRemove?(): void
|
||||
|
||||
/**
|
||||
* DOM element used for the widget
|
||||
*/
|
||||
element?: HTMLElement
|
||||
|
||||
tooltip?: string
|
||||
}
|
||||
|
||||
interface INodeOutputSlot {
|
||||
@@ -43,4 +50,21 @@ declare module '@comfyorg/litegraph' {
|
||||
interface LGraphNode {
|
||||
widgets_values?: unknown[]
|
||||
}
|
||||
|
||||
interface LGraphCanvas {
|
||||
/** This is in the litegraph types but has incorrect return type */
|
||||
isOverNodeInput(
|
||||
node: LGraphNode,
|
||||
canvasX: number,
|
||||
canvasY: number,
|
||||
slotPos: Vector2
|
||||
): number
|
||||
|
||||
isOverNodeOutput(
|
||||
node: LGraphNode,
|
||||
canvasX: number,
|
||||
canvasY: number,
|
||||
slotPos: Vector2
|
||||
): number
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user