This commit is contained in:
GroxicTinch
2025-05-12 09:18:33 +08:00
parent 89be333510
commit c812e64a6d
5 changed files with 429 additions and 0 deletions
+174
View File
@@ -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
+15
View File
@@ -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"]
+10
View File
@@ -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 = ""
+14
View File
@@ -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 ()
+216
View File
@@ -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);
}
});
}