Preserve node panel open/closed state in Blender 5.2 - #225
Conversation
Vollkornaffe
left a comment
There was a problem hiding this comment.
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.
|
|
||
| # 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), | ||
| ), | ||
| ) | ||
|
|
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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)
| # 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), | ||
| ) | ||
|
|
There was a problem hiding this comment.
Similar to the export, it seems better in the specific_handlers file.
| _node_panel_state_type: Any = getattr(bpy.types, "NodePanelState", None) | ||
| if _node_panel_state_type is not None: |
There was a problem hiding this comment.
We usually check the Blender version instead.
| 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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
The wording I've choosen of the comment on the SpecificImporter/SpecificExporter is too vague.
The helper classes should always be used if possible.
| 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 |
There was a problem hiding this comment.
It seems easier to use a built-in node that has a collapsible panel, like Mesh Bevel.
|
thanks for your time and having a look at this. Let's go with #226 then, I'll close this pr. |

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