/**
* Context menu functionality for ComfyUI_ControlFreak
*/
import { startMappingNode, startMappingCommand, startQuickMapping } from '../core/learningManager.js';
import { contextProvider } from '../core/contextProvider.js';
import { eventBus } from '../core/eventBus.js';
import { app } from '../../../scripts/app.js'; // Import app
import { showNotification } from '../ui/notifications.js'; // Make sure showNotification is imported
/**
* Position a context menu at the cursor position
* @param {HTMLElement} menuElement - The menu element to position
* @param {MouseEvent} event - The mouse event from which to get cursor position
*/
function positionContextMenu(menuElement, event) {
menuElement.style.top = event.clientY + "px";
menuElement.style.left = event.clientX + "px";
}
// Helper to check if a widget is mappable
function isWidgetMappable(widget) {
return widget &&
(widget.type === "number" ||
widget.type === "slider" ||
widget.type === "combo" ||
widget.type === "toggle" ||
widget.type === "boolean" ||
widget.type === "string") &&
widget.name &&
typeof widget.value !== 'undefined';
}
/**
* Register context menu extensions for nodes
* @param {Object} app - The ComfyUI app instance
*/
export function registerNodeContextMenu(appInstance) {
if (!window.LGraphCanvas || !window.LGraphCanvas.prototype) {
console.warn("ControlFreak: LGraphCanvas not available, cannot register context menu");
return;
}
const originalGetNodeMenuOptions = window.LGraphCanvas.prototype.getNodeMenuOptions;
const originalProcessContextMenu = window.LGraphCanvas.prototype.processContextMenu;
// Override processContextMenu to identify the clicked widget using getWidgetAtCursor
window.LGraphCanvas.prototype.processContextMenu = function(node, event) {
this.clickedWidget = null; // Reset
try {
const widget = this.getWidgetAtCursor();
if (widget && node && node.widgets && node.widgets.includes(widget)) {
this.clickedWidget = widget;
} else {
this.clickedWidget = null;
}
} catch (e) {
console.error("[ControlFreak Debug] Error calling getWidgetAtCursor():", e);
this.clickedWidget = null;
}
// Call the original method AFTER potentially setting this.clickedWidget
originalProcessContextMenu.call(this, node, event);
};
// Override getNodeMenuOptions
window.LGraphCanvas.prototype.getNodeMenuOptions = function(node) {
const options = originalGetNodeMenuOptions.call(this, node);
const clickedWidget = this.clickedWidget;
this.clickedWidget = null;
const mappingEngine = contextProvider.get('mappingEngine');
if (!mappingEngine) return options; // Early exit if engine not ready
let controlFreakMenuItems = [];
let controlFreakOptionsAdded = false;
try {
// 1. Add specific Quick Map / Quick Unmap options if a widget was clicked
if (clickedWidget && isWidgetMappable(clickedWidget)) {
const existingMappings = mappingEngine.getMappingsForWidget(node.id, clickedWidget.name);
controlFreakMenuItems.push(null); // Separator
// Only show Quick Unmap if there is exactly ONE mapping
if (existingMappings.length === 1) {
const mappingToUnmap = existingMappings[0];
// Add Edit Mapping option - this will open the edit dialog directly
controlFreakMenuItems.push({
content: `ControlFreak: Edit Mapping`,
callback: () => {
try {
// Import dynamically to avoid circular dependencies
import('./mappingComponent.js').then(({ showMappingEditor }) => {
showMappingEditor(mappingToUnmap.id);
}).catch(err => {
console.error("ControlFreak: Error importing mappingComponent:", err);
showNotification("Error opening mapping editor", "error");
});
} catch (err) {
console.error("ControlFreak: Error opening mapping editor:", err);
showNotification("Error opening mapping editor", "error");
}
},
className: 'controlfreak-edit-mapping-option'
});
// Add "Toggle Invert Mapping" option
controlFreakMenuItems.push({
content: `ControlFreak: ${mappingToUnmap.transform?.isInverted ? 'Disable' : 'Enable'} Invert Mapping`,
callback: () => {
try {
// Get current invert state and toggle it
const currentIsInverted = !!mappingToUnmap.transform?.isInverted;
const newIsInverted = !currentIsInverted;
// Update the mapping with new transform
const updatedData = {
transform: {
...(mappingToUnmap.transform || {}),
isInverted: newIsInverted
}
};
// Update the mapping and save
mappingEngine.updateMapping(mappingToUnmap.id, updatedData);
mappingEngine.saveMappings();
// Show notification and emit event
showNotification(
`Invert mapping ${newIsInverted ? 'enabled' : 'disabled'} for '${clickedWidget.name}'`,
"success"
);
} catch (err) {
console.error("ControlFreak: Error toggling invert mapping:", err);
showNotification("Error toggling invert mapping", "error");
}
},
className: 'controlfreak-toggle-invert-option'
});
// Add Quick Unmap option (existing code)
controlFreakMenuItems.push({
content: `ControlFreak: Quick Unmap '${clickedWidget.name}'`,
callback: () => {
try {
mappingEngine.deleteMapping(mappingToUnmap.id);
mappingEngine.saveMappings();
showNotification(`Unmapped '${clickedWidget.name}'`, "success");
eventBus.emit('mapping:deleted', { mappingId: mappingToUnmap.id });
} catch (err) {
console.error("ControlFreak: Error during quick unmap:", err);
showNotification("Error unmapping widget", "error");
}
},
className: 'controlfreak-quickunmap-option'
});
} else if (existingMappings.length === 0) {
// Only show Quick Map if there are NO mappings
controlFreakMenuItems.push({
content: `ControlFreak: Quick Map '${clickedWidget.name}'`,
callback: () => {
startQuickMapping(node, clickedWidget);
},
className: 'controlfreak-quickmap-option'
});
} else {
// If > 1 mapping exists, don't show Quick Unmap (user should use panel)
}
controlFreakOptionsAdded = true;
// Add the "Standard Map..." option directly since a mappable widget was clicked
controlFreakMenuItems.push({
content: `ControlFreak: Standard Map '${clickedWidget.name}'...`,
callback: () => {
startMappingNode(node.id, clickedWidget.name);
eventBus.emit('menu:mappingStarted', {
type: 'widget',
nodeId: node.id,
nodeType: node.type,
nodeTitle: node.title || node.type,
widgetName: clickedWidget.name,
widgetType: clickedWidget.type
});
},
className: 'controlfreak-standardmap-option' // Optional: Add class for styling
});
} else if (clickedWidget) {
// Clicked on a widget, but it's not mappable (e.g., button, custompaint)
// Do nothing specific for ControlFreak here for now
}
// ELSE: Clicked on node background OR a non-mappable widget
else if (node.widgets && node.widgets.length > 0) {
const mappableWidgets = node.widgets.filter(isWidgetMappable);
if (mappableWidgets.length > 0) {
// Only show the "Map Control..." submenu if no specific widget was clicked
const widgetMappingOptions = mappableWidgets.map(widget => ({
content: widget.name,
submenu: {
options: [
{
content: "Standard Map...",
callback: () => {
startMappingNode(node.id, widget.name);
eventBus.emit('menu:mappingStarted', {
type: 'widget',
nodeId: node.id,
nodeType: node.type,
nodeTitle: node.title || node.type,
widgetName: widget.name,
widgetType: widget.type
});
}
},
{
content: "Quick Map",
callback: () => {
startQuickMapping(node, widget);
eventBus.emit('menu:quickMappingStarted', {
type: 'widget',
nodeId: node.id,
nodeType: node.type,
nodeTitle: node.title || node.type,
widgetName: widget.name,
widgetType: widget.type
});
}
}
]
}
}));
// Check if there are actually mappable widgets before adding the submenu
if (widgetMappingOptions.length > 0) {
if (controlFreakMenuItems.length === 0 || (controlFreakMenuItems.length > 0 && controlFreakMenuItems[0] !== null) ) {
controlFreakMenuItems.push(null); // Separator needed
}
controlFreakMenuItems.push({
content: `ControlFreak: Map Control...`,
submenu: {
options: widgetMappingOptions
}
});
controlFreakOptionsAdded = true;
}
}
}
// Add all prepared ControlFreak options
if (controlFreakOptionsAdded) {
options.push(...controlFreakMenuItems);
}
} catch (error) {
console.error("ControlFreak: Error creating controller mapping menu:", error);
}
return options;
};
}
/**
* Register context menu for the queue button
*/
export function registerQueueButtonMenu() {
try {
// Find the queue button
const queueButton = document.getElementById("queue-button");
if (!queueButton) {
return;
}
// Add the context menu handler
queueButton.addEventListener("contextmenu", (e) => {
e.preventDefault();
// Create a simple context menu for the queue button
const menu = document.createElement("div");
menu.className = "litegraph-context-menu";
positionContextMenu(menu, e);
const option = document.createElement("div");
option.className = "menu-entry";
option.textContent = "Map Controller to Queue Button";
option.addEventListener("click", () => {
// Use the imported function directly
startMappingCommand("Comfy.QueuePrompt", {
label: "Queue Prompt",
description: "Execute current workflow"
});
// Remove the menu
document.body.removeChild(menu);
// Emit an event so we can track this in analytics if desired
eventBus.emit('menu:mappingStarted', {
type: 'command',
commandId: 'Comfy.QueuePrompt',
commandLabel: 'Queue Prompt'
});
});
menu.appendChild(option);
document.body.appendChild(menu);
// Remove the menu when clicking elsewhere
const removeMenu = () => {
if (document.body.contains(menu)) {
document.body.removeChild(menu);
}
document.body.removeEventListener("click", removeMenu);
};
setTimeout(() => {
document.body.addEventListener("click", removeMenu);
}, 0);
});
} catch (error) {
console.error("ControlFreak: Error registering queue button menu:", error);
}
}