diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0a19790 --- /dev/null +++ b/.gitignore @@ -0,0 +1,174 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..d558042 --- /dev/null +++ b/__init__.py @@ -0,0 +1,15 @@ +from .ui_node import UINode + +__version__ = "1.0.0" + +NODE_CLASS_MAPPINGS = { + "UINode": UINode +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "UINode": "UINode [GroxicTinch]", +} + +WEB_DIRECTORY = "./web/comfyui" + +__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"] \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d0e59e3 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "EasyUI" +description = "Allows making a mirror of options that are on a node, for use creating your own UI" +version = "1.0.2" +license = {text = "MIT License"} + +[tool.comfy] +PublisherId = "GroxicTinch" +DisplayName = "EasyUI" +Icon = "" diff --git a/ui_node.py b/ui_node.py new file mode 100644 index 0000000..7fac534 --- /dev/null +++ b/ui_node.py @@ -0,0 +1,14 @@ +class UINode: + @classmethod + def INPUT_TYPES(cls): + return {} + RETURN_TYPES = () + FUNCTION = "return_empty" + CATEGORY = "EasyUI" + + def __init__(self): + self.original_node_id = None + self.original_node_type = None + + def return_empty(self): + return () \ No newline at end of file diff --git a/web/comfyui/easyui.js b/web/comfyui/easyui.js new file mode 100644 index 0000000..20083d9 --- /dev/null +++ b/web/comfyui/easyui.js @@ -0,0 +1,216 @@ +import { app } from "../../scripts/app.js"; + +// [FIXME] Delete the UINode if the original node is deleted +// [TODO] Add a way to get taken to the original node from the UINode + +var ignoredWidgets = ['button']; + +/** ───────────────────────────────────────────────────────────────────── + * 1) Extract your two bits of logic into named functions. + * These will live in module scope so both the commands and the + * context-menu callbacks can refer to them. + * ───────────────────────────────────────────────────────────────────── */ +function createMirror(node) { + if (!node) return; + const graph = node.graph ?? app.canvas.graph; + const uiNode = LiteGraph.createNode("UINode"); + graph.add(uiNode); + + uiNode.properties = uiNode.properties || {}; + uiNode.properties.original_node_id = node.id; + + // position & clear I/O + uiNode.pos = [node.pos[0] + 50, node.pos[1] + 50]; + uiNode.inputs = []; + uiNode.outputs = []; + + // 1) redraw immediately so the DOM is created + app.canvas.setDirty(true); + app.canvas.draw(true); + + // [FIXME] elements arent working (preview image, customtext) + + // // 2) find the newly-rendered node element + // uiNode.element = node.element; + + // 3) pass that nodeEl into cloneWidgets + cloneWidgets(node, uiNode); + } + +/** ───────────────────────────────────────────────────────────────────── + * 2) Register a single extension that + * a) declares both commands (so invokeExtensionCommand() can find them) + * b) monkey-patches getExtraMenuOptions to call them in the right-click menu + * ───────────────────────────────────────────────────────────────────── */ +app.registerExtension({ + name: 'easyui', // just one extension now + + beforeRegisterNodeDef(nodeTypeInfo) { + const proto = nodeTypeInfo.prototype; + const original = proto.getExtraMenuOptions; + + proto.getExtraMenuOptions = function(_, options = []) { + // 1) keep all built-ins + if (original) { + original.apply(this, arguments); + } + + options.push(null); // inserts a divider + // 2) if it's a UINode, Don't add “Create Mirror Copy” + if (this.type !== 'UINode') { + options.push({ + content: '🖼️ EasyUI – Create Mirror Copy', + callback: () => createMirror(this) + }); + } + + // 3) if it’s a UINode, also add “Toggle Widget Visibility” + if (this.type === 'UINode') { + options.push({ + content: '🖼️ EasyUI – Toggle Widget Visibility', + has_submenu: true, + callback: make_submenu + }); + } + options.push(null); // inserts a divider + + return options; + }; + + // Add configure override for UINode to restore widgets on load + if (nodeTypeInfo.comfyClass === 'UINode') { + const originalConfigure = proto.configure; + proto.configure = function(serializedNode) { + // Call the original configure method first + if (originalConfigure) { + originalConfigure.apply(this, arguments); + } + // Clone widgets from the original node + const originalNodeId = this.properties.original_node_id; + if (originalNodeId) { + const originalNode = app.canvas.graph.getNodeById(originalNodeId); + if (originalNode) { + cloneWidgets(originalNode, this); + } else { + console.warn(`Original node with ID ${originalNodeId} not found.`); + } + } + }; + } + } +}); + +function make_submenu(value, options, e, menu, node) { + var subOptionsDisabled = []; + var subOptionsEnabled = []; + + const originalNodeId = node.properties.original_node_id; + if (originalNodeId) { + const originalNode = app.canvas.graph.getNodeById(originalNodeId); + if (originalNode) { + // Loop through the widgets and create sub-options + originalNode.widgets.forEach(widget => { + const isHidden = node.properties.hiddenWidgets.includes(widget.name); + if (isHidden) { + if(!widget.computedDisabled && !ignoredWidgets.includes(type)) { + subOptionsDisabled.push(`Enable ${widget.name}`); + } + } else { + subOptionsEnabled.push(`Disable ${widget.name}`); + } + }); + } else { + console.warn(`Original node with ID ${originalNodeId} not found.`); + } + } + + if(subOptionsDisabled.length > 0) { + subOptionsDisabled.push(null); // inserts a divider + } + + var subOptions = subOptionsDisabled.concat(subOptionsEnabled); + + if (subOptions.length === 0) { + subOptions.push("No widgets to toggle"); + } + + const submenu = new LiteGraph.ContextMenu( + subOptions, + { + event: e, + callback: function (v) { + if (v === "No widgets to toggle") { + return; + } + if (v.startsWith("Disable")) { + const widgetName = v.replace("Disable ", ""); + node.properties.hiddenWidgets.push(widgetName); + } else if (v.startsWith("Enable")) { + const widgetName = v.replace("Enable ", ""); + node.properties.hiddenWidgets = node.properties.hiddenWidgets.filter(name => name !== widgetName); + } + + const originalNode = app.canvas.graph.getNodeById(node.properties.original_node_id); + if (originalNode) { + cloneWidgets(originalNode, node); + app.canvas.setDirty(true); + app.canvas.draw(true); + } + }, + parentMenu: menu, + node:node + } + ) +} + +/** ───────────────────────────────────────────────────────────────────── + * 3) Keep your widget-cloning helper unchanged + * ───────────────────────────────────────────────────────────────────── */ +function cloneWidgets(originalNode, uiNode) { + uiNode.widgets = []; + uiNode.widgets_values = []; + uiNode.properties.hiddenWidgets = uiNode.properties.hiddenWidgets || []; + + originalNode.widgets.forEach(origW => { + const type = origW.type || 'string'; + const options = origW.options || {}; + try { + // Example: Mark some widgets to be hidden initially + const isHidden = uiNode.properties.hiddenWidgets.includes(origW.name); + if (origW.computedDisabled || isHidden || ignoredWidgets.includes(type)) { + if (!isHidden) { + uiNode.properties.hiddenWidgets.push(origW.name); + } + return; + } + + const oldCallback = origW.callback; + + const newW = uiNode.addWidget( + type, + origW.name, + origW.value, + function(...args) { + const v = args[0]; + origW.value = v; + if (typeof oldCallback === 'function') { + oldCallback.call(origW, ...args); + } + app.canvas.setDirty(true); + }, + options + ); + + origW.callback = function(...args) { + const v = args[0]; + if (typeof oldCallback === 'function') { + oldCallback.call(this, ...args); + } + newW.value = v; + app.canvas.setDirty(true); + }; + } catch (err) { + console.warn('Skipping unsupported widget:', origW, err); + } + }); +} \ No newline at end of file