410 lines
18 KiB
JavaScript
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;
|
|
};
|
|
}
|
|
}
|
|
}); |