Files
olivv-cs-ComfyUI-FunPack/widgets.py
T
digital-garbageandClaude Opus 5 614ce0a196 feat(models): the LoRA loader gets a real picker; advanced inputs fold away
Rows are picked from a searchable list of installed files instead of typed into a
table of dropdowns, and an input the node calls advanced (the trained stack) is
folded away unless something feeds it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 19:29:35 +03:00

114 lines
4.3 KiB
Python

"""Declarative list inputs: one STRING widget holding a JSON array of rows.
ComfyUI has no repeatable input, so a node that needs N of something (text encoders,
LoRAs) carries ONE string widget whose value is a JSON array. The `funpack_list` entry
in that widget's options describes a row, so both frontends can render it as rows with
an Add button instead of raw JSON: the canvas widget in `web/funpack_list.js`, and the
FunPack Editor's node page (`movie_editor/frontend/models.js`).
The node itself never has to care which frontend wrote the value — `parse_rows` accepts
the JSON string, an already-parsed list, or a single row.
"""
import json
# Row field kinds a frontend must know how to draw.
FIELD_KINDS = {"combo", "string", "int", "float", "boolean"}
def field(name, kind, *, label=None, choices=None, default=None, min=None, max=None,
step=None, tooltip=None, width=None):
"""One column of a list row."""
spec = {"name": name, "kind": kind, "label": label or name}
if choices is not None:
spec["choices"] = list(choices)
if default is not None:
spec["default"] = default
for key, value in (("min", min), ("max", max), ("step", step),
("tooltip", tooltip), ("width", width)):
if value is not None:
spec[key] = value
return spec
def list_widget(item, fields, *, default=None, add_label=None, max_rows=0, tooltip=None,
allow_empty=False, picker=None):
"""An INPUT_TYPES entry for a list input. Returns the ("STRING", {...}) tuple.
`allow_empty` marks a list whose empty state is a working state — the node passes its
input through — so a frontend does not flag it as something to fix.
"""
spec = {"item": item, "fields": list(fields), "add_label": add_label or f"+ Add {item}"}
if max_rows:
spec["max_rows"] = int(max_rows)
if allow_empty:
spec["allow_empty"] = True
if picker:
# Rows are PICKED from this field's file list rather than typed into a table: the
# frontend shows a searchable list of what is installed, and the row is that file
# plus its settings. A table of combos is a spreadsheet; this is a picker.
spec["picker"] = picker
opts = {
"default": json.dumps(default or []),
"multiline": False,
"funpack_list": spec,
}
if tooltip:
opts["tooltip"] = tooltip
return ("STRING", opts)
def _coerce(value, kind, default=None):
if kind == "boolean":
if isinstance(value, str):
return value.strip().lower() in {"1", "true", "yes", "on"}
return bool(value) if value is not None else bool(default)
if kind in ("int", "float"):
try:
number = float(value)
except (TypeError, ValueError):
number = float(default or 0)
if number != number or number in (float("inf"), float("-inf")):
number = float(default or 0)
return int(number) if kind == "int" else number
if value is None:
return default
return value
def parse_rows(value, fields, *, key=None, drop_empty=True):
"""Normalize a list widget's value into a list of row dicts.
`key` names the field a row is worthless without (the file name, usually); rows whose
key is missing, empty or "None" are dropped unless `drop_empty` is off. A row may also
carry "on": False to be skipped without deleting it.
"""
if isinstance(value, str):
try:
value = json.loads(value or "[]")
except (TypeError, ValueError, json.JSONDecodeError):
return []
if isinstance(value, dict):
value = [value]
if not isinstance(value, list):
return []
specs = {f["name"]: f for f in fields}
if key is None and fields:
key = fields[0]["name"]
rows = []
for index, raw in enumerate(value):
if not isinstance(raw, dict):
continue
if not _coerce(raw.get("on", True), "boolean", True):
continue
row = {"index": index}
for name, spec in specs.items():
row[name] = _coerce(raw.get(name), spec["kind"], spec.get("default"))
if drop_empty and key:
k = row.get(key)
if k is None or (isinstance(k, str) and k.strip() in ("", "None")):
continue
rows.append(row)
return rows