Skip to content

Commit

Permalink
Add support for node/input/output tooltips (#287)
Browse files Browse the repository at this point in the history
* Add support for node/input/output tooltips

* pr feedback

* Remove
  • Loading branch information
pythongosssss committed Aug 4, 2024
1 parent c48f68e commit 7a980f4
Show file tree
Hide file tree
Showing 10 changed files with 227 additions and 10 deletions.
2 changes: 2 additions & 0 deletions src/components/graph/GraphCanvas.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
168 changes: 168 additions & 0 deletions src/components/graph/NodeTooltip.vue
Original file line number Diff line number Diff line change
@@ -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>
3 changes: 2 additions & 1 deletion src/extensions/core/widgetInputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
10 changes: 7 additions & 3 deletions src/scripts/domWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down
9 changes: 8 additions & 1 deletion src/scripts/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -437,7 +444,7 @@ export class ComfyUI {
onchange: () => {
app.handleFile(fileInput.files[0])
}
}) as HTMLInputElement
})

this.loadFile = () => fileInput.click()

Expand Down
4 changes: 4 additions & 0 deletions src/scripts/widgets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
}
Expand Down
14 changes: 10 additions & 4 deletions src/stores/nodeDefStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { TreeNode } from 'primevue/treenode'
export class BaseInputSpec<T = any> {
name: string
type: string

tooltip?: string
default?: T

@Type(() => Boolean)
Expand Down Expand Up @@ -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 {
Expand All @@ -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
) {}
}

Expand All @@ -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

Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion src/stores/settingStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)
Expand Down
1 change: 1 addition & 0 deletions src/types/apiTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
24 changes: 24 additions & 0 deletions src/types/litegraph-augmentation.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}
}

0 comments on commit 7a980f4

Please sign in to comment.