Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for node/input/output tooltips #287

Merged
merged 4 commits into from
Aug 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>
pythongosssss marked this conversation as resolved.
Show resolved Hide resolved

<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
}
}
Loading