| import type {
|
| LGraphNode,
|
| ContextMenu,
|
| IContextMenuOptions,
|
| IContextMenuValue,
|
| } from "@comfyorg/frontend";
|
|
|
| import {app} from "scripts/app.js";
|
| import {rgthree} from "./rgthree.js";
|
| import {SERVICE as CONFIG_SERVICE} from "./services/config_service.js";
|
|
|
| const SPECIAL_ENTRIES = [/^(CHOOSE|NONE|DISABLE|OPEN)(\s|$)/i, /^\p{Extended_Pictographic}/gu];
|
|
|
| |
| |
| |
|
|
| app.registerExtension({
|
| name: "rgthree.ContextMenuAutoNest",
|
| async setup() {
|
| const logger = rgthree.newLogSession("[ContextMenuAutoNest]");
|
|
|
| const existingContextMenu = LiteGraph.ContextMenu;
|
|
|
|
|
| LiteGraph.ContextMenu = function (
|
| values: IContextMenuValue[],
|
| options: IContextMenuOptions<string, {rgthree_doNotNest: boolean}>,
|
| ) {
|
| const threshold = CONFIG_SERVICE.getConfigValue("features.menu_auto_nest.threshold", 20);
|
| const enabled = CONFIG_SERVICE.getConfigValue("features.menu_auto_nest.subdirs", false);
|
|
|
|
|
| let incompatible: string | boolean = !enabled || !!options?.extra?.rgthree_doNotNest;
|
| if (!incompatible) {
|
| if (values.length <= threshold) {
|
| incompatible = `Skipping context menu auto nesting b/c threshold is not met (${threshold})`;
|
| }
|
|
|
|
|
| if (!(options.parentMenu?.options as any)?.rgthree_originalCallback) {
|
|
|
| if (!options?.callback) {
|
| incompatible = `Skipping context menu auto nesting b/c a callback was expected.`;
|
| } else if (values.some((i) => typeof i !== "string")) {
|
| incompatible = `Skipping context menu auto nesting b/c not all values were strings.`;
|
| }
|
| }
|
| }
|
| if (incompatible) {
|
| if (enabled) {
|
| const [n, v] = logger.infoParts(
|
| "Skipping context menu auto nesting for incompatible menu.",
|
| );
|
| console[n]?.(...v);
|
| }
|
| return existingContextMenu.apply(this as any, [...arguments] as any);
|
| }
|
|
|
| const folders: {[key: string]: IContextMenuValue[]} = {};
|
| const specialOps: IContextMenuValue[] = [];
|
| const folderless: IContextMenuValue[] = [];
|
| for (const value of values) {
|
| if (!value) {
|
| folderless.push(value);
|
| continue;
|
| }
|
| const newValue = typeof value === "string" ? {content: value} : Object.assign({}, value);
|
| (newValue as any).rgthree_originalValue = (value as any).rgthree_originalValue || value;
|
| const valueContent = newValue.content || "";
|
| const splitBy = valueContent.indexOf("/") > -1 ? "/" : "\\";
|
| const valueSplit = valueContent.split(splitBy);
|
| if (valueSplit.length > 1) {
|
| const key = valueSplit.shift()!;
|
| newValue.content = valueSplit.join(splitBy);
|
| folders[key] = folders[key] || [];
|
| folders[key]!.push(newValue);
|
| } else if (SPECIAL_ENTRIES.some((r) => r.test(valueContent))) {
|
| specialOps.push(newValue);
|
| } else {
|
| folderless.push(newValue);
|
| }
|
| }
|
|
|
| const foldersCount = Object.values(folders).length;
|
| if (foldersCount > 0) {
|
|
|
| (options as any).rgthree_originalCallback =
|
| (options as any).rgthree_originalCallback ||
|
| (options.parentMenu?.options as any)?.rgthree_originalCallback ||
|
| options.callback;
|
| const oldCallback = (options as any)?.rgthree_originalCallback;
|
| options.callback = undefined;
|
| const newCallback = (
|
| item: IContextMenuValue,
|
| options: IContextMenuOptions,
|
| event: MouseEvent,
|
| parentMenu: ContextMenu | undefined,
|
| node: LGraphNode,
|
| ) => {
|
| oldCallback?.((item as any)?.rgthree_originalValue!, options, event, undefined, node);
|
| };
|
| const [n, v] = logger.infoParts(`Nested folders found (${foldersCount}).`);
|
| console[n]?.(...v);
|
| const newValues: IContextMenuValue[] = [];
|
| for (const [folderName, folderValues] of Object.entries(folders)) {
|
| newValues.push({
|
| content: `📁 ${folderName}`,
|
| has_submenu: true,
|
| callback: () => {
|
|
|
| },
|
| submenu: {
|
| options: folderValues.map((value) => {
|
| value!.callback = newCallback;
|
| return value;
|
| }),
|
| },
|
| });
|
| }
|
| values = ([] as IContextMenuValue[]).concat(
|
| specialOps.map((f) => {
|
| if (typeof f === "string") {
|
| f = {content: f};
|
| }
|
| f!.callback = newCallback;
|
| return f;
|
| }),
|
| newValues,
|
| folderless.map((f) => {
|
| if (typeof f === "string") {
|
| f = {content: f};
|
| }
|
| f!.callback = newCallback;
|
| return f;
|
| }),
|
| );
|
| }
|
| if (options.scale == null) {
|
| options.scale = Math.max(app.canvas.ds?.scale || 1, 1);
|
| }
|
|
|
| const oldCtrResponse = existingContextMenu.call(this as any, values, options as any);
|
|
|
|
|
|
|
|
|
|
|
|
|
| if ((oldCtrResponse as any)?.constructor) {
|
| (oldCtrResponse as any).constructor = LiteGraph.ContextMenu;
|
| }
|
| return this;
|
| };
|
| },
|
| });
|
|
|