Property Search: Find results in all tabs

This patch enables property search for all tabs in the property editor.
To make interaction faster, if the editor's current tab doesn't have a
result, the current tab changes to the next tab that has a match.

This patch implements basic code that only searches panels.
While we could run the existing "single tab" property search for every
tab, that would also do everything else related to the layout pass,
which would be less efficient, and maybe more complicated to maintain.

The search match status for every current tab of the property editor is
stored in a runtime bitfield and them displayed later by dimming icons
in the tab selector panel to the left. Using `BLI_bitmap` properly in
the runtime struct required moving it to `buttons_intern.h` and
adding a small API to access the search filter instead.

To make sure the editor isn't influenced by anything that happens while
building the layout for other tabs, most of the context is duplicated
and the new search is run in the duplicated editor.

Note that the tool settings tab works slightly different than the other
tabs, so I've disabled searching it for this commit. That would be a
relatively simple improvement, but would just require a bit of
refactoring of existing code.

Differential Revision: https://developer.blender.org/D8859
This commit is contained in:
Hans Goudey 2020-10-13 13:10:41 -05:00
parent 96dd299055
commit 7c633686e9
Notes: blender-bot 2023-02-14 03:46:57 +01:00
Referenced by issue #77824, Properties Search Implementation
10 changed files with 423 additions and 14 deletions

View File

@ -53,7 +53,11 @@ class PROPERTIES_PT_navigation_bar(Panel):
layout.scale_x = 1.4
layout.scale_y = 1.4
layout.prop_tabs_enum(view, "context", icon_only=True)
if view.search_filter:
layout.prop_tabs_enum(view, "context", data_highlight=view,
property_highlight="tab_search_results", icon_only=True)
else:
layout.prop_tabs_enum(view, "context", icon_only=True)
classes = (

View File

@ -29,6 +29,11 @@ extern "C" {
struct SpaceProperties;
int ED_buttons_tabs_list(struct SpaceProperties *sbuts, short *context_tabs_array);
bool ED_buttons_tab_has_search_result(struct SpaceProperties *sbuts, const int index);
void ED_buttons_search_string_set(struct SpaceProperties *sbuts, const char *value);
int ED_buttons_search_string_length(struct SpaceProperties *sbuts);
const char *ED_buttons_search_string_get(struct SpaceProperties *sbuts);
#ifdef __cplusplus
}

View File

@ -92,6 +92,11 @@ void ED_region_panels_layout_ex(const struct bContext *C,
struct ListBase *paneltypes,
const char *contexts[],
const char *category_override);
bool ED_region_property_search(const struct bContext *C,
struct ARegion *region,
struct ListBase *paneltypes,
const char *contexts[],
const char *category_override);
void ED_region_panels_layout(const struct bContext *C, struct ARegion *region);
void ED_region_panels_draw(const struct bContext *C, struct ARegion *region);

View File

@ -1874,6 +1874,7 @@ uiLayout *UI_block_layout(uiBlock *block,
const struct uiStyle *style);
void UI_block_layout_set_current(uiBlock *block, uiLayout *layout);
void UI_block_layout_resolve(uiBlock *block, int *r_x, int *r_y);
void UI_block_layout_free(uiBlock *block);
bool UI_block_apply_search_filter(uiBlock *block, const char *search_filter);

View File

@ -5600,6 +5600,19 @@ void uiLayoutSetFunc(uiLayout *layout, uiMenuHandleFunc handlefunc, void *argv)
layout->root->argv = argv;
}
/**
* Used for property search when the layout process needs to be cancelled in order to avoid
* computing the locations for buttons, but the layout items created while adding the buttons
* must still be freed.
*/
void UI_block_layout_free(uiBlock *block)
{
LISTBASE_FOREACH_MUTABLE (uiLayoutRoot *, root, &block->layouts) {
ui_layout_free(root->layout);
MEM_freeN(root);
}
}
void UI_block_layout_resolve(uiBlock *block, int *r_x, int *r_y)
{
BLI_assert(block->active);

View File

@ -48,6 +48,7 @@
#include "WM_toolsystem.h"
#include "WM_types.h"
#include "ED_buttons.h"
#include "ED_screen.h"
#include "ED_screen_types.h"
#include "ED_space_api.h"
@ -765,7 +766,7 @@ const char *ED_area_region_search_filter_get(const ScrArea *area, const ARegion
if (area->spacetype == SPACE_PROPERTIES) {
SpaceProperties *sbuts = area->spacedata.first;
if (region->regiontype == RGN_TYPE_WINDOW) {
return sbuts->runtime->search_string;
return ED_buttons_search_string_get(sbuts);
}
}
@ -3074,6 +3075,149 @@ void ED_region_panels_init(wmWindowManager *wm, ARegion *region)
WM_event_add_keymap_handler(&region->handlers, keymap);
}
/**
* Check whether any of the buttons generated by the \a panel_type's
* layout callbacks match the \a search_filter.
*
* \param panel: If non-NULL, use this instead of adding a new panel for the \a panel_type.
*/
static bool panel_property_search(const bContext *C,
ARegion *region,
const uiStyle *style,
Panel *panel,
PanelType *panel_type,
const char *search_filter)
{
uiBlock *block = UI_block_begin(C, region, panel_type->idname, UI_EMBOSS);
UI_block_set_search_only(block, true);
if (panel == NULL) {
bool open; /* Dummy variable. */
panel = UI_panel_begin(region, &region->panels, block, panel_type, panel, &open);
}
/* Build the layouts. Because they are only used for search,
* they don't need any of the proper style or layout information. */
if (panel->type->draw_header_preset != NULL) {
panel->layout = UI_block_layout(
block, UI_LAYOUT_HORIZONTAL, UI_LAYOUT_HEADER, 0, 0, 0, 0, 0, style);
panel_type->draw_header_preset(C, panel);
}
if (panel->type->draw_header != NULL) {
panel->layout = UI_block_layout(
block, UI_LAYOUT_HORIZONTAL, UI_LAYOUT_HEADER, 0, 0, 0, 0, 0, style);
panel_type->draw_header(C, panel);
}
if (LIKELY(panel->type->draw != NULL)) {
panel->layout = UI_block_layout(
block, UI_LAYOUT_VERTICAL, UI_LAYOUT_PANEL, 0, 0, 0, 0, 0, style);
panel_type->draw(C, panel);
}
UI_block_layout_free(block);
/* We could check after each layout to increase the likelyhood of returning early,
* but that probably wouldn't make much of a difference anyway. */
if (UI_block_apply_search_filter(block, search_filter)) {
return true;
}
LISTBASE_FOREACH (LinkData *, link, &panel_type->children) {
PanelType *panel_type_child = link->data;
if (!panel_type_child->poll || panel_type_child->poll(C, panel_type_child)) {
/* Search for the existing child panel here because it might be an instanced
* child panel with a custom data field that will be needed to build the layout. */
Panel *child_panel = UI_panel_find_by_type(&panel->children, panel_type_child);
if (panel_property_search(C, region, style, child_panel, panel_type_child, search_filter)) {
return true;
}
}
}
return false;
}
/**
* Build the same panel list as #ED_region_panels_layout_ex and checks whether any
* of the panels contain a search result based on the area / region's search filter.
*/
bool ED_region_property_search(const bContext *C,
ARegion *region,
ListBase *paneltypes,
const char *contexts[],
const char *category_override)
{
ScrArea *area = CTX_wm_area(C);
WorkSpace *workspace = CTX_wm_workspace(C);
const uiStyle *style = UI_style_get_dpi();
const char *search_filter = ED_area_region_search_filter_get(area, region);
LinkNode *panel_types_stack = NULL;
LISTBASE_FOREACH_BACKWARD (PanelType *, pt, paneltypes) {
if (panel_add_check(C, workspace, contexts, category_override, pt)) {
BLI_linklist_prepend_alloca(&panel_types_stack, pt);
}
}
const char *category = NULL;
bool use_category_tabs = (category_override == NULL) && region_uses_category_tabs(area, region);
if (use_category_tabs) {
category = region_panels_collect_categories(region, panel_types_stack, &use_category_tabs);
}
/* Run property search for each panel, stopping if a result is found. */
bool has_result = true;
bool has_instanced_panel = false;
for (LinkNode *pt_link = panel_types_stack; pt_link; pt_link = pt_link->next) {
PanelType *panel_type = pt_link->link;
/* Note that these checks are duplicated from #ED_region_panels_layout_ex. */
if (panel_type->flag & PNL_INSTANCED) {
has_instanced_panel = true;
continue;
}
if (use_category_tabs) {
if (panel_type->category[0] && !STREQ(category, panel_type->category)) {
continue;
}
}
/* We start property search with an empty panel list, so there's
* no point in trying to find an existing panel with this type. */
has_result = panel_property_search(C, region, style, NULL, panel_type, search_filter);
if (has_result) {
break;
}
}
/* Run property search for instanced panels (created in the layout calls of previous panels). */
if (!has_result && has_instanced_panel) {
LISTBASE_FOREACH (Panel *, panel, &region->panels) {
/* Note that these checks are duplicated from #ED_region_panels_layout_ex. */
if (panel->type == NULL || !(panel->type->flag & PNL_INSTANCED)) {
continue;
}
if (use_category_tabs) {
if (panel->type->category[0] && !STREQ(category, panel->type->category)) {
continue;
}
}
has_result = panel_property_search(C, region, style, panel, panel->type, search_filter);
if (has_result) {
break;
}
}
}
/* Free the panels and blocks, as they are only used for search. */
UI_blocklist_free(C, &region->uiblocks);
UI_panels_free_instanced(C, region);
BKE_area_region_panels_free(&region->panels);
return has_result;
}
void ED_region_header_layout(const bContext *C, ARegion *region)
{
const uiStyle *style = UI_style_get_dpi();

View File

@ -23,6 +23,7 @@
#pragma once
#include "BLI_bitmap.h"
#include "DNA_listBase.h"
#include "RNA_types.h"
@ -37,6 +38,16 @@ struct bNodeTree;
struct uiLayout;
struct wmOperatorType;
struct SpaceProperties_Runtime {
/** For filtering properties displayed in the space. */
char search_string[UI_MAX_NAME_STR];
/**
* Bitfield (in the same order as the tabs) for whether each tab has properties
* that match the search filter. Only valid when #search_string is set.
*/
BLI_bitmap *tab_search_results;
};
/* context data */
typedef struct ButsContextPath {

View File

@ -26,6 +26,7 @@
#include "MEM_guardedalloc.h"
#include "BLI_bitmap.h"
#include "BLI_blenlib.h"
#include "BLI_utildefines.h"
@ -48,6 +49,7 @@
#include "RNA_define.h"
#include "RNA_enum_types.h"
#include "UI_interface.h"
#include "UI_resources.h"
#include "buttons_intern.h" /* own include */
@ -110,7 +112,10 @@ static void buttons_free(SpaceLink *sl)
MEM_freeN(ct);
}
MEM_SAFE_FREE(sbuts->runtime);
if (sbuts->runtime != NULL) {
MEM_SAFE_FREE(sbuts->runtime->tab_search_results);
MEM_freeN(sbuts->runtime);
}
}
/* spacetype; init callback */
@ -121,6 +126,7 @@ static void buttons_init(struct wmWindowManager *UNUSED(wm), ScrArea *area)
if (sbuts->runtime == NULL) {
sbuts->runtime = MEM_mallocN(sizeof(SpaceProperties_Runtime), __func__);
sbuts->runtime->search_string[0] = '\0';
sbuts->runtime->tab_search_results = BLI_BITMAP_NEW(BCONTEXT_TOT * 2, __func__);
}
}
@ -135,6 +141,7 @@ static SpaceLink *buttons_duplicate(SpaceLink *sl)
if (sfile_old->runtime != NULL) {
sbutsn->runtime = MEM_dupallocN(sfile_old->runtime);
sbutsn->runtime->search_string[0] = '\0';
sbutsn->runtime->tab_search_results = BLI_BITMAP_NEW(BCONTEXT_TOT, __func__);
}
return (SpaceLink *)sbutsn;
@ -298,6 +305,163 @@ static void buttons_main_region_layout_properties(const bContext *C,
ED_region_panels_layout_ex(C, region, &region->type->paneltypes, contexts, NULL);
}
const char *ED_buttons_search_string_get(SpaceProperties *sbuts)
{
return sbuts->runtime->search_string;
}
int ED_buttons_search_string_length(struct SpaceProperties *sbuts)
{
return BLI_strnlen(sbuts->runtime->search_string, sizeof(sbuts->runtime->search_string));
}
void ED_buttons_search_string_set(SpaceProperties *sbuts, const char *value)
{
BLI_strncpy(sbuts->runtime->search_string, value, sizeof(sbuts->runtime->search_string));
}
bool ED_buttons_tab_has_search_result(SpaceProperties *sbuts, const int index)
{
return BLI_BITMAP_TEST(sbuts->runtime->tab_search_results, index);
}
static bool property_search_for_context(const bContext *C, ARegion *region, SpaceProperties *sbuts)
{
const char *contexts[2] = {buttons_main_region_context_string(sbuts->mainb), NULL};
if (sbuts->mainb == BCONTEXT_TOOL) {
return false;
}
buttons_context_compute(C, sbuts);
return ED_region_property_search(C, region, &region->type->paneltypes, contexts, NULL);
}
static void property_search_move_to_next_tab_with_results(SpaceProperties *sbuts,
const short *context_tabs_array,
const int tabs_len)
{
/* As long as all-tab search in the tool is disabled in the tool context, don't move from it. */
if (sbuts->mainb == BCONTEXT_TOOL) {
return;
}
int current_tab_index = 0;
for (int i = 0; i < tabs_len; i++) {
if (sbuts->mainb == context_tabs_array[i]) {
current_tab_index = i;
break;
}
}
/* Try the tabs after the current tab. */
for (int i = current_tab_index; i < tabs_len; i++) {
if (BLI_BITMAP_TEST(sbuts->runtime->tab_search_results, i)) {
sbuts->mainbuser = context_tabs_array[i];
return;
}
}
/* Try the tabs before the current tab. */
for (int i = 0; i < current_tab_index; i++) {
if (BLI_BITMAP_TEST(sbuts->runtime->tab_search_results, i)) {
sbuts->mainbuser = context_tabs_array[i];
return;
}
}
}
static void property_search_all_tabs(const bContext *C,
SpaceProperties *sbuts,
ARegion *main_region,
const short *context_tabs_array,
const int tabs_len)
{
/* Use local copies of the area and duplicate the region as a mainly-paranoid protection
* against changing any of the space / region data while running the search. */
ScrArea area_copy = *CTX_wm_area(C);
ARegion *region_copy = BKE_area_region_copy(area_copy.type, main_region);
bContext *C_copy = CTX_copy(C);
CTX_wm_area_set(C_copy, &area_copy);
CTX_wm_region_set(C_copy, region_copy);
SpaceProperties sbuts_copy = *sbuts;
sbuts_copy.path = NULL;
sbuts_copy.texuser = NULL;
sbuts_copy.runtime = MEM_dupallocN(sbuts->runtime);
sbuts_copy.runtime->tab_search_results = NULL;
area_copy.spacedata.first = &sbuts_copy;
/* Loop through the tabs added to the properties editor. */
for (int i = 0; i < tabs_len; i++) {
/* -1 corresponds to a spacer. */
if (context_tabs_array[i] == -1) {
continue;
}
/* Handle search for the current tab in the normal layout pass. */
if (context_tabs_array[i] == sbuts->mainb) {
continue;
}
sbuts_copy.mainb = sbuts_copy.mainbo = sbuts_copy.mainbuser = context_tabs_array[i];
/* Actually do the search and store the result in the bitmap. */
BLI_BITMAP_SET(sbuts->runtime->tab_search_results,
i,
property_search_for_context(C_copy, region_copy, &sbuts_copy));
UI_blocklist_free(C_copy, &region_copy->uiblocks);
}
BKE_area_region_free(area_copy.type, region_copy);
MEM_freeN(region_copy);
buttons_free((SpaceLink *)&sbuts_copy);
MEM_freeN(C_copy);
}
/**
* Handle property search for the layout pass, including finding which tabs have
* search results and switching if the current tab doesn't have a result.
*/
static void buttons_main_region_property_search(const bContext *C,
SpaceProperties *sbuts,
ARegion *region)
{
/* Theoretical maximum of every context shown with a spacer between every tab. */
short context_tabs_array[BCONTEXT_TOT * 2];
int tabs_len = ED_buttons_tabs_list(sbuts, context_tabs_array);
property_search_all_tabs(C, sbuts, region, context_tabs_array, tabs_len);
/* Check whether the current tab has a search match. */
bool current_tab_has_search_match = false;
LISTBASE_FOREACH (Panel *, panel, &region->panels) {
if (UI_panel_is_active(panel) && UI_panel_matches_search_filter(panel)) {
current_tab_has_search_match = true;
}
}
/* Find which index in the list the current tab corresponds to. */
int current_tab_index = -1;
for (int i = 0; i < tabs_len; i++) {
if (context_tabs_array[i] == sbuts->mainb) {
current_tab_index = i;
}
}
BLI_assert(current_tab_index != -1);
/* Update the tab search match flag for the current tab. */
BLI_BITMAP_SET(
sbuts->runtime->tab_search_results, current_tab_index, current_tab_has_search_match);
/* Move to the next tab with a result */
if (!current_tab_has_search_match) {
if (region->flag & RGN_FLAG_SEARCH_FILTER_UPDATE) {
property_search_move_to_next_tab_with_results(sbuts, context_tabs_array, tabs_len);
}
}
}
static void buttons_main_region_layout(const bContext *C, ARegion *region)
{
/* draw entirely, view changes should be handled here */
@ -310,6 +474,10 @@ static void buttons_main_region_layout(const bContext *C, ARegion *region)
buttons_main_region_layout_properties(C, sbuts, region);
}
if (region->flag & RGN_FLAG_SEARCH_FILTER_ACTIVE) {
buttons_main_region_property_search(C, sbuts, region);
}
sbuts->mainbo = sbuts->mainb;
}
@ -722,8 +890,8 @@ void ED_spacetype_buttons(void)
buttons_context_register(art);
BLI_addhead(&st->regiontypes, art);
/* Register the panel types from modifiers. The actual panels are built per modifier rather than
* per modifier type. */
/* Register the panel types from modifiers. The actual panels are built per modifier rather
* than per modifier type. */
for (ModifierType i = 0; i < NUM_MODIFIER_TYPES; i++) {
const ModifierTypeInfo *mti = BKE_modifier_get_info(i);
if (mti != NULL && mti->panelRegister != NULL) {

View File

@ -56,6 +56,9 @@ struct bNodeTree;
struct wmOperator;
struct wmTimer;
/* Defined in `buttons_intern.h`. */
typedef struct SpaceProperties_Runtime SpaceProperties_Runtime;
/* TODO 2.8: We don't write the global areas to files currently. Uncomment
* define to enable writing (should become the default in a bit). */
//#define WITH_GLOBAL_AREA_WRITING
@ -129,13 +132,6 @@ typedef enum eSpaceInfo_RptMask {
/** \name Properties Editor
* \{ */
#
#
typedef struct SpaceProperties_Runtime {
/** For filtering properties displayed in the space. Length defined as UI_MAX_NAME_STR. */
char search_string[128];
} SpaceProperties_Runtime;
/* Properties Editor */
typedef struct SpaceProperties {
SpaceLink *next, *prev;
@ -168,7 +164,7 @@ typedef struct SpaceProperties {
void *texuser;
/* Doesn't necessarily need to be a pointer, but runtime structs are still written to files. */
SpaceProperties_Runtime *runtime;
struct SpaceProperties_Runtime *runtime;
} SpaceProperties;
/* button defines (deprecated) */

View File

@ -1825,6 +1825,53 @@ static void rna_SpaceProperties_context_update(Main *UNUSED(bmain),
}
}
static int rna_SpaceProperties_tab_search_results_getlength(PointerRNA *ptr,
int length[RNA_MAX_ARRAY_DIMENSION])
{
SpaceProperties *sbuts = ptr->data;
short context_tabs_array[BCONTEXT_TOT * 2]; /* Dummy variable. */
const int tabs_len = ED_buttons_tabs_list(sbuts, context_tabs_array);
length[0] = tabs_len;
return length[0];
}
static void rna_SpaceProperties_tab_search_results_get(PointerRNA *ptr, bool *values)
{
SpaceProperties *sbuts = ptr->data;
short context_tabs_array[BCONTEXT_TOT * 2]; /* Dummy variable. */
const int tabs_len = ED_buttons_tabs_list(sbuts, context_tabs_array);
for (int i = 0; i < tabs_len; i++) {
values[i] = ED_buttons_tab_has_search_result(sbuts, i);
}
}
static void rna_SpaceProperties_search_filter_get(PointerRNA *ptr, char *value)
{
SpaceProperties *sbuts = ptr->data;
const char *search_filter = ED_buttons_search_string_get(sbuts);
strcpy(value, search_filter);
}
static int rna_SpaceProperties_search_filter_length(PointerRNA *ptr)
{
SpaceProperties *sbuts = ptr->data;
return ED_buttons_search_string_length(sbuts);
}
static void rna_SpaceProperties_search_filter_set(struct PointerRNA *ptr, const char *value)
{
SpaceProperties *sbuts = ptr->data;
ED_buttons_search_string_set(sbuts, value);
}
static void rna_SpaceProperties_search_filter_update(Main *UNUSED(bmain),
Scene *UNUSED(scene),
PointerRNA *ptr)
@ -4514,8 +4561,23 @@ static void rna_def_space_properties(BlenderRNA *brna)
RNA_def_property_ui_text(prop, "Pin ID", "Use the pinned context");
/* Property search. */
prop = RNA_def_property(srna, "tab_search_results", PROP_BOOLEAN, PROP_NONE);
RNA_def_property_array(prop, 0); /* Dynamic length, see next line. */
RNA_def_property_flag(prop, PROP_DYNAMIC);
RNA_def_property_clear_flag(prop, PROP_EDITABLE);
RNA_def_property_boolean_funcs(prop, "rna_SpaceProperties_tab_search_results_get", NULL);
RNA_def_property_dynamic_array_funcs(prop, "rna_SpaceProperties_tab_search_results_getlength");
RNA_def_property_ui_text(
prop, "Tab Search Results", "Whether or not each visible tab has a search result");
prop = RNA_def_property(srna, "search_filter", PROP_STRING, PROP_NONE);
RNA_def_property_string_sdna(prop, NULL, "runtime->search_string");
/* The search filter is stored in the property editor's runtime struct which
* is only defined in an internal header, so use the getter / setter here. */
RNA_def_property_string_funcs(prop,
"rna_SpaceProperties_search_filter_get",
"rna_SpaceProperties_search_filter_length",
"rna_SpaceProperties_search_filter_set");
RNA_def_property_ui_text(prop, "Display Filter", "Live search filtering string");
RNA_def_property_flag(prop, PROP_TEXTEDIT_UPDATE);
RNA_def_property_update(