UI: Support persistent view state, write tree-view height to files

Adds support for saving some view state persistently and uses this to keep the
height of a tree-view, even as the region containing it is hidden, or the file
re-loaded.

Fixes #129058.

Basically the design is to have state stored in the region, so it can be saved
to files. Views types (tree-view, grid-view, etc) can decide themselves if they
have state to be preserved, and what state that is. If a view wants to preserve
state, it's stored in a list inside the region, identified by the view's idname.

Limitation is that multiple instances of the same view would share these bits of
state, in practice I don't think that's ever an issue.

More state can be added to be preserved as needed. Since different kinds of
views may require different state, I was thinking we could add ID properties to
`uiViewState` even, making it much more dynamic.

Pull Request: https://projects.blender.org/blender/blender/pulls/130292
This commit is contained in:
Julian Eisel
2024-11-18 18:19:48 +01:00
committed by Julian Eisel
parent 9f0d20c056
commit f0db870822
28 changed files with 188 additions and 40 deletions

View File

@@ -369,6 +369,8 @@ ARegion *BKE_area_region_copy(const SpaceType *st, const ARegion *region)
BLI_listbase_clear(&newar->ui_previews);
BLI_duplicatelist(&newar->ui_previews, &region->ui_previews);
BLI_listbase_clear(&newar->view_states);
BLI_duplicatelist(&newar->view_states, &region->view_states);
return newar;
}
@@ -610,6 +612,7 @@ void BKE_area_region_free(SpaceType *st, ARegion *region)
BLI_freelistN(&region->ui_previews);
BLI_freelistN(&region->panels_category);
BLI_freelistN(&region->panels_category_active);
BLI_freelistN(&region->view_states);
}
void BKE_screen_area_free(ScrArea *area)
@@ -1143,6 +1146,10 @@ static void write_area(BlendWriter *writer, ScrArea *area)
LISTBASE_FOREACH (uiPreview *, ui_preview, &region->ui_previews) {
BLO_write_struct(writer, uiPreview, ui_preview);
}
LISTBASE_FOREACH (uiViewStateLink *, view_state, &region->view_states) {
BLO_write_struct(writer, uiViewStateLink, view_state);
}
}
LISTBASE_FOREACH (SpaceLink *, sl, &area->spacedata) {
@@ -1199,6 +1206,7 @@ static void direct_link_region(BlendDataReader *reader, ARegion *region, int spa
BLO_read_struct_list(reader, PanelCategoryStack, &region->panels_category_active);
BLO_read_struct_list(reader, uiList, &region->ui_lists);
BLO_read_struct_list(reader, uiViewStateLink, &region->view_states);
/* The area's search filter is runtime only, so we need to clear the active flag on read. */
/* Clear runtime flags (e.g. search filter is runtime only). */

View File

@@ -527,8 +527,7 @@ void region_layout(const bContext *C, ARegion *region)
0,
style);
build_asset_view(
*layout, active_shelf->settings.asset_library_reference, *active_shelf, *C, *region);
build_asset_view(*layout, active_shelf->settings.asset_library_reference, *active_shelf, *C);
int layout_height;
UI_block_layout_resolve(block, nullptr, &layout_height);

View File

@@ -31,8 +31,7 @@ namespace blender::ed::asset::shelf {
void build_asset_view(uiLayout &layout,
const AssetLibraryReference &library_ref,
const AssetShelf &shelf,
const bContext &C,
const ARegion &region);
const bContext &C);
void catalog_selector_panel_register(ARegionType *region_type);
void popover_panel_register(ARegionType *region_type);

View File

@@ -345,8 +345,7 @@ static std::string filter_string_get(const AssetShelf &shelf)
void build_asset_view(uiLayout &layout,
const AssetLibraryReference &library_ref,
const AssetShelf &shelf,
const bContext &C,
const ARegion &region)
const bContext &C)
{
list::storage_fetch(&library_ref, &C);
list::previews_fetch(&library_ref, &C);
@@ -371,7 +370,7 @@ void build_asset_view(uiLayout &layout,
grid_view->set_context_menu_title("Asset Shelf");
ui::GridViewBuilder builder(*block);
builder.build_grid_view(C, *grid_view, region.v2d, layout, filter_string_get(shelf));
builder.build_grid_view(C, *grid_view, layout, filter_string_get(shelf));
}
/* ---------------------------------------------------------------------- */

View File

@@ -215,7 +215,7 @@ static void catalog_selector_panel_draw(const bContext *C, Panel *panel)
"asset catalog tree view",
std::make_unique<AssetCatalogSelectorTree>(*library, *shelf));
tree_view->set_context_menu_title("Catalog");
ui::TreeViewBuilder::build_tree_view(*tree_view, *layout);
ui::TreeViewBuilder::build_tree_view(*C, *tree_view, *layout);
}
void catalog_selector_panel_register(ARegionType *region_type)

View File

@@ -155,7 +155,7 @@ class AssetCatalogTreeView : public ui::AbstractTreeView {
}
};
static void catalog_tree_draw(uiLayout &layout, AssetShelf &shelf)
static void catalog_tree_draw(const bContext &C, uiLayout &layout, AssetShelf &shelf)
{
const asset_system::AssetLibrary *library = list::library_get_once_available(
shelf.settings.asset_library_reference);
@@ -169,7 +169,7 @@ static void catalog_tree_draw(uiLayout &layout, AssetShelf &shelf)
"asset shelf catalog tree view",
std::make_unique<AssetCatalogTreeView>(*library, shelf));
ui::TreeViewBuilder::build_tree_view(*tree_view, layout);
ui::TreeViewBuilder::build_tree_view(C, *tree_view, layout);
}
static AssetShelfType *lookup_type_from_idname_in_context(const bContext *C)
@@ -201,8 +201,6 @@ static void popover_panel_draw(const bContext *C, Panel *panel)
AssetShelfType *shelf_type = lookup_type_from_idname_in_context(C);
BLI_assert_msg(shelf_type != nullptr, "couldn't find asset shelf type from context");
const ARegion *region = CTX_wm_region_popup(C) ? CTX_wm_region_popup(C) : CTX_wm_region(C);
uiLayout *layout = panel->layout;
uiLayoutSetUnitsX(layout, layout_width_units);
@@ -222,7 +220,7 @@ static void popover_panel_draw(const bContext *C, Panel *panel)
uiLayoutSetUnitsX(catalogs_col, LEFT_COL_WIDTH_UNITS);
uiLayoutSetFixedSize(catalogs_col, true);
library_selector_draw(C, catalogs_col, *shelf);
catalog_tree_draw(*catalogs_col, *shelf);
catalog_tree_draw(*C, *catalogs_col, *shelf);
uiLayout *right_col = uiLayoutColumn(row, false);
uiLayout *sub = uiLayoutRow(right_col, false);
@@ -241,7 +239,7 @@ static void popover_panel_draw(const bContext *C, Panel *panel)
uiLayoutSetUnitsX(asset_view_col, layout_width_units - LEFT_COL_WIDTH_UNITS);
uiLayoutSetFixedSize(asset_view_col, true);
build_asset_view(*asset_view_col, shelf->settings.asset_library_reference, *shelf, *C, *region);
build_asset_view(*asset_view_col, shelf->settings.asset_library_reference, *shelf, *C);
}
static bool popover_panel_poll(const bContext *C, PanelType * /*panel_type*/)

View File

@@ -104,6 +104,20 @@ class AbstractView {
virtual bool supports_scrolling() const;
virtual void scroll(ViewScrollDirection direction);
/**
* From the current view state, return certain state that will be written to files (stored in
* #ARegion.view_states) to preserve it over UI changes and file loading. The state can be
* restored using #persistent_state_apply().
*
* Return an empty value if there's no state to preserve (default implementation).
*/
virtual std::optional<uiViewState> persistent_state() const;
/**
* Restore a view state given in \a state, which was created by #persistent_state() for saving in
* files, and potentially loaded from a file.
*/
virtual void persistent_state_apply(const uiViewState &state);
/**
* Makes \a item valid for display in this view. Behavior is undefined for items not registered
* with this.

View File

@@ -172,7 +172,6 @@ class GridViewBuilder {
void build_grid_view(const bContext &C,
AbstractGridView &grid_view,
const View2D &v2d,
uiLayout &layout,
std::optional<StringRef> search_string = {});
};

View File

@@ -273,6 +273,8 @@ void UI_list_filter_and_sort_items(uiList *ui_list,
/**
* Override this for all available view types.
* \param idname: Used for restoring persistent state of this view, potentially written to files.
* Must not be longer than #BKE_ST_MAXNAME (including 0 terminator).
*/
blender::ui::AbstractGridView *UI_block_add_view(
uiBlock &block,

View File

@@ -2857,6 +2857,7 @@ void template_asset_shelf_popover(uiLayout &layout,
}
void uiTemplateLightLinkingCollection(uiLayout *layout,
bContext *C,
uiLayout *context_layout,
PointerRNA *ptr,
const char *propname);
@@ -2864,7 +2865,7 @@ void uiTemplateLightLinkingCollection(uiLayout *layout,
void uiTemplateBoneCollectionTree(uiLayout *layout, bContext *C);
void uiTemplateGreasePencilLayerTree(uiLayout *layout, bContext *C);
void uiTemplateNodeTreeInterface(uiLayout *layout, PointerRNA *ptr);
void uiTemplateNodeTreeInterface(uiLayout *layout, bContext *C, PointerRNA *ptr);
/**
* Draw all node buttons and socket default values with the same panel structure used by the node.
*/

View File

@@ -146,6 +146,9 @@ class AbstractTreeView : public AbstractView, public TreeViewItemContainer {
protected:
virtual void build_tree() = 0;
std::optional<uiViewState> persistent_state() const override;
void persistent_state_apply(const uiViewState &state) override;
private:
void foreach_view_item(FunctionRef<void(AbstractViewItem &)> iter_fn) const final;
void update_children_from_old(const AbstractView &old_view) override;
@@ -404,7 +407,8 @@ class TreeViewItemDropTarget : public DropTargetInterface {
class TreeViewBuilder {
public:
static void build_tree_view(AbstractTreeView &tree_view,
static void build_tree_view(const bContext &C,
AbstractTreeView &tree_view,
uiLayout &layout,
std::optional<StringRef> search_string = {},
bool add_box = true);

View File

@@ -2013,7 +2013,7 @@ void UI_block_end_ex(const bContext *C,
/* Update bounds of all views in this block. If this block is a panel, this will be done later in
* #UI_panels_end(), because buttons are offset there. */
if (!block->panel) {
ui_block_views_bounds_calc(block);
ui_block_views_end(region, block);
}
if (block->rect.xmin == 0.0f && block->rect.xmax == 0.0f) {

View File

@@ -1609,7 +1609,10 @@ void ui_interface_tag_script_reload_queries();
/* interface_view.cc */
void ui_block_free_views(uiBlock *block);
void ui_block_views_bounds_calc(const uiBlock *block);
void ui_block_views_end(ARegion *region, const uiBlock *block);
void ui_block_view_persistent_state_restore(const ARegion &region,
const uiBlock &block,
blender::ui::AbstractView &view);
void ui_block_views_listen(const uiBlock *block, const wmRegionListenerParams *listener_params);
void ui_block_views_draw_overlays(const ARegion *region, const uiBlock *block);
blender::ui::AbstractView *ui_block_view_find_matching_in_old_block(

View File

@@ -1944,7 +1944,7 @@ void UI_panels_end(const bContext *C, ARegion *region, int *r_x, int *r_y)
/* Update bounds for all "views" in this block. Usually this is done in #UI_block_end(), but
* that wouldn't work because of the offset applied above. */
ui_block_views_bounds_calc(block);
ui_block_views_end(region, block);
}
}

View File

@@ -476,5 +476,5 @@ void uiTemplateBoneCollectionTree(uiLayout *layout, bContext *C)
tree_view->set_context_menu_title("Bone Collection");
tree_view->set_default_rows(3);
ui::TreeViewBuilder::build_tree_view(*tree_view, *layout);
ui::TreeViewBuilder::build_tree_view(*C, *tree_view, *layout);
}

View File

@@ -513,5 +513,5 @@ void uiTemplateGreasePencilLayerTree(uiLayout *layout, bContext *C)
tree_view->set_context_menu_title("Grease Pencil Layer");
tree_view->set_default_rows(6);
ui::TreeViewBuilder::build_tree_view(*tree_view, *layout);
ui::TreeViewBuilder::build_tree_view(*C, *tree_view, *layout);
}

View File

@@ -355,10 +355,8 @@ class CollectionView : public AbstractTreeView {
} // namespace blender::ui::light_linking
void uiTemplateLightLinkingCollection(uiLayout *layout,
uiLayout *context_layout,
PointerRNA *ptr,
const char *propname)
void uiTemplateLightLinkingCollection(
uiLayout *layout, bContext *C, uiLayout *context_layout, PointerRNA *ptr, const char *propname)
{
if (!ptr->data) {
return;
@@ -402,5 +400,5 @@ void uiTemplateLightLinkingCollection(uiLayout *layout,
tree_view->set_context_menu_title("Light Linking");
tree_view->set_default_rows(3);
blender::ui::TreeViewBuilder::build_tree_view(*tree_view, *layout);
blender::ui::TreeViewBuilder::build_tree_view(*C, *tree_view, *layout);
}

View File

@@ -501,7 +501,7 @@ wmDragNodeTreeInterface *NodePanelDropTarget::get_drag_node_tree_declaration(
} // namespace blender::ui::nodes
void uiTemplateNodeTreeInterface(uiLayout *layout, PointerRNA *ptr)
void uiTemplateNodeTreeInterface(uiLayout *layout, bContext *C, PointerRNA *ptr)
{
if (!ptr->data) {
return;
@@ -521,5 +521,5 @@ void uiTemplateNodeTreeInterface(uiLayout *layout, PointerRNA *ptr)
tree_view->set_context_menu_title("Node Tree Interface");
tree_view->set_default_rows(3);
blender::ui::TreeViewBuilder::build_tree_view(*tree_view, *layout);
blender::ui::TreeViewBuilder::build_tree_view(*C, *tree_view, *layout);
}

View File

@@ -136,6 +136,13 @@ void AbstractView::scroll(ViewScrollDirection /*direction*/)
BLI_assert_msg(false, "Unsupported for this view type");
}
std::optional<uiViewState> AbstractView::persistent_state() const
{
return {};
}
void AbstractView::persistent_state_apply(const uiViewState & /*state*/) {}
/** \} */
/* ---------------------------------------------------------------------- */

View File

@@ -12,6 +12,7 @@
#include <optional>
#include <stdexcept>
#include "BKE_context.hh"
#include "BKE_icons.h"
#include "BLI_index_range.hh"
@@ -242,7 +243,7 @@ BuildOnlyVisibleButtonsHelper::BuildOnlyVisibleButtonsHelper(
const AbstractGridViewItem *force_visible_item)
: grid_view_(grid_view), style_(grid_view.get_style()), cols_per_row_(cols_per_row)
{
if ((v2d.flag & V2D_IS_INIT) && grid_view.get_item_count_filtered()) {
if (v2d.flag & V2D_IS_INIT && grid_view.get_item_count_filtered()) {
visible_items_range_ = this->get_visible_range(v2d, force_visible_item);
}
}
@@ -452,12 +453,14 @@ GridViewBuilder::GridViewBuilder(uiBlock & /*block*/) {}
void GridViewBuilder::build_grid_view(const bContext &C,
AbstractGridView &grid_view,
const View2D &v2d,
uiLayout &layout,
std::optional<StringRef> search_string)
{
uiBlock &block = *uiLayoutGetBlock(&layout);
const ARegion *region = CTX_wm_region_popup(&C) ? CTX_wm_region_popup(&C) : CTX_wm_region(&C);
ui_block_view_persistent_state_restore(*region, block, grid_view);
grid_view.build_items();
grid_view.update_from_old(block);
grid_view.change_state_delayed();
@@ -467,7 +470,7 @@ void GridViewBuilder::build_grid_view(const bContext &C,
UI_block_layout_set_current(&block, &layout);
GridViewLayoutBuilder builder(layout);
builder.build_from_view(C, grid_view, v2d);
builder.build_from_view(C, grid_view, region->v2d);
}
/* ---------------------------------------------------------------------- */

View File

@@ -27,6 +27,7 @@
#include "BLI_listbase.h"
#include "BLI_map.hh"
#include "BLI_string.h"
#include "ED_screen.hh"
@@ -56,6 +57,8 @@ static T *ui_block_add_view_impl(uiBlock &block,
StringRef idname,
std::unique_ptr<AbstractView> view)
{
BLI_assert(idname.size() < int64_t(sizeof(uiViewStateLink::idname)));
ViewLink *view_link = MEM_new<ViewLink>(__func__);
BLI_addtail(&block.views, view_link);
@@ -126,9 +129,58 @@ void ViewLink::views_bounds_calc(const uiBlock &block)
}
}
void ui_block_views_bounds_calc(const uiBlock *block)
void ui_block_view_persistent_state_restore(const ARegion &region,
const uiBlock &block,
ui::AbstractView &view)
{
StringRef idname = [&]() -> StringRef {
LISTBASE_FOREACH (ViewLink *, link, &block.views) {
if (link->view.get() == &view) {
return link->idname;
}
}
return "";
}();
if (idname.is_empty()) {
BLI_assert_unreachable();
return;
}
LISTBASE_FOREACH (uiViewStateLink *, stored_state, &region.view_states) {
if (stored_state->idname == idname) {
view.persistent_state_apply(stored_state->state);
}
}
}
static uiViewStateLink *ensure_view_state(ARegion &region, const ViewLink &link)
{
LISTBASE_FOREACH (uiViewStateLink *, stored_state, &region.view_states) {
if (link.idname == stored_state->idname) {
return stored_state;
}
}
uiViewStateLink *new_state = MEM_cnew<uiViewStateLink>(__func__);
link.idname.copy(new_state->idname, sizeof(new_state->idname));
BLI_addhead(&region.view_states, new_state);
return new_state;
}
void ui_block_views_end(ARegion *region, const uiBlock *block)
{
ViewLink::views_bounds_calc(*block);
if (region && region->regiontype != RGN_TYPE_TEMPORARY) {
LISTBASE_FOREACH (const ViewLink *, link, &block->views) {
/* Ensure persistent view state storage for writing to files if needed. */
if (std::optional<uiViewState> temp_state = link->view->persistent_state()) {
uiViewStateLink *state_link = ensure_view_state(*region, *link);
state_link->state = *temp_state;
}
}
}
}
void ui_block_views_listen(const uiBlock *block, const wmRegionListenerParams *listener_params)

View File

@@ -131,6 +131,28 @@ void AbstractTreeView::set_default_rows(int default_rows)
custom_height_ = std::make_unique<int>(default_rows * padded_item_height());
}
std::optional<uiViewState> AbstractTreeView::persistent_state() const
{
if (!custom_height_) {
return {};
}
uiViewState state{0};
if (custom_height_) {
state.custom_height = *custom_height_ * UI_INV_SCALE_FAC;
}
return state;
}
void AbstractTreeView::persistent_state_apply(const uiViewState &state)
{
if (state.custom_height) {
set_default_rows(round_fl_to_int(state.custom_height * UI_SCALE_FAC) / padded_item_height());
}
}
int AbstractTreeView::count_visible_descendants(const AbstractTreeViewItem &parent) const
{
if (parent.is_collapsed()) {
@@ -915,13 +937,19 @@ void TreeViewBuilder::ensure_min_rows_items(AbstractTreeView &tree_view)
}
}
void TreeViewBuilder::build_tree_view(AbstractTreeView &tree_view,
void TreeViewBuilder::build_tree_view(const bContext &C,
AbstractTreeView &tree_view,
uiLayout &layout,
std::optional<StringRef> search_string,
const bool add_box)
{
uiBlock &block = *uiLayoutGetBlock(&layout);
const ARegion *region = CTX_wm_region_popup(&C) ? CTX_wm_region_popup(&C) : CTX_wm_region(&C);
if (region) {
ui_block_view_persistent_state_restore(*region, block, tree_view);
}
tree_view.build_tree();
tree_view.update_from_old(block);
tree_view.change_state_delayed();

View File

@@ -754,7 +754,8 @@ bool file_is_asset_visible_in_catalog_filter_settings(
/* ---------------------------------------------------------------------- */
void file_create_asset_catalog_tree_view_in_layout(asset_system::AssetLibrary *asset_library,
void file_create_asset_catalog_tree_view_in_layout(const bContext *C,
asset_system::AssetLibrary *asset_library,
uiLayout *layout,
SpaceFile *space_file,
FileAssetSelectParams *params)
@@ -769,7 +770,7 @@ void file_create_asset_catalog_tree_view_in_layout(asset_system::AssetLibrary *a
std::make_unique<ed::asset_browser::AssetCatalogTreeView>(
asset_library, params, *space_file));
tree_view->set_context_menu_title("Catalog");
ui::TreeViewBuilder::build_tree_view(*tree_view, *layout);
ui::TreeViewBuilder::build_tree_view(*C, *tree_view, *layout);
}
} // namespace blender::ed::asset_browser

View File

@@ -232,7 +232,8 @@ void file_path_to_ui_path(const char *path, char *r_path, int r_path_maxncpy);
namespace blender::ed::asset_browser {
void file_create_asset_catalog_tree_view_in_layout(asset_system::AssetLibrary *asset_library,
void file_create_asset_catalog_tree_view_in_layout(const bContext *C,
asset_system::AssetLibrary *asset_library,
uiLayout *layout,
SpaceFile *space_file,
FileAssetSelectParams *params);

View File

@@ -255,7 +255,7 @@ static void file_panel_asset_catalog_buttons_draw(const bContext *C, Panel *pane
uiItemS(col);
blender::ed::asset_browser::file_create_asset_catalog_tree_view_in_layout(
asset_library, col, sfile, params);
C, asset_library, col, sfile, params);
}
void file_tools_region_panels_register(ARegionType *art)

View File

@@ -728,7 +728,7 @@ void spreadsheet_data_set_panel_draw(const bContext *C, Panel *panel)
"Instances Tree View",
std::make_unique<GeometryInstancesTreeView>(root_geometry, *C));
tree_view->set_context_menu_title("Instance");
ui::TreeViewBuilder::build_tree_view(*tree_view, *panel, {}, false);
ui::TreeViewBuilder::build_tree_view(*C, *tree_view, *panel, {}, false);
}
if (uiLayout *panel = uiLayoutPanel(
C, layout, "geometry_domain_tree_view", false, IFACE_("Domain")))
@@ -740,7 +740,7 @@ void spreadsheet_data_set_panel_draw(const bContext *C, Panel *panel)
"Data Set Tree View",
std::make_unique<GeometryDataSetTreeView>(std::move(instance_geometry), *C));
tree_view->set_context_menu_title("Domain");
ui::TreeViewBuilder::build_tree_view(*tree_view, *panel, {}, false);
ui::TreeViewBuilder::build_tree_view(*C, *tree_view, *panel, {}, false);
}
}

View File

@@ -322,6 +322,31 @@ typedef struct uiList { /* some list UI data need to be saved in file */
uiListDyn *dyn_data;
} uiList;
/** See #uiViewStateLink. */
typedef struct uiViewState {
/**
* User set height of the view in unscaled pixels. A value of 0 means no custom height was set
* and the default should be used.
*/
int custom_height;
char _pad[4];
} uiViewState;
/**
* Persistent storage for some state of views (#ui::AbstractView), for storage in a region. The
* view state is matched to the view using the view's idname.
*
* The actual state is stored in #uiViewState, so views can manage this conveniently without having
* to care about the idname and listbase pointers themselves.
*/
typedef struct uiViewStateLink {
struct uiViewStateLink *next, *prev;
char idname[64]; /* #BKE_ST_MAXNAME */
uiViewState state;
} uiViewStateLink;
typedef struct TransformOrientation {
struct TransformOrientation *next, *prev;
/** MAX_NAME. */
@@ -512,6 +537,11 @@ typedef struct ARegion {
ListBase handlers;
/** Panel categories runtime. */
ListBase panels_category;
/**
* Permanent state storage of #ui::AbstractView instances, so hiding regions with views or
* loading files remembers the view state.
*/
ListBase view_states; /* #uiViewStateLink */
/** Gizmo-map of this region. */
struct wmGizmoMap *gizmo_map;

View File

@@ -2387,6 +2387,7 @@ void RNA_api_ui_layout(StructRNA *srna)
srna, "template_light_linking_collection", "uiTemplateLightLinkingCollection");
RNA_def_function_ui_description(func,
"Visualization of a content of a light linking collection");
RNA_def_function_flag(func, FUNC_USE_CONTEXT);
parm = RNA_def_pointer(func,
"context_layout",
"UILayout",
@@ -2406,6 +2407,7 @@ void RNA_api_ui_layout(StructRNA *srna)
func = RNA_def_function(srna, "template_node_tree_interface", "uiTemplateNodeTreeInterface");
RNA_def_function_ui_description(func, "Show a node tree interface");
RNA_def_function_flag(func, FUNC_USE_CONTEXT);
parm = RNA_def_pointer(func,
"interface",
"NodeTreeInterface",