Property Search: Set panel expansion when tab changes

This commit makes the panel expansion set based on the search results
when the active tab in the properties editor changes. The multi-tab
search patch (D8859) actually doesn't handle this because it uses a
different code path.

This feature uncovered a subtle but fairly significant issue with the
implementation of property search (More details in T81113). Basically,
the search needed multiple redraws to properly display the expansion of
panels based on the search results. Because there is no animation of
panel expansion when switching tabs, the problem was exposed only now.

With this commit, hiding of "search only" buttons and panel size
calculation happens in a single final step of the panel layout pass.
The "search only" layout root flag is removed. Instead every button
inside a panel header is in a single "uiButtonGroup" marked with a
specific "in header" flag, an idea which could be generalized in the
future.

Differential Revision: https://developer.blender.org/D9006
This commit is contained in:
Hans Goudey 2020-10-03 11:25:13 -05:00
parent 724370b2f9
commit cb6234fccf
7 changed files with 191 additions and 108 deletions

View File

@ -1683,14 +1683,15 @@ struct Panel *UI_panel_begin(struct ARegion *region,
struct PanelType *pt,
struct Panel *panel,
bool *r_open);
void UI_panel_end(const struct ARegion *region, uiBlock *block, int width, int height, bool open);
void UI_panel_header_buttons_begin(struct Panel *panel);
void UI_panel_header_buttons_end(struct Panel *panel);
void UI_panel_end(struct Panel *panel, int width, int height);
bool UI_panel_is_active(const struct Panel *panel);
void UI_panel_label_offset(struct uiBlock *block, int *r_x, int *r_y);
int UI_panel_size_y(const struct Panel *panel);
bool UI_panel_is_dragging(const struct Panel *panel);
bool UI_panel_matches_search_filter(const struct Panel *panel);
void UI_panels_set_expansion_from_seach_filter(const struct bContext *C, struct ARegion *region);
bool UI_panel_category_is_visible(const struct ARegion *region);
void UI_panel_category_add(struct ARegion *region, const char *name);
@ -1922,7 +1923,6 @@ float uiLayoutGetUnitsY(uiLayout *layout);
int uiLayoutGetEmboss(uiLayout *layout);
bool uiLayoutGetPropSep(uiLayout *layout);
bool uiLayoutGetPropDecorate(uiLayout *layout);
void uiLayoutRootSetSearchOnly(uiLayout *layout, bool search_only);
/* layout specifiers */
uiLayout *uiLayoutRow(uiLayout *layout, bool align);

View File

@ -32,17 +32,26 @@
* Every function that adds a set of buttons must create another group,
* then #ui_def_but adds buttons to the current group (the last).
*/
void ui_block_new_button_group(uiBlock *block)
void ui_block_new_button_group(uiBlock *block, short flag)
{
/* Don't create a new group if there is a "lock" on new groups. */
if (!BLI_listbase_is_empty(&block->button_groups)) {
uiButtonGroup *last_button_group = block->button_groups.last;
if (last_button_group->flag & UI_BUTTON_GROUP_LOCK) {
return;
}
}
uiButtonGroup *new_group = MEM_mallocN(sizeof(uiButtonGroup), __func__);
BLI_listbase_clear(&new_group->buttons);
new_group->flag = flag;
BLI_addtail(&block->button_groups, new_group);
}
void ui_button_group_add_but(uiBlock *block, uiBut *but)
{
if (BLI_listbase_is_empty(&block->button_groups)) {
ui_block_new_button_group(block);
ui_block_new_button_group(block, 0);
}
uiButtonGroup *current_button_group = block->button_groups.last;

View File

@ -426,8 +426,17 @@ enum eBlockContentHints {
typedef struct uiButtonGroup {
void *next, *prev;
ListBase buttons; /* #LinkData with #uiBut data field. */
short flag;
} uiButtonGroup;
/* #uiButtonGroup.flag. */
typedef enum uiButtonGroupFlag {
/** While this flag is set, don't create new button groups for layout item calls. */
UI_BUTTON_GROUP_LOCK = (1 << 0),
/** The buttons in this group are inside a panel header. */
UI_BUTTON_GROUP_PANEL_HEADER = (1 << 1),
} uiButtonGroupFlag;
struct uiBlock {
uiBlock *next, *prev;
@ -1023,7 +1032,7 @@ void ui_item_menutype_func(struct bContext *C, struct uiLayout *layout, void *ar
void ui_item_paneltype_func(struct bContext *C, struct uiLayout *layout, void *arg_pt);
/* interface_button_group.c */
void ui_block_new_button_group(uiBlock *block);
void ui_block_new_button_group(uiBlock *block, short flag);
void ui_button_group_add_but(uiBlock *block, uiBut *but);
void ui_button_group_replace_but_ptr(uiBlock *block, const void *old_but_ptr, uiBut *new_but);
void ui_block_free_button_groups(uiBlock *block);

View File

@ -84,13 +84,6 @@ typedef struct uiLayoutRoot {
int type;
int opcontext;
/**
* If true, the root will be removed as part of the property search process.
* Necessary for cases like searching the contents of closed panels, where the
* block-level tag isn't enough, as there might be visible buttons in the header.
*/
bool search_only;
int emw, emh;
int padding;
@ -1985,7 +1978,7 @@ void uiItemFullR(uiLayout *layout,
#endif /* UI_PROP_DECORATE */
UI_block_layout_set_current(block, layout);
ui_block_new_button_group(block);
ui_block_new_button_group(block, 0);
/* retrieve info */
const PropertyType type = RNA_property_type(prop);
@ -2723,7 +2716,7 @@ void uiItemPointerR_prop(uiLayout *layout,
{
const bool use_prop_sep = ((layout->item.flag & UI_ITEM_PROP_SEP) != 0);
ui_block_new_button_group(uiLayoutGetBlock(layout));
ui_block_new_button_group(uiLayoutGetBlock(layout), 0);
const PropertyType type = RNA_property_type(prop);
if (!ELEM(type, PROP_POINTER, PROP_STRING, PROP_ENUM)) {
@ -2829,7 +2822,7 @@ static uiBut *ui_item_menu(uiLayout *layout,
uiLayout *heading_layout = ui_layout_heading_find(layout);
UI_block_layout_set_current(block, layout);
ui_block_new_button_group(block);
ui_block_new_button_group(block, 0);
if (!name) {
name = "";
@ -3095,7 +3088,7 @@ static uiBut *uiItemL_(uiLayout *layout, const char *name, int icon)
uiBlock *block = layout->root->block;
UI_block_layout_set_current(block, layout);
ui_block_new_button_group(block);
ui_block_new_button_group(block, 0);
if (!name) {
name = "";
@ -5015,16 +5008,6 @@ int uiLayoutGetEmboss(uiLayout *layout)
return layout->emboss;
}
/**
* Tags the layout root as search only, meaning the search process will run, but not the rest of
* the layout process. Use in situations where part of the block's contents normally wouldn't be
* drawn, but must be searched anyway, like the contents of closed panels with headers.
*/
void uiLayoutRootSetSearchOnly(uiLayout *layout, bool search_only)
{
layout->root->search_only = search_only;
}
/** \} */
/* -------------------------------------------------------------------- */
@ -5044,42 +5027,6 @@ static bool block_search_panel_label_matches(const uiBlock *block, const char *s
return false;
}
/**
* Buttons for search only layouts (closed panel sub-panels) have still been added from the
* layout functions, but they need to be hidden. Theoretically they could be removed too.
*/
static void layout_free_and_hide_buttons(uiLayout *layout)
{
LISTBASE_FOREACH_MUTABLE (uiItem *, item, &layout->items) {
if (item->type == ITEM_BUTTON) {
uiButtonItem *button_item = (uiButtonItem *)item;
BLI_assert(button_item->but != NULL);
button_item->but->flag |= UI_HIDDEN;
MEM_freeN(item);
}
else {
layout_free_and_hide_buttons((uiLayout *)item);
}
}
MEM_freeN(layout);
}
/**
* Remove layouts used only for search and hide their buttons.
* (See comment for #uiLayoutRootSetSearchOnly and in #uiLayoutRoot).
*/
static void block_search_remove_search_only_roots(uiBlock *block)
{
LISTBASE_FOREACH_MUTABLE (uiLayoutRoot *, root, &block->layouts) {
if (root->search_only) {
layout_free_and_hide_buttons(root->layout);
BLI_remlink(&block->layouts, root);
MEM_freeN(root);
}
}
}
/**
* Returns true if a button or the data / operator it represents matches the search filter.
*/
@ -5198,8 +5145,6 @@ bool UI_block_apply_search_filter(uiBlock *block, const char *search_filter)
true :
block_search_filter_tag_buttons(block, search_filter);
block_search_remove_search_only_roots(block);
if (block->panel != NULL) {
if (has_result) {
ui_panel_tag_search_filter_match(block->panel);
@ -5642,9 +5587,6 @@ void UI_block_layout_resolve(uiBlock *block, int *r_x, int *r_y)
block->curlayout = NULL;
LISTBASE_FOREACH_MUTABLE (uiLayoutRoot *, root, &block->layouts) {
/* Search only roots should be removed by #UI_block_apply_search_filter. */
BLI_assert(!root->search_only);
ui_layout_add_padding_button(root);
/* NULL in advance so we don't interfere when adding button */

View File

@ -193,10 +193,11 @@ static bool panel_active_animation_changed(ListBase *lb,
return false;
}
static bool panels_need_realign(ScrArea *area, ARegion *region, Panel **r_panel_animation)
/**
* \return True if the properties editor switch tabs since the last layout pass.
*/
static bool properties_space_needs_realign(ScrArea *area, ARegion *region)
{
*r_panel_animation = NULL;
if (area->spacetype == SPACE_PROPERTIES && region->regiontype == RGN_TYPE_WINDOW) {
SpaceProperties *sbuts = area->spacedata.first;
@ -205,6 +206,17 @@ static bool panels_need_realign(ScrArea *area, ARegion *region, Panel **r_panel_
}
}
return false;
}
static bool panels_need_realign(ScrArea *area, ARegion *region, Panel **r_panel_animation)
{
*r_panel_animation = NULL;
if (properties_space_needs_realign(area, region)) {
return true;
}
/* Detect if a panel was added or removed. */
Panel *panel_animation = NULL;
bool no_animation = false;
@ -678,6 +690,8 @@ Panel *UI_panel_begin(
panel->type = pt;
}
panel->runtime.block = block;
/* Do not allow closed panels without headers! Else user could get "disappeared" UI! */
if ((pt->flag & PNL_NO_HEADER) && (panel->flag & PNL_CLOSED)) {
panel->flag &= ~PNL_CLOSED;
@ -732,6 +746,41 @@ Panel *UI_panel_begin(
return panel;
}
/**
* Create the panel header button group, used to mark which buttons are part of
* panel headers for later panel search handling. Should be called before adding
* buttons for the panel's header layout.
*/
void UI_panel_header_buttons_begin(Panel *panel)
{
uiBlock *block = panel->runtime.block;
ui_block_new_button_group(block, UI_BUTTON_GROUP_LOCK | UI_BUTTON_GROUP_PANEL_HEADER);
}
/**
* Allow new button groups to be created after the header group.
*/
void UI_panel_header_buttons_end(Panel *panel)
{
uiBlock *block = panel->runtime.block;
/* There should always be the button group created in #UI_panel_header_buttons_begin. */
BLI_assert(!BLI_listbase_is_empty(&block->button_groups));
uiButtonGroup *button_group = block->button_groups.last;
/* Repurpose the first "header" button group if it is empty, in case the first button added to
* the panel doesn't add a new group (if the button is created directly rather than through an
* interface layout call). */
if (BLI_listbase_count(&block->button_groups) == 1 &&
BLI_listbase_is_empty(&button_group->buttons)) {
button_group->flag &= ~UI_BUTTON_GROUP_PANEL_HEADER;
}
button_group->flag &= ~UI_BUTTON_GROUP_LOCK;
}
static float panel_region_offset_x_get(const ARegion *region)
{
if (UI_panel_category_is_visible(region)) {
@ -740,22 +789,23 @@ static float panel_region_offset_x_get(const ARegion *region)
}
}
return 0;
return 0.0f;
}
void UI_panel_end(const ARegion *region, uiBlock *block, int width, int height, bool open)
/**
* Starting from the "block size" set in #UI_panel_end, calculate the full size
* of the panel including the subpanel headers and buttons.
*/
static void panel_calculate_size_recursive(ARegion *region, Panel *panel)
{
Panel *panel = block->panel;
int width = panel->blocksizex;
int height = panel->blocksizey;
/* Set panel size excluding children. */
panel->blocksizex = width;
panel->blocksizey = height;
/* Compute total panel size including children. */
LISTBASE_FOREACH (Panel *, pachild, &panel->children) {
if (pachild->runtime_flag & PANEL_ACTIVE) {
width = max_ii(width, pachild->sizex);
height += get_panel_real_size_y(pachild);
LISTBASE_FOREACH (Panel *, child_panel, &panel->children) {
if (child_panel->runtime_flag & PANEL_ACTIVE) {
panel_calculate_size_recursive(region, child_panel);
width = max_ii(width, child_panel->sizex);
height += get_panel_real_size_y(child_panel);
}
}
@ -773,7 +823,7 @@ void UI_panel_end(const ARegion *region, uiBlock *block, int width, int height,
if (width != 0) {
panel->sizex = width;
}
if (height != 0 || open) {
if (height != 0 || !(panel->flag & PNL_CLOSED)) {
panel->sizey = height;
}
@ -790,6 +840,14 @@ void UI_panel_end(const ARegion *region, uiBlock *block, int width, int height,
}
}
void UI_panel_end(Panel *panel, int width, int height)
{
/* Store the size of the buttons layout in the panel. The actual panel size
* (including subpanels) is calculated in #UI_panels_end. */
panel->blocksizex = width;
panel->blocksizey = height;
}
static void ui_offset_panel_block(uiBlock *block)
{
const uiStyle *style = UI_style_get_dpi();
@ -840,12 +898,14 @@ bool UI_panel_matches_search_filter(const Panel *panel)
/**
* Expands a panel if it was tagged as having a result by property search, otherwise collapses it.
*/
static void panel_set_expansion_from_seach_filter_recursive(const bContext *C, Panel *panel)
static void panel_set_expansion_from_seach_filter_recursive(const bContext *C,
Panel *panel,
const bool use_animation)
{
if (!(panel->type->flag & PNL_NO_HEADER)) {
short start_flag = panel->flag;
SET_FLAG_FROM_TEST(panel->flag, !UI_panel_matches_search_filter(panel), PNL_CLOSED);
if (start_flag != panel->flag) {
if (use_animation && start_flag != panel->flag) {
panel_activate_state(C, panel, PANEL_STATE_ANIMATION);
}
}
@ -853,7 +913,7 @@ static void panel_set_expansion_from_seach_filter_recursive(const bContext *C, P
/* If the panel is filtered (removed) we need to check that its children are too. */
LISTBASE_FOREACH (Panel *, child_panel, &panel->children) {
if (child_panel->runtime_flag & PANEL_ACTIVE) {
panel_set_expansion_from_seach_filter_recursive(C, child_panel);
panel_set_expansion_from_seach_filter_recursive(C, child_panel, use_animation);
}
}
}
@ -862,11 +922,63 @@ static void panel_set_expansion_from_seach_filter_recursive(const bContext *C, P
* Uses the panel's search filter flag to set its expansion, activating animation if it was closed
* or opened. Note that this can't be set too often, or manual interaction becomes impossible.
*/
void UI_panels_set_expansion_from_seach_filter(const bContext *C, ARegion *region)
static void region_panels_set_expansion_from_seach_filter(const bContext *C,
ARegion *region,
const bool use_animation)
{
LISTBASE_FOREACH (Panel *, panel, &region->panels) {
if (panel->runtime_flag & PANEL_ACTIVE) {
panel_set_expansion_from_seach_filter_recursive(C, panel);
panel_set_expansion_from_seach_filter_recursive(C, panel, use_animation);
}
}
set_panels_list_data_expand_flag(C, region);
}
/**
* Hide buttons in invisible layouts, which are created because in order to search,
* buttons must be added for all panels, even panels that will end up closed.
*/
static void panel_remove_invisible_layouts_recursive(Panel *panel, const Panel *parent_panel)
{
uiBlock *block = panel->runtime.block;
BLI_assert(block != NULL);
BLI_assert(block->active);
if (parent_panel != NULL && parent_panel->flag & PNL_CLOSED) {
/* The parent panel is closed, so this panel can be completely removed. */
UI_block_set_search_only(block, true);
LISTBASE_FOREACH (uiBut *, but, &block->buttons) {
but->flag |= UI_HIDDEN;
}
}
else if (panel->flag & PNL_CLOSED) {
/* If subpanels have no search results but the parent panel does, then the parent panel open
* and the subpanels will close. In that case there must be a way to hide the buttons in the
* panel but keep the header buttons. */
LISTBASE_FOREACH (uiButtonGroup *, button_group, &block->button_groups) {
if (button_group->flag & UI_BUTTON_GROUP_PANEL_HEADER) {
continue;
}
LISTBASE_FOREACH (LinkData *, link, &button_group->buttons) {
uiBut *but = link->data;
but->flag |= UI_HIDDEN;
}
}
}
LISTBASE_FOREACH (Panel *, child_panel, &panel->children) {
if (child_panel->runtime_flag & PANEL_ACTIVE) {
BLI_assert(child_panel->runtime.block != NULL);
panel_remove_invisible_layouts_recursive(child_panel, panel);
}
}
}
static void region_panels_remove_invisible_layouts(ARegion *region)
{
LISTBASE_FOREACH (Panel *, panel, &region->panels) {
if (panel->runtime_flag & PANEL_ACTIVE) {
BLI_assert(panel->runtime.block != NULL);
panel_remove_invisible_layouts_recursive(panel, NULL);
}
}
}
@ -1933,12 +2045,24 @@ void UI_panels_end(const bContext *C, ARegion *region, int *r_x, int *r_y)
region_panels_set_expansion_from_list_data(C, region);
/* Update panel expansion based on property search results. */
if (region->flag & RGN_FLAG_SEARCH_FILTER_UPDATE) {
/* Don't use the last update from the deactivation, or all the panels will be left closed. */
if (region->flag & RGN_FLAG_SEARCH_FILTER_ACTIVE) {
UI_panels_set_expansion_from_seach_filter(C, region);
set_panels_list_data_expand_flag(C, region);
if (region->flag & RGN_FLAG_SEARCH_FILTER_ACTIVE) {
/* Update panel expansion based on property search results. Keep this inside the check
* for an active search filter, or all panels will be left closed when a search ends. */
if (region->flag & RGN_FLAG_SEARCH_FILTER_UPDATE) {
region_panels_set_expansion_from_seach_filter(C, region, true);
}
else if (properties_space_needs_realign(area, region)) {
region_panels_set_expansion_from_seach_filter(C, region, false);
}
/* Clean up the extra panels and buttons created for searching. */
region_panels_remove_invisible_layouts(region);
}
LISTBASE_FOREACH (Panel *, panel, &region->panels) {
if (panel->runtime_flag & PANEL_ACTIVE) {
BLI_assert(panel->runtime.block != NULL);
panel_calculate_size_recursive(region, panel);
}
}

View File

@ -2610,8 +2610,7 @@ static void ed_panel_draw(const bContext *C,
int w,
int em,
char *unique_panel_str,
const char *search_filter,
bool search_only)
const char *search_filter)
{
const uiStyle *style = UI_style_get_dpi();
@ -2624,7 +2623,6 @@ static void ed_panel_draw(const bContext *C,
strncat(block_name, unique_panel_str, INSTANCED_PANEL_UNIQUE_STR_LEN);
}
uiBlock *block = UI_block_begin(C, region, block_name, UI_EMBOSS);
UI_block_set_search_only(block, search_only);
bool open;
panel = UI_panel_begin(region, lb, block, pt, panel, &open);
@ -2635,6 +2633,7 @@ static void ed_panel_draw(const bContext *C,
int xco, yco, h = 0;
int headerend = w - UI_UNIT_X;
UI_panel_header_buttons_begin(panel);
if (pt->draw_header_preset && !(pt->flag & PNL_NO_HEADER)) {
/* for preset menu */
panel->layout = UI_block_layout(block,
@ -2646,7 +2645,6 @@ static void ed_panel_draw(const bContext *C,
1,
0,
style);
uiLayoutRootSetSearchOnly(panel->layout, search_only);
pt->draw_header_preset(C, panel);
@ -2678,7 +2676,6 @@ static void ed_panel_draw(const bContext *C,
panel->layout = UI_block_layout(
block, UI_LAYOUT_HORIZONTAL, UI_LAYOUT_HEADER, labelx, labely, UI_UNIT_Y, 1, 0, style);
}
uiLayoutRootSetSearchOnly(panel->layout, search_only);
pt->draw_header(C, panel);
@ -2690,6 +2687,7 @@ static void ed_panel_draw(const bContext *C,
else {
panel->labelofs = 0;
}
UI_panel_header_buttons_end(panel);
if (open || search_filter_active) {
short panelContext;
@ -2715,7 +2713,6 @@ static void ed_panel_draw(const bContext *C,
em,
0,
style);
uiLayoutRootSetSearchOnly(panel->layout, search_only || !open);
pt->draw(C, panel);
@ -2745,13 +2742,12 @@ static void ed_panel_draw(const bContext *C,
w,
em,
unique_panel_str,
search_filter,
!open);
search_filter);
}
}
}
UI_panel_end(region, block, w, h, open);
UI_panel_end(panel, w, h);
}
/**
@ -2921,8 +2917,7 @@ void ED_region_panels_layout_ex(const bContext *C,
(pt->flag & PNL_DRAW_BOX) ? w_box_panel : w,
em,
NULL,
search_filter,
false);
search_filter);
}
/* Draw "polyinstantaited" panels that don't have a 1 to 1 correspondence with their types. */
@ -2956,8 +2951,7 @@ void ED_region_panels_layout_ex(const bContext *C,
(panel->type->flag & PNL_DRAW_BOX) ? w_box_panel : w,
em,
unique_panel_str,
search_filter,
false);
search_filter);
}
}

View File

@ -38,6 +38,7 @@ struct Scene;
struct SpaceLink;
struct SpaceType;
struct uiLayout;
struct uiBlock;
struct wmDrawBuffer;
struct wmTimer;
struct wmTooltipState;
@ -143,6 +144,10 @@ typedef struct Panel_Runtime {
* This avoids freeing the same pointer twice when panels are removed.
*/
struct PointerRNA *custom_data_ptr;
/* Pointer to the panel's block. Useful when changes to panel #uiBlocks
* need some context from traversal of the panel "tree". */
struct uiBlock *block;
} Panel_Runtime;
/** The part from uiBlock that needs saved in file. */