Files
CoreyCorza-ComfyUI-CRZnodes/js/Dropdown.js
T
2025-09-05 23:49:38 +10:00

406 lines
19 KiB
JavaScript

// ComfyUI.CRZ.Dropdown
import { app } from "../../scripts/app.js";
import {
NODE_WIDTH,
NODE_HEIGHT_MULTIPLIER,
LABEL_LEFT_PADDING,
DROPDOWN_RIGHT_PADDING,
DROPDOWN_WIDTH,
COMBO_TEXT_OFFSET,
NODE_BACKGROUND_COLOR,
LABEL_COLOR,
INACTIVE_LABEL_COLOR,
VALUE_COLOR,
INACTIVE_VALUE_COLOR,
DROPDOWN_BG_COLOR,
DROPDOWN_BG_COLOR_INACTIVE,
DROPDOWN_BORDER_COLOR,
VALUE_TEXT_COLOR
} from "./CRZConfig.js";
app.registerExtension({
name: "CRZDropdown",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "CRZDropdown") {
// Hide the title bar
nodeType.title_mode = LiteGraph.NO_TITLE;
nodeType.prototype.getTitle = function() { return ""; };
nodeType.prototype.onDrawTitle = function() { return false; };
// Set minimum size
nodeType.min_size = [NODE_WIDTH, Math.floor(LiteGraph.NODE_SLOT_HEIGHT * NODE_HEIGHT_MULTIPLIER)];
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
if (onNodeCreated) {
onNodeCreated.apply(this, arguments);
}
// Set background color
this.bgcolor = NODE_BACKGROUND_COLOR;
// Mark this as a CRZ node for connection hiding
this.isCRZNode = true;
// Initialize properties
this.properties = this.properties || {};
this.properties.label = this.properties.label || "Dropdown";
this.properties.value = this.properties.value || "";
this.properties.options = this.properties.options || [""];
// Set consistent height
this.size = [NODE_WIDTH, Math.floor(LiteGraph.NODE_SLOT_HEIGHT * NODE_HEIGHT_MULTIPLIER)];
// Move widgets off-screen like FloatSlider
this.widgets_start_y = -2.4e8*LiteGraph.NODE_SLOT_HEIGHT;
// Hide the default widget
if (this.widgets && this.widgets[0]) {
this.widgets[0].hidden = true;
this.widgets[0].type = "hidden";
}
// Store reference to the value widget for syncing
this.valueWidget = this.widgets?.find(w => w.name === "value");
// Clear output socket label
this.onAdded = function() {
if (this.outputs && this.outputs[0]) {
this.outputs[0].name = this.outputs[0].localized_name = "";
}
};
// Tooltip state
this.showTooltip = false;
this.tooltipText = "";
this.isTruncated = false;
// Mouse move handler for tooltip
this.onMouseMove = function(e) {
if (e.canvasY - this.pos[1] < 0) return false;
const dropdownLeft = this.size[0] - DROPDOWN_RIGHT_PADDING - DROPDOWN_WIDTH;
const dropdownY = (this.slot_start_y || 0) + LiteGraph.NODE_SUBTEXT_SIZE * 1.5 - LiteGraph.NODE_SUBTEXT_SIZE + 3;
const dropdownHeight = LiteGraph.NODE_SUBTEXT_SIZE + 4;
const isInDropdown = e.canvasX >= this.pos[0] + dropdownLeft &&
e.canvasX <= this.pos[0] + dropdownLeft + DROPDOWN_WIDTH &&
e.canvasY >= this.pos[1] + dropdownY - 2 &&
e.canvasY <= this.pos[1] + dropdownY + dropdownHeight - 2;
if (isInDropdown) {
// Only show tooltip if text is actually truncated
this.showTooltip = this.isTruncated;
this.tooltipText = this.properties.value || "";
// Store the mouse event for later use
this.lastMouseEvent = e;
} else {
this.showTooltip = false;
}
return false;
};
// Mouse leave handler
this.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.onRemoved = function() {
if (this.tooltipElement) {
this.tooltipElement.remove();
this.tooltipElement = null;
}
};
// Simple function to set specific combo options
this.setComboOptions = function(options) {
this.properties.options = [...options];
if (!options.includes(this.properties.value)) {
this.properties.value = options[0] || "";
}
// Sync with hidden widget
if (this.valueWidget) {
this.valueWidget.value = this.properties.value;
this.valueWidget.options = this.valueWidget.options || {};
this.valueWidget.options.values = [...options];
}
this.setDirtyCanvas(true, true);
};
// Function to update combo options based on connection
this.updateComboOptions = function() {
// Check if our output is connected
const output = this.outputs?.[0];
if (!output || !output.links || output.links.length === 0) {
this.setComboOptions([""]);
return;
}
// Get connected node info
const linkId = output.links[0];
const link = this.graph.links[linkId];
if (!link) {
return;
}
let targetNode = this.graph.getNodeById(link.target_id);
let targetInput = targetNode?.inputs?.[link.target_slot];
// PASSTHROUGH FIX: If target is a passthrough, find the final target
if (targetNode && targetNode.isPassthrough && targetNode.getPassthroughTarget) {
const passthroughTarget = targetNode.getPassthroughTarget();
if (passthroughTarget) {
targetNode = passthroughTarget.node;
targetInput = passthroughTarget.input;
}
}
if (!targetNode || !targetInput) {
return;
}
// Method 1: Try to get from target node's widget
const targetWidget = targetNode.widgets?.find(w => w.name === targetInput.name);
if (targetWidget && targetWidget.options?.values) {
this.setComboOptions(targetWidget.options.values);
return;
}
// Method 2: Try to get from target node's constructor
const nodeConstructor = targetNode.constructor;
if (nodeConstructor && nodeConstructor.nodeData) {
const inputDef = nodeConstructor.nodeData.input?.required?.[targetInput.name] ||
nodeConstructor.nodeData.input?.optional?.[targetInput.name];
if (inputDef && Array.isArray(inputDef[0])) {
this.setComboOptions(inputDef[0]);
return;
}
}
// Method 3: Try global app nodeData
if (app.nodeData && app.nodeData[targetNode.type]) {
const nodeDef = app.nodeData[targetNode.type];
const inputDef = nodeDef.input?.required?.[targetInput.name] ||
nodeDef.input?.optional?.[targetInput.name];
if (inputDef && Array.isArray(inputDef[0])) {
this.setComboOptions(inputDef[0]);
return;
}
}
this.setComboOptions([""]);
};
// Override onConnectionsChange
const originalOnConnectionsChange = this.onConnectionsChange;
this.onConnectionsChange = function(type, index, connected, link_info) {
if (originalOnConnectionsChange) {
originalOnConnectionsChange.apply(this, arguments);
}
// Update after a short delay
setTimeout(() => {
this.updateComboOptions();
}, 200);
};
// Property change handler
this.onPropertyChanged = function(propName) {
if (propName === "value" && this.valueWidget) {
this.valueWidget.value = this.properties.value;
}
};
// Mouse click handler for dropdown
this.onMouseDown = function(e) {
if (e.canvasY - this.pos[1] < 0) return false;
// Check if click is in dropdown area
const dropdownLeft = this.size[0] - DROPDOWN_RIGHT_PADDING - DROPDOWN_WIDTH;
if (e.canvasX < this.pos[0] + dropdownLeft ||
e.canvasX > this.pos[0] + dropdownLeft + DROPDOWN_WIDTH) return false;
// Show dropdown menu
const options = this.properties.options || [""];
if (options.length > 1) {
const menu = new LiteGraph.ContextMenu(options, {
event: e,
callback: (value) => {
this.properties.value = value;
this.onPropertyChanged("value");
this.setDirtyCanvas(true, true);
}
});
}
return true;
};
// Tooltip state
this.showTooltip = false;
this.tooltipText = "";
// Add the double-click handler for renaming
const originalOnDblClick = this.onDblClick;
this.onDblClick = function(e, pos, canvas) {
// Double-click on label area to rename
if (e.canvasX > this.pos[0] + LABEL_LEFT_PADDING &&
e.canvasX < this.pos[0] + this.size[0] - DROPDOWN_RIGHT_PADDING - DROPDOWN_WIDTH - 10 &&
e.canvasY > this.pos[1] && e.canvasY < this.pos[1] + this.size[1]) {
canvas.prompt("Label", this.properties.label || "Dropdown", function(newLabel) {
if (newLabel && newLabel.trim()) {
this.properties.label = newLabel.trim();
}
}.bind(this), e);
return true;
}
// Force update combo options
this.updateComboOptions();
if (originalOnDblClick) {
return originalOnDblClick.apply(this, arguments);
}
return false;
};
};
// Add drawing functionality
nodeType.prototype.onDrawForeground = function(ctx) {
if (this.flags.collapsed) 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;
}
const fontsize = LiteGraph.NODE_SUBTEXT_SIZE;
const shX = (this.slot_start_y || 0) + fontsize * 1.5;
// Check if this dropdown 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 = (this.properties.label || "Dropdown") !== "Dropdown";
const isActive = isConnected || hasCustomName;
// Choose colors based on active status
const labelColor = isActive ? LABEL_COLOR : INACTIVE_LABEL_COLOR;
const valueColor = isActive ? VALUE_COLOR : INACTIVE_VALUE_COLOR;
// Draw the label
ctx.save();
ctx.fillStyle = labelColor;
ctx.font = fontsize + "px Arial";
ctx.textAlign = "left";
ctx.fillText(this.properties.label || "Dropdown", LABEL_LEFT_PADDING, shX);
// Draw dropdown area - lower position and rounded corners
const dropdownLeft = this.size[0] - DROPDOWN_RIGHT_PADDING - DROPDOWN_WIDTH;
const dropdownY = shX - fontsize + 3; // Lower the box
const dropdownHeight = fontsize + 4;
const cornerRadius = 3;
// Draw rounded dropdown background
ctx.fillStyle = isActive ? DROPDOWN_BG_COLOR : DROPDOWN_BG_COLOR_INACTIVE;
ctx.beginPath();
ctx.roundRect(dropdownLeft, dropdownY - 2, DROPDOWN_WIDTH, dropdownHeight, cornerRadius);
ctx.fill();
// Draw current value
ctx.fillStyle = valueColor;
ctx.font = fontsize + "px Arial";
ctx.textAlign = "center";
const displayValue = this.properties.value || "";
const maxWidth = DROPDOWN_WIDTH - 20;
let truncatedValue = displayValue;
// Check if text is truncated and store the state
this.isTruncated = ctx.measureText(displayValue).width > maxWidth;
if (this.isTruncated) {
truncatedValue = displayValue.substring(0, 16) + "..";
}
ctx.fillText(truncatedValue, dropdownLeft + DROPDOWN_WIDTH/2, shX + 3 + COMBO_TEXT_OFFSET);
// Draw tooltip using same method as preferences dialog
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);
}
// 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);
}
ctx.restore();
};
// Set compute size function to match other CRZ nodes
nodeType.prototype.computeSize = () => [NODE_WIDTH-100, Math.floor(LiteGraph.NODE_SLOT_HEIGHT * NODE_HEIGHT_MULTIPLIER)];
}
}
});