Files
ComfyUI_frontend/tools/devtools/nodes/inputs.py
AustinMroz 13b42d9b59 Ensure dynamic combo children cleanup state (#13073)
#12617 introduced a regression in Dynamic Combos. If two options have
child widgets of the same name (such as `bit_depth` on `Save Image
(Advanced)`), then widget state would be incorrectly shared between the
two widgets.

This is resolved by having removed widgets also delete their state.

There was previous interest in having widgets of this type keep state
when valid. This interest remains, but will require a more controlled
intentional implementation in the future.

Since the bit depth options on `Save Image (Advanced)` could potentially
be expanded in the future, this PR specifically adds a new devtools node
for testing with.

---------

Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-06-26 01:08:06 +00:00

429 lines
12 KiB
Python

from __future__ import annotations
import time
from comfy_api.v0_0_2 import IO
class LongComboDropdown:
@classmethod
def INPUT_TYPES(cls):
return {"required": {"option": ([f"Option {i}" for i in range(1_000)],)}}
RETURN_TYPES = ()
OUTPUT_NODE = True
FUNCTION = "long_combo_dropdown"
CATEGORY = "DevTools"
DESCRIPTION = "A long combo dropdown"
def long_combo_dropdown(self, option: str):
print(option)
class NodeWithOptionalInput:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {"required_input": ("IMAGE",)},
"optional": {"optional_input": ("IMAGE", {"default": None})},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "node_with_optional_input"
CATEGORY = "DevTools"
DESCRIPTION = "A node with an optional input"
def node_with_optional_input(self, required_input, optional_input=None):
print(
f"Calling node with required_input: {required_input} and optional_input: {optional_input}"
)
return (required_input,)
class NodeWithOptionalComboInput:
@classmethod
def INPUT_TYPES(cls):
return {
"optional": {
"optional_combo_input": (
[f"Random Unique Option {time.time()}" for _ in range(8)],
{"default": None},
)
},
}
RETURN_TYPES = ("STRING",)
FUNCTION = "node_with_optional_combo_input"
CATEGORY = "DevTools"
DESCRIPTION = "A node with an optional combo input that returns unique values every time INPUT_TYPES is called"
def node_with_optional_combo_input(self, optional_combo_input=None):
print(f"Calling node with optional_combo_input: {optional_combo_input}")
return (optional_combo_input,)
class NodeWithOnlyOptionalInput:
@classmethod
def INPUT_TYPES(s):
return {
"optional": {
"text": ("STRING", {"multiline": True, "dynamicPrompts": True}),
"clip": ("CLIP", {}),
}
}
RETURN_TYPES = ()
FUNCTION = "node_with_only_optional_input"
CATEGORY = "DevTools"
DESCRIPTION = "A node with only optional input"
def node_with_only_optional_input(self, clip=None, text=None):
pass
class NodeWithOutputList:
@classmethod
def INPUT_TYPES(cls):
return {"required": {}}
RETURN_TYPES = (
"INT",
"INT",
)
RETURN_NAMES = (
"INTEGER OUTPUT",
"INTEGER LIST OUTPUT",
)
OUTPUT_IS_LIST = (
False,
True,
)
FUNCTION = "node_with_output_list"
CATEGORY = "DevTools"
DESCRIPTION = "A node with an output list"
def node_with_output_list(self):
return (1, [1, 2, 3])
class NodeWithForceInput:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"int_input": ("INT", {"forceInput": True}),
"int_input_widget": ("INT", {"default": 1}),
},
"optional": {"float_input": ("FLOAT", {"forceInput": True})},
}
RETURN_TYPES = ()
OUTPUT_NODE = True
FUNCTION = "node_with_force_input"
CATEGORY = "DevTools"
DESCRIPTION = "A node with a forced input"
def node_with_force_input(
self, int_input: int, int_input_widget: int, float_input: float = 0.0
):
print(
f"int_input: {int_input}, int_input_widget: {int_input_widget}, float_input: {float_input}"
)
class NodeWithDefaultInput:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"int_input": ("INT", {"defaultInput": True}),
"int_input_widget": ("INT", {"default": 1}),
},
"optional": {"float_input": ("FLOAT", {"defaultInput": True})},
}
RETURN_TYPES = ()
OUTPUT_NODE = True
FUNCTION = "node_with_default_input"
CATEGORY = "DevTools"
DESCRIPTION = "A node with a default input"
def node_with_default_input(
self, int_input: int, int_input_widget: int, float_input: float = 0.0
):
print(
f"int_input: {int_input}, int_input_widget: {int_input_widget}, float_input: {float_input}"
)
class NodeWithStringInput:
@classmethod
def INPUT_TYPES(cls):
return {"required": {"string_input": ("STRING",)}}
RETURN_TYPES = ()
FUNCTION = "node_with_string_input"
CATEGORY = "DevTools"
DESCRIPTION = "A node with a string input"
def node_with_string_input(self, string_input: str):
print(f"string_input: {string_input}")
class NodeWithUnionInput:
@classmethod
def INPUT_TYPES(cls):
return {
"optional": {
"string_or_int_input": ("STRING,INT",),
"string_input": ("STRING", {"forceInput": True}),
"int_input": ("INT", {"forceInput": True}),
}
}
RETURN_TYPES = ()
OUTPUT_NODE = True
FUNCTION = "node_with_union_input"
CATEGORY = "DevTools"
DESCRIPTION = "A node with a union input"
def node_with_union_input(
self,
string_or_int_input: str | int = "",
string_input: str = "",
int_input: int = 0,
):
print(
f"string_or_int_input: {string_or_int_input}, string_input: {string_input}, int_input: {int_input}"
)
return {
"ui": {
"text": string_or_int_input,
}
}
class NodeWithBooleanInput:
@classmethod
def INPUT_TYPES(cls):
return {"required": {"boolean_input": ("BOOLEAN",)}}
RETURN_TYPES = ()
FUNCTION = "node_with_boolean_input"
CATEGORY = "DevTools"
DESCRIPTION = "A node with a boolean input"
def node_with_boolean_input(self, boolean_input: bool):
print(f"boolean_input: {boolean_input}")
class SimpleSlider:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": (
"FLOAT",
{
"display": "slider",
"default": 0.5,
"min": 0.0,
"max": 1.0,
"step": 0.001,
},
),
},
}
RETURN_TYPES = ("FLOAT",)
FUNCTION = "execute"
CATEGORY = "DevTools"
def execute(self, value):
return (value,)
class NodeWithSeedInput:
@classmethod
def INPUT_TYPES(cls):
return {"required": {"seed": ("INT", {"default": 0})}}
RETURN_TYPES = ()
FUNCTION = "node_with_seed_input"
CATEGORY = "DevTools"
DESCRIPTION = "A node with a seed input"
OUTPUT_NODE = True
def node_with_seed_input(self, seed: int):
print(f"seed: {seed}")
class NodeWithValidation:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {"int_input": ("INT",)},
}
@classmethod
def VALIDATE_INPUTS(cls, int_input: int):
if int_input < 0:
raise ValueError("int_input must be greater than 0")
return True
RETURN_TYPES = ()
FUNCTION = "execute"
CATEGORY = "DevTools"
DESCRIPTION = "A node that validates an input"
OUTPUT_NODE = True
def execute(self, int_input: int):
print(f"int_input: {int_input}")
return tuple()
class NodeWithV2ComboInput:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"combo_input": (
"COMBO",
{"options": ["A", "B"]},
),
}
}
RETURN_TYPES = ("COMBO",)
FUNCTION = "node_with_v2_combo_input"
CATEGORY = "DevTools"
DESCRIPTION = (
"A node that outputs a combo type that adheres to the v2 combo input spec"
)
def node_with_v2_combo_input(self, combo_input: str):
return (combo_input,)
class NodeWithLegacyWidget:
@classmethod
def INPUT_TYPES(cls):
return {
"required": { "legacy_widget": ("INT", { "widgetType": "DEVTOOLSLEGACYWIDGET" }) }
}
RETURN_TYPES = ()
FUNCTION = "node_with_legacy_widget"
CATEGORY = "DevTools"
DESCRIPTION = ("A node with a legacy widget")
def node_with_legacy_widget(self):
return ()
class NodeWithPriceBadge(IO.ComfyNode):
@classmethod
def define_schema(cls):
return IO.Schema(
node_id="DevToolsNodeWithPriceBadge",
display_name="Node With Price Badge",
description="An API node with a price badge",
inputs=[IO.Combo.Input("price", options=["1x", "2x", "3x"])],
is_api_node=True,
price_badge=IO.PriceBadge(
depends_on=IO.PriceBadgeDepends(widgets=["price"]),
expr="""
(
$p := widgets.price;
{"type":"usd","usd": $contains($p, "2x") ? 2 : $contains($p, "3x") ? 3 : 1}
)
""",
),
)
@classmethod
async def execute(cls, price):
return IO.NodeOutput()
class NodeWithDynamicCombo(IO.ComfyNode):
@classmethod
def define_schema(cls):
return IO.Schema(
node_id="DevToolsNodeWithDynamicCombo",
display_name="Node With Dynamic Combo",
description="A node with a Dynamic combo",
inputs=[IO.DynamicCombo.Input("combo", options=[
IO.DynamicCombo.Option("option1", [IO.Combo.Input("suboption", options=["1x"])]),
IO.DynamicCombo.Option("option2", [IO.Combo.Input("suboption", options=["2x"])]),
IO.DynamicCombo.Option("option3", [IO.Image.Input("image")]),
IO.DynamicCombo.Option("option4", [
IO.DynamicCombo.Input("subcombo", options=[
IO.DynamicCombo.Option("opt1", [IO.Float.Input("float_x"), IO.Float.Input("float_y")]),
IO.DynamicCombo.Option("opt2", [IO.Mask.Input("mask1", optional=True)]),
])
])]
)],
)
@classmethod
async def execute(cls):
return IO.NodeOutput()
NODE_CLASS_MAPPINGS = {
"DevToolsLongComboDropdown": LongComboDropdown,
"DevToolsNodeWithOptionalInput": NodeWithOptionalInput,
"DevToolsNodeWithOptionalComboInput": NodeWithOptionalComboInput,
"DevToolsNodeWithOnlyOptionalInput": NodeWithOnlyOptionalInput,
"DevToolsNodeWithOutputList": NodeWithOutputList,
"DevToolsNodeWithForceInput": NodeWithForceInput,
"DevToolsNodeWithDefaultInput": NodeWithDefaultInput,
"DevToolsNodeWithStringInput": NodeWithStringInput,
"DevToolsNodeWithUnionInput": NodeWithUnionInput,
"DevToolsNodeWithBooleanInput": NodeWithBooleanInput,
"DevToolsSimpleSlider": SimpleSlider,
"DevToolsNodeWithSeedInput": NodeWithSeedInput,
"DevToolsNodeWithValidation": NodeWithValidation,
"DevToolsNodeWithV2ComboInput": NodeWithV2ComboInput,
"DevToolsNodeWithLegacyWidget": NodeWithLegacyWidget,
"DevToolsNodeWithPriceBadge": NodeWithPriceBadge,
"DevToolsNodeWithDynamicCombo": NodeWithDynamicCombo,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"DevToolsLongComboDropdown": "Long Combo Dropdown",
"DevToolsNodeWithOptionalInput": "Node With Optional Input",
"DevToolsNodeWithOptionalComboInput": "Node With Optional Combo Input",
"DevToolsNodeWithOnlyOptionalInput": "Node With Only Optional Input",
"DevToolsNodeWithOutputList": "Node With Output List",
"DevToolsNodeWithForceInput": "Node With Force Input",
"DevToolsNodeWithDefaultInput": "Node With Default Input",
"DevToolsNodeWithStringInput": "Node With String Input",
"DevToolsNodeWithUnionInput": "Node With Union Input",
"DevToolsNodeWithBooleanInput": "Node With Boolean Input",
"DevToolsSimpleSlider": "Simple Slider",
"DevToolsNodeWithSeedInput": "Node With Seed Input",
"DevToolsNodeWithValidation": "Node With Validation",
"DevToolsNodeWithV2ComboInput": "Node With V2 Combo Input",
"DevToolsNodeWithLegacyWidget": "Node With Legacy Widget",
"DevToolsNodeWithPriceBadge": "Node With Price Badge",
"DevToolsNodeWithDynamicCombo": "Node With Dynamic Combo",
}
__all__ = [
"LongComboDropdown",
"NodeWithOptionalInput",
"NodeWithOptionalComboInput",
"NodeWithOnlyOptionalInput",
"NodeWithOutputList",
"NodeWithForceInput",
"NodeWithDefaultInput",
"NodeWithStringInput",
"NodeWithUnionInput",
"NodeWithBooleanInput",
"SimpleSlider",
"NodeWithSeedInput",
"NodeWithValidation",
"NodeWithV2ComboInput",
"NODE_CLASS_MAPPINGS",
"NODE_DISPLAY_NAME_MAPPINGS",
]