Python: add Python API for layout panels

This adds a Python API for layout panels that have been introduced in #113584.
Two new methods on `UILayout` are added:
* `.panel(idname, text="...", default_closed=False) -> Optional[UILayout]`
* `.panel_prop(owner, prop_name, text="...") -> Optional[UILayout]`

Both create a panel and return `None` if the panel is collapsed. The difference lies
in how the open-close-state is stored. The first method internally manages the
open-close-state based on the provided identifier. The second one allows for
providing a boolean property that stores whether the panel is open. This is useful
when creating a dynamic of panels and when it is difficult to create a unique idname.

For the `.panel(...)` method, a new internal map on `Panel` is created which keeps
track of all the panel states based on the idname. Currently, there is no mechanism
for freeing any elements once they have been added to the map. This is unlikely to
cause a problem anytime soon, but we might need some kind of garbage collection
in the future.

```python
import bpy
from bpy.props import BoolProperty

class LayoutDemoPanel(bpy.types.Panel):
    bl_label = "Layout Panel Demo"
    bl_idname = "SCENE_PT_layout_panel"
    bl_space_type = 'PROPERTIES'
    bl_region_type = 'WINDOW'
    bl_context = "scene"

    def draw(self, context):
        layout = self.layout
        scene = context.scene

        layout.label(text="Before")

        if panel := layout.panel("my_panel_id", text="Hello World", default_closed=False):
            panel.label(text="Success")

        if panel := layout.panel_prop(scene, "show_demo_panel", text="My Panel"):
            panel.prop(scene, "frame_start")
            panel.prop(scene, "frame_end")

        layout.label(text="After")

bpy.utils.register_class(LayoutDemoPanel)
bpy.types.Scene.show_demo_panel = BoolProperty(default=False)
```

Pull Request: https://projects.blender.org/blender/blender/pulls/116949
This commit is contained in:
Jacques Lucke
2024-01-11 19:08:45 +01:00
parent d499710218
commit 8896446f7e
8 changed files with 161 additions and 0 deletions

View File

@@ -27,6 +27,7 @@ struct BlendWriter;
struct Header;
struct ID;
struct IDRemapper;
struct LayoutPanelState;
struct LibraryForeachIDData;
struct ListBase;
struct Menu;
@@ -606,6 +607,14 @@ void BKE_screen_area_free(ScrArea *area);
void BKE_region_callback_free_gizmomap_set(void (*callback)(wmGizmoMap *));
void BKE_region_callback_refresh_tag_gizmomap_set(void (*callback)(wmGizmoMap *));
/**
* Get the layout panel state for the given idname. If it does not exist yet, initialize a new
* panel state with the given default value.
*/
LayoutPanelState *BKE_panel_layout_panel_state_ensure(Panel *panel,
const char *idname,
bool default_closed);
/**
* Find a region of type \a region_type in provided \a regionbase.
*

View File

@@ -323,6 +323,14 @@ static void panel_list_copy(ListBase *newlb, const ListBase *lb)
new_panel->runtime = new_runtime;
new_panel->activedata = nullptr;
new_panel->drawname = nullptr;
BLI_listbase_clear(&new_panel->layout_panel_states);
LISTBASE_FOREACH (LayoutPanelState *, src_state, &old_panel->layout_panel_states) {
LayoutPanelState *new_state = MEM_new<LayoutPanelState>(__func__, *src_state);
new_state->idname = BLI_strdup(src_state->idname);
BLI_addtail(&new_panel->layout_panel_states, new_state);
}
BLI_addtail(newlb, new_panel);
panel_list_copy(&new_panel->children, &old_panel->children);
}
@@ -487,6 +495,22 @@ void BKE_region_callback_free_gizmomap_set(void (*callback)(wmGizmoMap *))
region_free_gizmomap_callback = callback;
}
LayoutPanelState *BKE_panel_layout_panel_state_ensure(Panel *panel,
const char *idname,
const bool default_closed)
{
LISTBASE_FOREACH (LayoutPanelState *, state, &panel->layout_panel_states) {
if (STREQ(state->idname, idname)) {
return state;
}
}
LayoutPanelState *state = MEM_cnew<LayoutPanelState>(__func__);
state->idname = BLI_strdup(idname);
SET_FLAG_FROM_TEST(state->flag, !default_closed, LAYOUT_PANEL_STATE_FLAG_OPEN);
BLI_addtail(&panel->layout_panel_states, state);
return state;
}
Panel *BKE_panel_new(PanelType *panel_type)
{
Panel *panel = MEM_cnew<Panel>(__func__);
@@ -502,6 +526,12 @@ void BKE_panel_free(Panel *panel)
{
MEM_SAFE_FREE(panel->activedata);
MEM_SAFE_FREE(panel->drawname);
LISTBASE_FOREACH (LayoutPanelState *, state, &panel->layout_panel_states) {
MEM_freeN(state->idname);
}
BLI_freelistN(&panel->layout_panel_states);
MEM_delete(panel->runtime);
MEM_freeN(panel);
}
@@ -1054,6 +1084,10 @@ static void write_panel_list(BlendWriter *writer, ListBase *lb)
{
LISTBASE_FOREACH (Panel *, panel, lb) {
BLO_write_struct(writer, Panel, panel);
BLO_write_struct_list(writer, LayoutPanelState, &panel->layout_panel_states);
LISTBASE_FOREACH (LayoutPanelState *, state, &panel->layout_panel_states) {
BLO_write_string(writer, state->idname);
}
write_panel_list(writer, &panel->children);
}
}
@@ -1116,6 +1150,10 @@ static void direct_link_panel_list(BlendDataReader *reader, ListBase *lb)
panel->activedata = nullptr;
panel->type = nullptr;
panel->drawname = nullptr;
BLO_read_list(reader, &panel->layout_panel_states);
LISTBASE_FOREACH (LayoutPanelState *, state, &panel->layout_panel_states) {
BLO_read_data_address(reader, &state->idname);
}
direct_link_panel_list(reader, &panel->children);
}
}

View File

@@ -2280,6 +2280,7 @@ float uiLayoutGetUnitsY(uiLayout *layout);
eUIEmbossType uiLayoutGetEmboss(uiLayout *layout);
bool uiLayoutGetPropSep(uiLayout *layout);
bool uiLayoutGetPropDecorate(uiLayout *layout);
Panel *uiLayoutGetRootPanel(uiLayout *layout);
/* Layout create functions. */

View File

@@ -5307,6 +5307,11 @@ void uiLayoutSetPropDecorate(uiLayout *layout, bool is_sep)
SET_FLAG_FROM_TEST(layout->item.flag, is_sep, UI_ITEM_PROP_DECORATE);
}
Panel *uiLayoutGetRootPanel(uiLayout *layout)
{
return layout->root->block->panel;
}
bool uiLayoutGetActive(uiLayout *layout)
{
return layout->active;

View File

@@ -2005,6 +2005,7 @@ static void ui_panel_drag_collapse(const bContext *C,
const_cast<bContext *>(C),
&header.open_owner_ptr,
RNA_struct_find_property(&header.open_owner_ptr, header.open_prop_name.c_str()));
ED_region_tag_redraw(region);
}
}
@@ -2109,6 +2110,7 @@ static void ui_handle_layout_panel_header(
const_cast<bContext *>(C),
&header->open_owner_ptr,
RNA_struct_find_property(&header->open_owner_ptr, header->open_prop_name.c_str()));
ED_region_tag_redraw(CTX_wm_region(C));
if (event_type == LEFTMOUSE) {
ui_panel_drag_collapse_handler_add(C, is_open);

View File

@@ -129,6 +129,19 @@ typedef struct ScrAreaMap {
ListBase areabase;
} ScrAreaMap;
typedef struct LayoutPanelState {
struct LayoutPanelState *next, *prev;
/** Identifier of the panel. */
char *idname;
uint8_t flag;
char _pad[7];
} LayoutPanelState;
enum LayoutPanelStateFlag {
/** If set, the panel is currently open. Otherwise it is collapsed. */
LAYOUT_PANEL_STATE_FLAG_OPEN = (1 << 0),
};
/** The part from uiBlock that needs saved in file. */
typedef struct Panel {
struct Panel *next, *prev;
@@ -158,6 +171,12 @@ typedef struct Panel {
/** Sub panels. */
ListBase children;
/**
* List of #LayoutPanelState. This stores the open-close-state of layout-panels created with
* `layout.panel(...)` in Python. For more information on layout-panels, see `uiLayoutPanel`.
*/
ListBase layout_panel_states;
struct Panel_Runtime *runtime;
} Panel;

View File

@@ -2383,6 +2383,18 @@ static void rna_def_file_handler(BlenderRNA *brna)
RNA_def_parameter_flags(parm, PropertyFlag(0), PARM_REQUIRED);
}
static void rna_def_layout_panel_state(BlenderRNA *brna)
{
StructRNA *srna;
PropertyRNA *prop;
srna = RNA_def_struct(brna, "LayoutPanelState", nullptr);
prop = RNA_def_property(srna, "is_open", PROP_BOOLEAN, PROP_NONE);
RNA_def_property_boolean_sdna(prop, nullptr, "flag", LAYOUT_PANEL_STATE_FLAG_OPEN);
RNA_def_property_ui_text(prop, "Is Open", "");
}
void RNA_def_ui(BlenderRNA *brna)
{
rna_def_ui_layout(brna);
@@ -2392,6 +2404,7 @@ void RNA_def_ui(BlenderRNA *brna)
rna_def_menu(brna);
rna_def_asset_shelf(brna);
rna_def_file_handler(brna);
rna_def_layout_panel_state(brna);
}
#endif /* RNA_RUNTIME */

View File

@@ -780,6 +780,33 @@ static uiLayout *rna_uiLayoutColumnWithHeading(
return uiLayoutColumnWithHeading(layout, align, heading);
}
struct uiLayout *rna_uiLayoutPanelProp(uiLayout *layout,
bContext *C,
PointerRNA *data,
const char *property,
const char *text,
const char *text_ctxt,
const bool translate)
{
text = rna_translate_ui_text(text, text_ctxt, nullptr, nullptr, translate);
return uiLayoutPanel(C, layout, text, data, property);
}
struct uiLayout *rna_uiLayoutPanel(uiLayout *layout,
bContext *C,
const char *idname,
const char *text,
const char *text_ctxt,
const bool translate,
const bool default_closed)
{
text = RNA_translate_ui_text(text, text_ctxt, nullptr, nullptr, translate);
Panel *panel = uiLayoutGetRootPanel(layout);
LayoutPanelState *state = BKE_panel_layout_panel_state_ensure(panel, idname, default_closed);
PointerRNA state_ptr = RNA_pointer_create(nullptr, &RNA_LayoutPanelState, state);
return uiLayoutPanel(C, layout, text, &state_ptr, "is_open");
}
static void rna_uiLayout_template_node_asset_menu_items(uiLayout *layout,
bContext *C,
const char *catalog_path)
@@ -1048,6 +1075,53 @@ void RNA_api_ui_layout(StructRNA *srna)
RNA_def_boolean(func, "align", false, "", "Align buttons to each other");
api_ui_item_common_heading(func);
func = RNA_def_function(srna, "panel", "rna_uiLayoutPanel");
RNA_def_function_ui_description(func,
"Creates a collapsable panel. Whether it is open or closed is "
"stored in the region using the given idname");
RNA_def_function_flag(func, FUNC_USE_CONTEXT);
parm = RNA_def_string(func, "idname", nullptr, 0, "", "Identifier of the panel");
RNA_def_parameter_flags(parm, PROP_NEVER_NULL, PARM_REQUIRED);
api_ui_item_common_text(func);
RNA_def_boolean(func,
"default_closed",
false,
"Open by Default",
"When true, the panel will be open the first time it is shown");
parm = RNA_def_pointer(func,
"layout",
"UILayout",
"",
"Sub-layout to put items in. Will be none is the panel is collapsed");
RNA_def_function_return(func, parm);
func = RNA_def_function(srna, "panel_prop", "rna_uiLayoutPanelProp");
RNA_def_function_ui_description(
func,
"Similar to `.panel(...)` but instead of storing whether it is open or closed in the "
"region, it is stored in the provided boolean property. This should be used when multiple "
"instances of the same panel can exist. For example one for every item in a collection "
"property or list");
RNA_def_function_flag(func, FUNC_USE_CONTEXT);
parm = RNA_def_pointer(
func, "data", "AnyType", "", "Data from which to take the open-state property");
RNA_def_parameter_flags(parm, PROP_NEVER_NULL, PARM_REQUIRED | PARM_RNAPTR);
parm = RNA_def_string(
func,
"property",
nullptr,
0,
"",
"Identifier of the boolean property that determines whether the panel is open or closed");
RNA_def_parameter_flags(parm, PropertyFlag(0), PARM_REQUIRED);
api_ui_item_common_text(func);
parm = RNA_def_pointer(func,
"layout",
"UILayout",
"",
"Sub-layout to put items in. Will be none is the panel is collapsed");
RNA_def_function_return(func, parm);
func = RNA_def_function(srna, "column_flow", "uiLayoutColumnFlow");
RNA_def_int(func, "columns", 0, 0, INT_MAX, "", "Number of columns, 0 is automatic", 0, INT_MAX);
parm = RNA_def_pointer(func, "layout", "UILayout", "", "Sub-layout to put items in");