Files
2025-09-05 23:49:38 +10:00

410 lines
18 KiB
JavaScript

// ComfyUI.CRZ.StringNode
import { app } from "../../scripts/app.js";
import {
NODE_WIDTH,
NODE_HEIGHT_MULTIPLIER,
NODE_PADDING,
LABEL_LEFT_PADDING,
TRACK_RIGHT_PADDING,
VALUE_RIGHT_SHIFT,
VALUE_RIGHT_OFFSET,
VALUE_RIGHT_PADDING,
DASHBOARD_LINE_SPACING,
NODE_BACKGROUND_COLOR,
LABEL_COLOR,
VALUE_COLOR,
INACTIVE_LABEL_COLOR,
INACTIVE_VALUE_COLOR,
DROPDOWN_BG_COLOR,
DROPDOWN_BG_COLOR_INACTIVE
} from "./CRZConfig.js";
class CRZStringNode {
constructor(node) {
this.node = node;
// Initialize properties
this.node.properties = this.node.properties || {};
this.node.properties.text_value = this.node.properties.text_value ?? "";
this.node.properties.text_name = this.node.properties.text_name ?? "String";
// Define layout constants in constructor scope
this.node.size = [NODE_WIDTH, Math.floor(LiteGraph.NODE_SLOT_HEIGHT * NODE_HEIGHT_MULTIPLIER)];
const fontsize = LiteGraph.NODE_SUBTEXT_SIZE;
const shX = (this.node.slot_start_y || 0) + fontsize * 1.5;
const shiftLeft = 10;
const shiftRight = 60;
this.node.onAdded = function() {
// Hide widget and set output
if (this.widgets && this.widgets[0]) {
this.widgets[0].hidden = true;
this.widgets[0].type = "hidden";
}
if (this.outputs && this.outputs[0]) {
this.outputs[0].name = this.outputs[0].localized_name = "";
this.outputs[0].type = "STRING";
}
this.widgets_start_y = -2.4e8 * LiteGraph.NODE_SLOT_HEIGHT;
};
this.node.onConfigure = function() {
if (this.outputs && this.outputs[0]) {
this.outputs[0].type = "STRING";
}
};
this.node.onGraphConfigured = function() {
this.configured = true;
this.onPropertyChanged();
};
// Property changed handler
this.node.onPropertyChanged = function(propName) {
if (!this.configured) return;
// Update widget with current text value
if (this.widgets && this.widgets[0]) {
this.widgets[0].value = this.properties.text_value || "";
}
};
// Tooltip state
this.node.showTooltip = false;
this.node.tooltipText = "";
this.node.isTruncated = false;
// Mouse move handler for tooltip
this.node.onMouseMove = function(e) {
if (e.canvasY - this.pos[1] < 0) return false;
const textAreaWidth = 115;
const textAreaLeft = this.size[0] - TRACK_RIGHT_PADDING;
const textAreaY = NODE_PADDING + 10 - 8;
const textAreaHeight = 16;
const isInTextArea = e.canvasX >= this.pos[0] + textAreaLeft &&
e.canvasX <= this.pos[0] + textAreaLeft + textAreaWidth &&
e.canvasY >= this.pos[1] + textAreaY &&
e.canvasY <= this.pos[1] + textAreaY + textAreaHeight;
if (isInTextArea) {
// Only show tooltip if text is actually truncated
this.showTooltip = this.isTruncated;
this.tooltipText = this.properties.text_value || "";
// Store the mouse event for later use
this.lastMouseEvent = e;
} else {
this.showTooltip = false;
}
return false;
};
// Mouse leave handler
this.node.onMouseLeave = function(e) {
this.showTooltip = false;
if (this.tooltipElement) {
// Trigger fade out
this.tooltipElement.style.opacity = '0';
// Hide after transition completes
setTimeout(() => {
if (this.tooltipElement) {
this.tooltipElement.style.display = 'none';
}
}, 200);
}
return false;
};
// Cleanup tooltip when node is removed
this.node.onRemoved = function() {
if (this.tooltipElement) {
this.tooltipElement.remove();
this.tooltipElement = null;
}
};
// Store the mouse handler function to avoid recursion
this.mouseDownHandler = function(e)
{
if ( e.canvasY - this.pos[1] < 0 ) return false;
// Text area hit detection (similar to BooleanToggle's expanded toggle)
const textAreaWidth = 115;
const textAreaLeft = this.size[0] - TRACK_RIGHT_PADDING;
if ( e.canvasX < this.pos[0] + textAreaLeft - 5 || e.canvasX > this.pos[0] + textAreaLeft + textAreaWidth + 5 ) return false;
if ( e.canvasY < this.pos[1] + shiftLeft - 5 || e.canvasY > this.pos[1] + this.size[1] - shiftLeft + 5 ) return false;
// Show text input prompt using ComfyUI dialog
const canvas = app.canvas;
if (canvas && canvas.prompt) {
canvas.prompt(
"Text Value",
this.properties.text_value || "",
function(newText) {
this.properties.text_value = newText || "";
this.onPropertyChanged('text_value');
// Update widget
if (this.widgets && this.widgets[0]) {
this.widgets[0].value = this.properties.text_value;
}
// Mark the graph as changed
if (this.graph && this.graph.setDirtyCanvas) {
this.graph.setDirtyCanvas(true);
} else if (this.graph && this.graph._version !== undefined) {
this.graph._version++;
}
}.bind(this),
e
);
}
return true;
}.bind(this.node);
}
}
app.registerExtension({
name: "CRZ.StringNode",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name === "CRZStringNode") {
// Hide the title bar
nodeType.title_mode = LiteGraph.NO_TITLE;
nodeType.prototype.getTitle = function() { return ""; };
nodeType.prototype.onDrawTitle = function() { return false; };
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
onNodeCreated?.apply(this, arguments);
this.bgcolor = NODE_BACKGROUND_COLOR;
// Initialize and hide the widget
if (this.widgets && this.widgets[0]) {
this.widgets[0].value = this.properties.text_value || "";
this.widgets[0].hidden = true;
this.widgets[0].type = "hidden";
} else {
// Manually create the widget if it doesn't exist
this.addWidget("text", "text", "", function(v) {
this.properties.text_value = v;
});
if (this.widgets && this.widgets[0]) {
this.widgets[0].hidden = true;
this.widgets[0].type = "hidden";
}
}
this.crzStringNode = new CRZStringNode(this);
// Override the onMouseDown at the instance level to ensure it's called
const originalOnMouseDown = this.onMouseDown;
this.onMouseDown = function(e) {
// Try the CRZ handler first
if (this.crzStringNode && this.crzStringNode.mouseDownHandler) {
const result = this.crzStringNode.mouseDownHandler(e);
if (result) return result;
}
// Fall back to original if needed
if (originalOnMouseDown) {
return originalOnMouseDown.call(this, e);
}
return false;
};
// Mark as CRZ node for connection hiding
this.isCRZNode = true;
};
// Force the node to use explicit width
nodeType.prototype.computeSize = () => [NODE_WIDTH-100, Math.floor(LiteGraph.NODE_SLOT_HEIGHT * NODE_HEIGHT_MULTIPLIER)];
nodeType.prototype.onDrawForeground = function(ctx) {
if (this.flags.collapsed || !this.crzStringNode) return;
// Height snapping - prevent vertical resize
if (this.size[1] > LiteGraph.NODE_SLOT_HEIGHT * NODE_HEIGHT_MULTIPLIER) {
this.size[1] = LiteGraph.NODE_SLOT_HEIGHT * NODE_HEIGHT_MULTIPLIER;
}
// Mark as configured
this.configured = true;
const textValue = this.properties.text_value || "";
const name = this.properties.text_name || "String";
const stringY = NODE_PADDING;
// Check if this string node should be active (not greyed out)
const isConnected = this.outputs && this.outputs[0] &&
this.outputs[0].links && this.outputs[0].links.length > 0;
const hasCustomName = name !== "String";
const hasText = textValue !== "";
const isActive = isConnected || hasCustomName || hasText;
// Choose colors based on active status
const labelColor = isActive ? LABEL_COLOR : INACTIVE_LABEL_COLOR;
const valueColor = isActive ? VALUE_COLOR : INACTIVE_VALUE_COLOR;
// Draw name on the left
ctx.fillStyle = labelColor;
ctx.font = LiteGraph.NODE_SUBTEXT_SIZE + "px Arial";
ctx.textAlign = "left";
ctx.fillText(name, LABEL_LEFT_PADDING, stringY + 15);
// Draw text area - same positioning as dropdown/combo nodes
const textAreaWidth = 115; // Match dropdown width
const textAreaLeft = this.size[0] - TRACK_RIGHT_PADDING;
const textAreaY = stringY + 10 - 8;
const textAreaHeight = 16;
const cornerRadius = 3;
// Draw rounded text background
ctx.fillStyle = isActive ? DROPDOWN_BG_COLOR : DROPDOWN_BG_COLOR_INACTIVE;
ctx.beginPath();
ctx.roundRect(textAreaLeft, textAreaY, textAreaWidth, textAreaHeight, cornerRadius);
ctx.fill();
// Draw current text value (centered in text area)
ctx.fillStyle = valueColor;
ctx.font = LiteGraph.NODE_SUBTEXT_SIZE + "px Arial";
ctx.textAlign = "center";
const maxWidth = textAreaWidth - 20;
let truncatedValue = textValue || "\"\"";
// Check if text is truncated and store the state
this.isTruncated = ctx.measureText(truncatedValue).width > maxWidth;
if (this.isTruncated) {
truncatedValue = truncatedValue.substring(0, 12) + "..";
}
ctx.fillText(truncatedValue, textAreaLeft + textAreaWidth/2, stringY + 14);
// Draw tooltip using same method as dropdown
if (this.showTooltip && this.tooltipText) {
if (!this.tooltipElement) {
this.tooltipElement = document.createElement("div");
this.tooltipElement.style.cssText = `
position: fixed;
background: rgba(24, 24, 24, 0.95);
color: #868686;
padding: 4px 8px;
border-radius: 4px;
font-size: 16px;
pointer-events: none;
z-index: 10000;
border: 1px solid rgba(41, 41, 41, 0.9);
white-space: nowrap;
opacity: 0;
transition: opacity 0.2s ease-in-out;
`;
document.body.appendChild(this.tooltipElement);
}
// Position tooltip above text area - try multiple canvas detection methods
let canvas = null;
if (this.graph && this.graph.canvas) {
canvas = this.graph.canvas.canvas || this.graph.canvas;
}
if (!canvas && this.graph && this.graph.constructor && this.graph.constructor.canvas) {
canvas = this.graph.constructor.canvas;
}
if (!canvas && app && app.canvas) {
canvas = app.canvas.canvas || app.canvas;
}
// Only update if tooltip text changed or not visible
if (this.tooltipElement.textContent !== this.tooltipText || this.tooltipElement.style.opacity === '0') {
// Get screen coordinates from the stored mouse event
let screenX = 0;
let screenY = 0;
if (this.lastMouseEvent) {
// Try to get client coordinates from the original event
const originalEvent = this.lastMouseEvent.originalEvent || this.lastMouseEvent;
screenX = originalEvent.clientX || 0;
screenY = originalEvent.clientY || 0;
}
this.tooltipElement.textContent = this.tooltipText;
this.tooltipElement.style.display = 'block';
// Position off-screen first to calculate width
this.tooltipElement.style.left = '-9999px';
this.tooltipElement.style.top = '-9999px';
this.tooltipElement.style.opacity = '0';
// Position correctly after width is calculated
requestAnimationFrame(() => {
this.tooltipElement.style.left = (screenX - this.tooltipElement.offsetWidth/2) + 'px';
this.tooltipElement.style.top = (screenY - 60) + 'px';
// Then fade in
requestAnimationFrame(() => {
this.tooltipElement.style.opacity = '1';
});
});
}
} else if (this.tooltipElement && this.tooltipElement.style.opacity !== '0') {
// Trigger fade out
this.tooltipElement.style.opacity = '0';
// Hide after transition completes
setTimeout(() => {
if (this.tooltipElement) {
this.tooltipElement.style.display = 'none';
}
}, 200);
}
};
nodeType.prototype.onDblClick = function(e, pos, canvas) {
if (this.flags.collapsed || !this.crzStringNode) return false;
const stringY = this.pos[1] + NODE_PADDING;
// Double-click on text area to edit text (same as single-click now)
const textAreaWidth = 115;
const textAreaLeft = this.pos[0] + this.size[0] - TRACK_RIGHT_PADDING;
const textAreaY = stringY + 10 - 8;
const textAreaHeight = 16;
if (e.canvasX > textAreaLeft && e.canvasX < textAreaLeft + textAreaWidth &&
e.canvasY > textAreaY && e.canvasY < textAreaY + textAreaHeight) {
canvas.prompt(
"Text Value",
this.properties.text_value || "",
function(newText) {
this.properties.text_value = newText || "";
this.onPropertyChanged('text_value');
// Update widget
if (this.widgets && this.widgets[0]) {
this.widgets[0].value = this.properties.text_value;
}
}.bind(this),
e
);
return true;
}
// Double-click on label area to rename
if (e.canvasX > this.pos[0] + LABEL_LEFT_PADDING &&
e.canvasX < this.pos[0] + this.size[0] - TRACK_RIGHT_PADDING - 10 &&
e.canvasY > stringY && e.canvasY < stringY + DASHBOARD_LINE_SPACING - 2) {
canvas.prompt("String Name", this.properties.text_name, function(newName) {
if (newName && newName.trim()) {
this.properties.text_name = newName.trim();
}
}.bind(this), e);
return true;
}
return false;
};
}
}
});