361 lines
14 KiB
JavaScript
361 lines
14 KiB
JavaScript
// Extension to add shift-click functionality for widget/input conversion
|
|
(function() {
|
|
class WidgetHotkeyHandler {
|
|
constructor(app) {
|
|
this.app = app;
|
|
console.log("[Widget Hotkey] Starting to load extension...");
|
|
this.setupEventHandler();
|
|
}
|
|
|
|
setupEventHandler() {
|
|
if (!this.app.canvas) {
|
|
console.log("[Widget Hotkey] Canvas not available yet, waiting...");
|
|
setTimeout(() => this.setupEventHandler(), 100);
|
|
return;
|
|
}
|
|
|
|
console.log("[Widget Hotkey] Setting up event handler...");
|
|
|
|
// Store the original mousedown handler
|
|
const originalMouseDown = this.app.canvas.onMouseDown;
|
|
|
|
// Override the mousedown handler
|
|
this.app.canvas.onMouseDown = (e) => {
|
|
console.log("[Widget Hotkey] Canvas mouse down:", {
|
|
shift: e.ctrlKey,
|
|
canvasX: e.canvasX,
|
|
canvasY: e.canvasY,
|
|
api: !!this.app.api,
|
|
graph: !!this.app.graph
|
|
});
|
|
|
|
if (e.ctrlKey) {
|
|
// Get the node under the mouse
|
|
const node = this.app.graph.getNodeOnPos(e.canvasX, e.canvasY);
|
|
if (node) {
|
|
// Convert canvas position to node local position
|
|
const localPos = [
|
|
e.canvasX - node.pos[0],
|
|
e.canvasY - node.pos[1]
|
|
];
|
|
|
|
// Debug all widget positions
|
|
if (node.widgets) {
|
|
console.log("[Widget Hotkey] All widgets:", node.widgets.map(w => ({
|
|
name: w.name,
|
|
type: w.type,
|
|
pos: w.last_pos,
|
|
computedPos: this.computeWidgetPos(node, w),
|
|
origType: w.origType,
|
|
isConverted: w.type?.startsWith("converted-widget")
|
|
})));
|
|
}
|
|
|
|
// Find widget at position
|
|
const widget = this.findWidgetAtPos(node, localPos);
|
|
|
|
console.log("[Widget Hotkey] Detection results:", {
|
|
nodeTitle: node.title,
|
|
localPos,
|
|
foundWidget: widget ? widget.name : "none",
|
|
widgetType: widget ? widget.type : "none",
|
|
isConverted: widget ? widget.type?.startsWith("converted-widget") : false
|
|
});
|
|
|
|
if (widget) {
|
|
console.log("[Widget Hotkey] Widget found:", {
|
|
name: widget.name,
|
|
type: widget.type,
|
|
origType: widget.origType,
|
|
isConverted: widget.type?.startsWith("converted-widget")
|
|
});
|
|
|
|
return this.handleWidgetClick(node, widget);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Call the original handler
|
|
if (originalMouseDown) {
|
|
return originalMouseDown.call(this.app.canvas, e);
|
|
}
|
|
};
|
|
|
|
console.log("[Widget Hotkey] Event handler setup complete!");
|
|
}
|
|
|
|
computeWidgetPos(node, widget) {
|
|
const titleHeight = 30;
|
|
const widgetHeight = 20;
|
|
const widgetSpacing = 4;
|
|
let y = titleHeight;
|
|
|
|
// Find this widget's position
|
|
for (const w of node.widgets) {
|
|
// Skip converted widgets in position calculation
|
|
if (w.type?.startsWith("converted-widget")) {
|
|
continue;
|
|
}
|
|
if (w === widget) {
|
|
return {
|
|
x: 10,
|
|
y: y,
|
|
width: node.size[0] - 20,
|
|
height: widgetHeight
|
|
};
|
|
}
|
|
y += widgetHeight + widgetSpacing;
|
|
}
|
|
|
|
// For converted widgets, check if they have a corresponding input
|
|
if (widget.type?.startsWith("converted-widget")) {
|
|
const inputIndex = node.inputs.findIndex(i => i.widget?.name === widget.name);
|
|
if (inputIndex !== -1) {
|
|
return {
|
|
x: 0,
|
|
y: titleHeight + inputIndex * (widgetHeight + widgetSpacing),
|
|
width: 20,
|
|
height: widgetHeight
|
|
};
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
findWidgetAtPos(node, localPos) {
|
|
// Constants for layout
|
|
const titleHeight = 30;
|
|
const widgetHeight = 20;
|
|
const widgetSpacing = 4;
|
|
const slotHeight = LiteGraph.NODE_SLOT_HEIGHT;
|
|
|
|
// Count number of inputs before each widget to adjust y position
|
|
let inputCount = 0;
|
|
let y = titleHeight;
|
|
|
|
// First check regular widgets
|
|
for (const widget of node.widgets) {
|
|
// Skip converted widgets
|
|
if (widget.type?.startsWith("converted-widget")) {
|
|
continue;
|
|
}
|
|
|
|
// Adjust y position based on inputs that come before this widget
|
|
while (inputCount < node.inputs.length &&
|
|
(!node.inputs[inputCount].widget ||
|
|
node.inputs[inputCount].widget.name !== widget.name)) {
|
|
y += slotHeight;
|
|
inputCount++;
|
|
}
|
|
|
|
// Check if point is inside widget area
|
|
if (localPos[0] >= 10 &&
|
|
localPos[0] <= node.size[0] - 10 &&
|
|
localPos[1] >= y &&
|
|
localPos[1] <= y + widgetHeight) {
|
|
return widget;
|
|
}
|
|
|
|
y += widgetHeight + widgetSpacing;
|
|
}
|
|
|
|
// Reset for checking converted widgets
|
|
y = titleHeight;
|
|
inputCount = 0;
|
|
|
|
// Then check converted widgets (inputs)
|
|
for (const input of node.inputs) {
|
|
if (input.widget?.type?.startsWith("converted-widget")) {
|
|
// Check if point is inside input area
|
|
if (localPos[0] >= 0 &&
|
|
localPos[0] <= 20 &&
|
|
localPos[1] >= y &&
|
|
localPos[1] <= y + slotHeight) {
|
|
return input.widget;
|
|
}
|
|
}
|
|
y += slotHeight;
|
|
inputCount++;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
handleWidgetClick(node, widget) {
|
|
// If it's already an input (has origType), convert back to widget
|
|
if (widget.origType) {
|
|
console.log("[Widget Hotkey] Converting input back to widget:", {
|
|
name: widget.name,
|
|
origType: widget.origType,
|
|
currentType: widget.type
|
|
});
|
|
this.convertToWidget(node, widget);
|
|
} else {
|
|
// Get widget config and check if convertible
|
|
const config = [widget.type, widget.options || {}];
|
|
console.log("[Widget Hotkey] Widget config:", {
|
|
widget: widget.name,
|
|
config: config,
|
|
type: widget.type,
|
|
options: widget.options
|
|
});
|
|
|
|
const isConvertible = this.isConvertibleWidget(widget, config);
|
|
console.log("[Widget Hotkey] Is convertible:", {
|
|
widget: widget.name,
|
|
isConvertible: isConvertible,
|
|
type: widget.type,
|
|
configType: config[0],
|
|
forceInput: widget.options?.forceInput
|
|
});
|
|
|
|
if (isConvertible) {
|
|
console.log("[Widget Hotkey] Converting widget to input");
|
|
this.convertToInput(node, widget, config);
|
|
console.log("[Widget Hotkey] Conversion complete. New widget state:", {
|
|
name: widget.name,
|
|
type: widget.type,
|
|
origType: widget.origType
|
|
});
|
|
} else {
|
|
console.log("[Widget Hotkey] Widget is not convertible");
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
convertToWidget(node, widget) {
|
|
console.log("[Widget Hotkey] Converting to widget:", widget.name);
|
|
widget.type = widget.origType;
|
|
widget.computeSize = widget.origComputeSize;
|
|
widget.serializeValue = widget.origSerializeValue;
|
|
delete widget.origType;
|
|
delete widget.origComputeSize;
|
|
delete widget.origSerializeValue;
|
|
if (widget.linkedWidgets) {
|
|
for (const w of widget.linkedWidgets) {
|
|
this.convertToWidget(node, w);
|
|
}
|
|
}
|
|
|
|
const [oldWidth, oldHeight] = node.size;
|
|
node.removeInput(node.inputs.findIndex((i) => i.widget?.name === widget.name));
|
|
for (const widget2 of node.widgets) {
|
|
widget2.last_y -= this.app.canvas.constructor.NODE_SLOT_HEIGHT;
|
|
}
|
|
node.setSize([
|
|
Math.max(oldWidth, node.size[0]),
|
|
Math.max(oldHeight, node.size[1])
|
|
]);
|
|
|
|
// Add node redraw
|
|
this.app.graph.setDirtyCanvas(true, true); // Mark both the node and connections as dirty
|
|
this.app.graph.change(); // Trigger graph change event
|
|
}
|
|
|
|
recreateNode(node) {
|
|
// Store the node's current state
|
|
const id = node.id;
|
|
const pos = [...node.pos];
|
|
const size = [...node.size];
|
|
const type = node.type;
|
|
const inputs = [...node.inputs];
|
|
const outputs = [...node.outputs];
|
|
const properties = {...node.properties};
|
|
const widgets = node.widgets ? node.widgets.map(w => ({...w})) : [];
|
|
|
|
// Remove the old node
|
|
this.app.graph.remove(node);
|
|
|
|
// Create a new node of the same type
|
|
const newNode = LiteGraph.createNode(type);
|
|
newNode.id = id;
|
|
newNode.pos = pos;
|
|
newNode.size = size;
|
|
|
|
// Configure the new node
|
|
newNode.configure({
|
|
id: id,
|
|
type: type,
|
|
pos: pos,
|
|
size: size,
|
|
properties: properties,
|
|
inputs: inputs,
|
|
outputs: outputs,
|
|
widgets_values: widgets.map(w => w.value)
|
|
});
|
|
|
|
// Add it to the graph
|
|
this.app.graph.add(newNode);
|
|
|
|
return newNode;
|
|
}
|
|
|
|
convertToInput(node, widget, config) {
|
|
const pos = node.widgets.indexOf(widget);
|
|
if (pos === -1) return;
|
|
|
|
widget.origType = widget.type;
|
|
widget.origComputeSize = widget.computeSize;
|
|
widget.origSerializeValue = widget.serializeValue;
|
|
widget.type = "converted-widget";
|
|
widget.computeSize = null;
|
|
widget.serializeValue = null;
|
|
|
|
const w = {
|
|
name: widget.name,
|
|
type: config[0],
|
|
options: config[1],
|
|
widget: widget,
|
|
};
|
|
|
|
node.addInput(node.inputs.length, w.type, w);
|
|
|
|
// Recompute widget positions
|
|
let y = 30; // Start after title
|
|
for (const w of node.widgets) {
|
|
if (!w.type?.startsWith("converted-widget")) {
|
|
w.last_y = y;
|
|
y += 20 + 4; // widget height + spacing
|
|
}
|
|
}
|
|
|
|
// Recreate the node to ensure proper rendering
|
|
this.recreateNode(node);
|
|
}
|
|
|
|
isConvertibleWidget(widget, config) {
|
|
const VALID_TYPES = [
|
|
"STRING",
|
|
"combo",
|
|
"number",
|
|
"toggle",
|
|
"BOOLEAN",
|
|
"text",
|
|
"string"
|
|
];
|
|
return (VALID_TYPES.includes(widget.type) || VALID_TYPES.includes(config[0])) && !widget.options?.forceInput;
|
|
}
|
|
}
|
|
|
|
function registerWidgetHotkeyExtension() {
|
|
const app = window.app || window.ComfyApp || window.comfyAPI?.app?.app;
|
|
if (app && app.registerExtension) {
|
|
app.registerExtension({
|
|
name: "RyanOnTheInside.WidgetInputHotkey",
|
|
setup(app) {
|
|
console.log("[Widget Hotkey] Setup phase starting...");
|
|
// Create an instance of the handler class and initialize it
|
|
new WidgetHotkeyHandler(app);
|
|
}
|
|
});
|
|
} else {
|
|
// Try again after a short delay if app is not available yet
|
|
setTimeout(registerWidgetHotkeyExtension, 100);
|
|
}
|
|
}
|
|
|
|
// Start trying to register the extension
|
|
registerWidgetHotkeyExtension();
|
|
})();
|