Skip to content

Preserve node panel open/closed state in Blender 5.2 - #225

Closed
kolibril13 wants to merge 1 commit into
mainfrom
panel_options
Closed

Preserve node panel open/closed state in Blender 5.2#225
kolibril13 wants to merge 1 commit into
mainfrom
panel_options

Conversation

@kolibril13

Copy link
Copy Markdown
Collaborator

fixes #46 and #177

code was generated with help of codex GPT5.6 Sol.

Here's a demo.

Screen.Recording.2026-08-16.at.17.12.30.mp4

and here's the online asset:
https://tree-clipper.com/jan-hendrik/testing-of-the-new-panel-hide-option

context:https://projects.blender.org/blender/blender/pulls/157179

@kolibril13

Copy link
Copy Markdown
Collaborator Author

here's the JSON diff:
image

@Vollkornaffe
Vollkornaffe self-requested a review August 16, 2026 15:41

@Vollkornaffe Vollkornaffe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for this PR!

Overall, I would rather go with #226.

A few things differ too much from the ideas in the existing code or seem unnecessary, and since the panel states are another collection on general nodes like the inputs and outputs (sockets), we should handle them in the same way.

Comment on lines +486 to +505

# Blender 5.2 exposes the open/closed state of interface panels as
# a read-only collection on every node. It has to be requested
# explicitly because a node's specific handler owns all inherited
# Node properties. Avoid writing an empty collection for the many
# nodes that do not have panels.
if isinstance(obj, bpy.types.Node) and hasattr(obj, PANEL_STATES):
panel_states = getattr(obj, PANEL_STATES)
if len(panel_states) > 0:
prop = obj.bl_rna.properties[PANEL_STATES]
no_clobber(
data,
prop.identifier,
exporter._export_property_collection(
obj=obj,
prop=cast(bpy.types.CollectionProperty, prop),
from_root=from_root.add_prop(prop),
),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not written anywhere explicitly, but the idea for export_nodes.py and import_nodes.py is to be as general as possible.

With that in mind, this code seems out of place and should be handled in the specific_handlers file instead.

It would also be more consistent to keep the comments shorter and link to the GitHub issue.

# Blender 5.2 exposes the open/closed state of interface panels as
# a read-only collection on every node. It has to be requested
# explicitly because a node's specific handler owns all inherited
# Node properties. Avoid writing an empty collection for the many

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An empty collection is fine.
We generally export "too much", but that puts us on the safe side for extending the import later.

(In fact, it would have been nice if we were already exporting the panel_states)

Comment on lines +415 to +426
# Panel declarations can depend on node-specific properties and
# sockets, so restore their UI state only after the specific node
# handler has finished rebuilding the node.
if isinstance(getter(), bpy.types.Node) and PANEL_STATES in serialization:
prop = getter().bl_rna.properties[PANEL_STATES]
self._import_property_collection(
getter=getter,
prop=cast(bpy.types.CollectionProperty, prop),
serialization=serialization[PANEL_STATES],
from_root=from_root.add_prop(prop),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the export, it seems better in the specific_handlers file.

Comment on lines +422 to +423
_node_panel_state_type: Any = getattr(bpy.types, "NodePanelState", None)
if _node_panel_state_type is not None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We usually check the Blender version instead.

Comment on lines +425 to +455
def _export_node_panel_state(
exporter: Exporter,
obj: bpy.types.bpy_struct,
from_root,
):
# `identifier` is intentionally omitted. It matches the interface
# panel's read-only persistent UID, which may change when an interface
# containing gaps in its UIDs is recreated on import. The collection
# order follows the interface panel order.
return exporter.export_all_simple_writable_properties(
obj=obj,
assumed_type=_node_panel_state_type,
from_root=from_root,
)

def _import_node_panel_state(
importer: Importer,
getter: GETTER,
serialization: dict[str, Any],
from_root,
):
importer.import_all_simple_writable_properties(
getter=getter,
serialization=serialization,
assumed_type=_node_panel_state_type,
forbidden=[],
from_root=from_root,
)

no_clobber(_BUILT_IN_EXPORTER, _node_panel_state_type, _export_node_panel_state)
no_clobber(_BUILT_IN_IMPORTER, _node_panel_state_type, _import_node_panel_state)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this is unnecessary, as the default property handling should suffice.

EDIT: the identifier is unexpectetly exported, this is a separate bug in the default handling.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The wording I've choosen of the comment on the SpecificImporter/SpecificExporter is too vague.

The helper classes should always be used if possible.

Comment on lines +16 to +40
inner = make_test_node_tree(name="panel state inner")

# Leave a gap in the persistent UIDs. Those identifiers are
# read-only and are therefore not guaranteed to survive import.
removed_panel = inner.interface.new_panel(name="removed")
inner.interface.remove(removed_panel)

open_panel = inner.interface.new_panel(name="open")
closed_panel = inner.interface.new_panel(name="closed")
inner.interface.new_socket(
name="Open Value",
in_out="INPUT",
socket_type="NodeSocketFloat",
parent=open_panel,
)
inner.interface.new_socket(
name="Closed Value",
in_out="INPUT",
socket_type="NodeSocketFloat",
parent=closed_panel,
)

outer = make_test_node_tree(name="panel state outer")
group_node = outer.nodes.new(type="GeometryNodeGroup")
group_node.node_tree = inner

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems easier to use a built-in node that has a collapsible panel, like Mesh Bevel.

@Vollkornaffe Vollkornaffe mentioned this pull request Aug 17, 2026
@kolibril13

Copy link
Copy Markdown
Collaborator Author

thanks for your time and having a look at this. Let's go with #226 then, I'll close this pr.

@kolibril13 kolibril13 closed this Aug 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Interface Panels aren't Closed on Import

2 participants