406 lines
19 KiB
JavaScript
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)];
|
|
}
|
|
}
|
|
}); |