UI support for showing candidates for string properties

Currently strings are used for cases where a list of identifiers would
be useful to show.

Add support for string properties to reference a callback to populate
candidates to show when editing a string. The user isn't prevented from
typing in text not found in this list, it's just useful as a reference.

Support for expanding the following strings has been added:

- Operator, menu & panel identifiers in the keymap editor.
- WM operators that reference data-paths expand using the
  Python-consoles auto-complete functionality.
- Names of keying sets for insert/delete keyframe operators.

Details:

- `bpy.props.StringProperty` takes an option `search` callback.

- A new string callback has been added, set via
  `RNA_def_property_string_search_func` or
  `RNA_def_property_string_search_func_runtime`.

- Addresses usability issue highlighted by T89560,
  where setting keying set identifiers as strings isn't practical.

- Showing additional right-aligned text in the search results is
  supported but disabled by default as the text is too cramped in most
  string search popups where the feature would make sense. It could be
  enabled as part of other layout tweaks.

Reviewed By: brecht

Ref D14986
This commit is contained in:
Campbell Barton 2022-05-20 14:30:17 +10:00
parent 11480763b6
commit 3f3d82cfe9
Notes: blender-bot 2023-10-12 12:49:04 +02:00
Referenced by commit a631dc5575, Fix T100731: Keymap Editor context menu crash
Referenced by commit 49032a8ca5, Fix error checking the search callback
Referenced by issue #100731, Regression: Keymap operator right click crash
21 changed files with 766 additions and 70 deletions

View File

@ -21,10 +21,28 @@ from bpy.props import (
)
from bpy.app.translations import pgettext_iface as iface_
def rna_path_prop_search_for_context(self, context, edit_text):
# Use the same logic as auto-completing in the Python console to expand the data-path.
from bl_console_utils.autocomplete import intellisense
context_prefix = "context."
line = context_prefix + edit_text
cursor = len(line)
namespace = {"context": context}
comp_prefix, _, comp_options = intellisense.expand(line=line, cursor=len(line), namespace=namespace, private=False)
prefix = comp_prefix[len(context_prefix):] # Strip "context."
for attr in comp_options.split("\n"):
# Exclude function calls because they are generally not part of data-paths.
if attr.endswith(("(", ")")):
continue
yield prefix + attr.lstrip()
rna_path_prop = StringProperty(
name="Context Attributes",
description="RNA context string",
maxlen=1024,
search=rna_path_prop_search_for_context,
)
rna_reverse_prop = BoolProperty(

View File

@ -81,6 +81,7 @@ typedef struct Global {
* * 1 - 30: EEVEE debug/stats values (01/2018).
* * 31: Enable the Select Debug Engine. Only available with #WITH_DRAW_DEBUG (08/2021).
* * 101: Enable UI debug drawing of fullscreen area's corner widget (10/2014).
* * 102: Enable extra items in string search UI (05/2022).
* * 666: Use quicker batch delete for outliners' delete hierarchy (01/2019).
* * 777: Enable UI node panel's sockets polling (11/2011).
* * 799: Enable some mysterious new depsgraph behavior (05/2015).

View File

@ -2051,6 +2051,8 @@ void ANIM_OT_keyframe_insert_by_name(wmOperatorType *ot)
/* keyingset to use (idname) */
prop = RNA_def_string(
ot->srna, "type", NULL, MAX_ID_NAME - 2, "Keying Set", "The Keying Set to use");
RNA_def_property_string_search_func_runtime(
prop, ANIM_keyingset_visit_for_search_no_poll, PROP_STRING_SEARCH_SUGGESTION);
RNA_def_property_flag(prop, PROP_HIDDEN);
ot->prop = prop;
}
@ -2246,6 +2248,8 @@ void ANIM_OT_keyframe_delete_by_name(wmOperatorType *ot)
/* keyingset to use (idname) */
prop = RNA_def_string(
ot->srna, "type", NULL, MAX_ID_NAME - 2, "Keying Set", "The Keying Set to use");
RNA_def_property_string_search_func_runtime(
prop, ANIM_keyingset_visit_for_search_no_poll, PROP_STRING_SEARCH_SUGGESTION);
RNA_def_property_flag(prop, PROP_HIDDEN);
ot->prop = prop;
}

View File

@ -708,6 +708,72 @@ KeyingSet *ANIM_get_keyingset_for_autokeying(const Scene *scene, const char *tra
return ANIM_builtin_keyingset_get_named(NULL, transformKSName);
}
static void anim_keyingset_visit_for_search_impl(const bContext *C,
StringPropertySearchVisitFunc visit_fn,
void *visit_user_data,
const bool use_poll)
{
/* Poll requires context. */
if (use_poll && (C == NULL)) {
return;
}
Scene *scene = C ? CTX_data_scene(C) : NULL;
KeyingSet *ks;
/* Active Keying Set. */
if (!use_poll || (scene && scene->active_keyingset)) {
StringPropertySearchVisitParams visit_params = {NULL};
visit_params.text = "__ACTIVE__";
visit_params.info = "Active Keying Set";
visit_fn(visit_user_data, &visit_params);
}
/* User-defined Keying Sets. */
if (scene && scene->keyingsets.first) {
for (ks = scene->keyingsets.first; ks; ks = ks->next) {
if (use_poll && !ANIM_keyingset_context_ok_poll((bContext *)C, ks)) {
continue;
}
StringPropertySearchVisitParams visit_params = {NULL};
visit_params.text = ks->idname;
visit_params.info = ks->name;
visit_fn(visit_user_data, &visit_params);
}
}
/* Builtin Keying Sets. */
for (ks = builtin_keyingsets.first; ks; ks = ks->next) {
if (use_poll && !ANIM_keyingset_context_ok_poll((bContext *)C, ks)) {
continue;
}
StringPropertySearchVisitParams visit_params = {NULL};
visit_params.text = ks->idname;
visit_params.info = ks->name;
visit_fn(visit_user_data, &visit_params);
}
}
void ANIM_keyingset_visit_for_search(const bContext *C,
PointerRNA *UNUSED(ptr),
PropertyRNA *UNUSED(prop),
const char *UNUSED(edit_text),
StringPropertySearchVisitFunc visit_fn,
void *visit_user_data)
{
anim_keyingset_visit_for_search_impl(C, visit_fn, visit_user_data, false);
}
void ANIM_keyingset_visit_for_search_no_poll(const bContext *C,
PointerRNA *UNUSED(ptr),
PropertyRNA *UNUSED(prop),
const char *UNUSED(edit_text),
StringPropertySearchVisitFunc visit_fn,
void *visit_user_data)
{
anim_keyingset_visit_for_search_impl(C, visit_fn, visit_user_data, true);
}
/* Menu of All Keying Sets ----------------------------- */
const EnumPropertyItem *ANIM_keying_sets_enum_itemf(bContext *C,

View File

@ -351,6 +351,19 @@ int ANIM_scene_get_keyingset_index(struct Scene *scene, struct KeyingSet *ks);
struct KeyingSet *ANIM_get_keyingset_for_autokeying(const struct Scene *scene,
const char *transformKSName);
void ANIM_keyingset_visit_for_search(const struct bContext *C,
struct PointerRNA *ptr,
struct PropertyRNA *prop,
const char *edit_text,
StringPropertySearchVisitFunc visit_fn,
void *visit_user_data);
void ANIM_keyingset_visit_for_search_no_poll(const struct bContext *C,
struct PointerRNA *ptr,
struct PropertyRNA *prop,
const char *edit_text,
StringPropertySearchVisitFunc visit_fn,
void *visit_user_data);
/**
* Dynamically populate an enum of Keying Sets.
*/

View File

@ -2338,7 +2338,14 @@ void uiItemFullR(uiLayout *layout,
/* property with separate label */
else if (ELEM(type, PROP_ENUM, PROP_STRING, PROP_POINTER)) {
but = ui_item_with_label(layout, block, name, icon, ptr, prop, index, 0, 0, w, h, flag);
but = ui_but_add_search(but, ptr, prop, NULL, NULL, false);
bool results_are_suggestions = false;
if (type == PROP_STRING) {
const eStringPropertySearchFlag search_flag = RNA_property_string_search_flag(prop);
if (search_flag & PROP_STRING_SEARCH_SUGGESTION) {
results_are_suggestions = true;
}
}
but = ui_but_add_search(but, ptr, prop, NULL, NULL, results_are_suggestions);
if (layout->redalert) {
UI_but_flag_enable(but, UI_BUT_REDALERT);
@ -2711,11 +2718,16 @@ uiBut *ui_but_add_search(uiBut *but,
PropertyRNA *prop,
PointerRNA *searchptr,
PropertyRNA *searchprop,
bool results_are_suggestions)
const bool results_are_suggestions)
{
/* for ID's we do automatic lookup */
bool has_search_fn = false;
PointerRNA sptr;
if (!searchprop) {
if (RNA_property_type(prop) == PROP_STRING) {
has_search_fn = (RNA_property_string_search_flag(prop) != 0);
}
if (RNA_property_type(prop) == PROP_POINTER) {
StructRNA *ptype = RNA_property_pointer_type(ptr, prop);
search_id_collection(ptype, &sptr, &searchprop);
@ -2724,14 +2736,18 @@ uiBut *ui_but_add_search(uiBut *but,
}
/* turn button into search button */
if (searchprop) {
if (has_search_fn || searchprop) {
uiRNACollectionSearch *coll_search = MEM_mallocN(sizeof(*coll_search), __func__);
uiButSearch *search_but;
but = ui_but_change_type(but, UI_BTYPE_SEARCH_MENU);
search_but = (uiButSearch *)but;
search_but->rnasearchpoin = *searchptr;
search_but->rnasearchprop = searchprop;
if (searchptr) {
search_but->rnasearchpoin = *searchptr;
search_but->rnasearchprop = searchprop;
}
but->hardmax = MAX2(but->hardmax, 256.0f);
but->drawflag |= UI_BUT_ICON_LEFT | UI_BUT_TEXT_LEFT;
if (RNA_property_is_unlink(prop)) {
@ -2740,8 +2756,17 @@ uiBut *ui_but_add_search(uiBut *but,
coll_search->target_ptr = *ptr;
coll_search->target_prop = prop;
coll_search->search_ptr = *searchptr;
coll_search->search_prop = searchprop;
if (searchptr) {
coll_search->search_ptr = *searchptr;
coll_search->search_prop = searchprop;
}
else {
/* Rely on `has_search_fn`. */
coll_search->search_ptr = PointerRNA_NULL;
coll_search->search_prop = NULL;
}
coll_search->search_but = but;
coll_search->butstore_block = but->block;
coll_search->butstore = UI_butstore_create(coll_search->butstore_block);

View File

@ -24,6 +24,7 @@
#include "BLT_translation.h"
#include "BKE_context.h"
#include "BKE_global.h"
#include "BKE_lib_id.h"
#include "BKE_report.h"
@ -516,71 +517,136 @@ void ui_rna_collection_search_update_fn(
StringSearch *search = skip_filter ? nullptr : BLI_string_search_new();
/* build a temporary list of relevant items first */
int item_index = 0;
RNA_PROP_BEGIN (&data->search_ptr, itemptr, data->search_prop) {
if (data->search_prop != nullptr) {
/* build a temporary list of relevant items first */
int item_index = 0;
RNA_PROP_BEGIN (&data->search_ptr, itemptr, data->search_prop) {
if (flag & PROP_ID_SELF_CHECK) {
if (itemptr.data == data->target_ptr.owner_id) {
continue;
}
}
/* use filter */
if (is_ptr_target) {
if (RNA_property_pointer_poll(&data->target_ptr, data->target_prop, &itemptr) == 0) {
continue;
}
}
int name_prefix_offset = 0;
int iconid = ICON_NONE;
bool has_sep_char = false;
const bool is_id = itemptr.type && RNA_struct_is_ID(itemptr.type);
if (is_id) {
iconid = ui_id_icon_get(C, static_cast<ID *>(itemptr.data), false);
if (!ELEM(iconid, 0, ICON_BLANK1)) {
has_id_icon = true;
if (flag & PROP_ID_SELF_CHECK) {
if (itemptr.data == data->target_ptr.owner_id) {
continue;
}
}
if (requires_exact_data_name) {
name = RNA_struct_name_get_alloc(&itemptr, name_buf, sizeof(name_buf), nullptr);
/* use filter */
if (is_ptr_target) {
if (RNA_property_pointer_poll(&data->target_ptr, data->target_prop, &itemptr) == 0) {
continue;
}
}
int name_prefix_offset = 0;
int iconid = ICON_NONE;
bool has_sep_char = false;
const bool is_id = itemptr.type && RNA_struct_is_ID(itemptr.type);
if (is_id) {
iconid = ui_id_icon_get(C, static_cast<ID *>(itemptr.data), false);
if (!ELEM(iconid, 0, ICON_BLANK1)) {
has_id_icon = true;
}
if (requires_exact_data_name) {
name = RNA_struct_name_get_alloc(&itemptr, name_buf, sizeof(name_buf), nullptr);
}
else {
const ID *id = static_cast<ID *>(itemptr.data);
BKE_id_full_name_ui_prefix_get(name_buf, id, true, UI_SEP_CHAR, &name_prefix_offset);
BLI_STATIC_ASSERT(sizeof(name_buf) >= MAX_ID_FULL_NAME_UI,
"Name string buffer should be big enough to hold full UI ID name");
name = name_buf;
has_sep_char = ID_IS_LINKED(id);
}
}
else {
const ID *id = static_cast<ID *>(itemptr.data);
BKE_id_full_name_ui_prefix_get(name_buf, id, true, UI_SEP_CHAR, &name_prefix_offset);
BLI_STATIC_ASSERT(sizeof(name_buf) >= MAX_ID_FULL_NAME_UI,
"Name string buffer should be big enough to hold full UI ID name");
name = name_buf;
has_sep_char = ID_IS_LINKED(id);
name = RNA_struct_name_get_alloc(&itemptr, name_buf, sizeof(name_buf), nullptr);
}
}
else {
name = RNA_struct_name_get_alloc(&itemptr, name_buf, sizeof(name_buf), nullptr);
}
if (name) {
CollItemSearch *cis = MEM_cnew<CollItemSearch>(__func__);
cis->data = itemptr.data;
cis->name = BLI_strdup(name);
cis->index = item_index;
cis->iconid = iconid;
cis->is_id = is_id;
cis->name_prefix_offset = name_prefix_offset;
cis->has_sep_char = has_sep_char;
if (!skip_filter) {
BLI_string_search_add(search, name, cis, 0);
if (name) {
CollItemSearch *cis = MEM_cnew<CollItemSearch>(__func__);
cis->data = itemptr.data;
cis->name = BLI_strdup(name);
cis->index = item_index;
cis->iconid = iconid;
cis->is_id = is_id;
cis->name_prefix_offset = name_prefix_offset;
cis->has_sep_char = has_sep_char;
if (!skip_filter) {
BLI_string_search_add(search, name, cis, 0);
}
BLI_addtail(items_list, cis);
if (name != name_buf) {
MEM_freeN(name);
}
}
BLI_addtail(items_list, cis);
if (name != name_buf) {
MEM_freeN(name);
}
}
item_index++;
item_index++;
}
RNA_PROP_END;
}
else {
BLI_assert(RNA_property_type(data->target_prop) == PROP_STRING);
const eStringPropertySearchFlag search_flag = RNA_property_string_search_flag(
data->target_prop);
BLI_assert(search_flag & PROP_STRING_SEARCH_SUPPORTED);
struct SearchVisitUserData {
StringSearch *search;
bool skip_filter;
int item_index;
ListBase *items_list;
const char *func_id;
} user_data = {nullptr};
user_data.search = search;
user_data.skip_filter = skip_filter;
user_data.items_list = items_list;
user_data.func_id = __func__;
RNA_property_string_search(
C,
&data->target_ptr,
data->target_prop,
str,
[](void *user_data, const StringPropertySearchVisitParams *visit_params) {
const bool show_extra_info = (G.debug_value == 102);
SearchVisitUserData *search_data = (struct SearchVisitUserData *)user_data;
CollItemSearch *cis = MEM_cnew<CollItemSearch>(search_data->func_id);
cis->data = nullptr;
if (visit_params->info && show_extra_info) {
cis->name = BLI_sprintfN(
"%s" UI_SEP_CHAR_S "%s", visit_params->text, visit_params->info);
}
else {
cis->name = BLI_strdup(visit_params->text);
}
cis->index = search_data->item_index;
cis->iconid = ICON_NONE;
cis->is_id = false;
cis->name_prefix_offset = 0;
cis->has_sep_char = visit_params->info != nullptr;
if (!search_data->skip_filter) {
BLI_string_search_add(search_data->search, visit_params->text, cis, 0);
}
BLI_addtail(search_data->items_list, cis);
search_data->item_index++;
},
(void *)&user_data);
if (search_flag & PROP_STRING_SEARCH_SORT) {
BLI_listbase_sort(items_list, [](const void *a_, const void *b_) -> int {
const CollItemSearch *cis_a = (const CollItemSearch *)a_;
const CollItemSearch *cis_b = (const CollItemSearch *)b_;
return BLI_strcasecmp_natural(cis_a->name, cis_b->name);
});
int i = 0;
LISTBASE_FOREACH (CollItemSearch *, cis, items_list) {
cis->index = i;
i++;
}
}
}
RNA_PROP_END;
if (skip_filter) {
LISTBASE_FOREACH (CollItemSearch *, cis, items_list) {

View File

@ -358,6 +358,21 @@ char *RNA_property_string_get_alloc(
PointerRNA *ptr, PropertyRNA *prop, char *fixedbuf, int fixedlen, int *r_len);
void RNA_property_string_set(PointerRNA *ptr, PropertyRNA *prop, const char *value);
void RNA_property_string_set_bytes(PointerRNA *ptr, PropertyRNA *prop, const char *value, int len);
eStringPropertySearchFlag RNA_property_string_search_flag(PropertyRNA *prop);
/**
* Search candidates for string `prop` by calling `visit_fn` with each string.
* Typically these strings are collected in `visit_user_data` in a format defined by the caller.
*
* See #PropStringSearchFunc for details.
*/
void RNA_property_string_search(const struct bContext *C,
PointerRNA *ptr,
PropertyRNA *prop,
const char *edit_text,
StringPropertySearchVisitFunc visit_fn,
void *visit_user_data);
/**
* \return the length without `\0` terminator.
*/

View File

@ -446,6 +446,9 @@ void RNA_def_property_string_funcs(PropertyRNA *prop,
const char *get,
const char *length,
const char *set);
void RNA_def_property_string_search_func(PropertyRNA *prop,
const char *search,
eStringPropertySearchFlag search_flag);
void RNA_def_property_pointer_funcs(
PropertyRNA *prop, const char *get, const char *set, const char *type_fn, const char *poll);
void RNA_def_property_collection_funcs(PropertyRNA *prop,
@ -490,6 +493,9 @@ void RNA_def_property_string_funcs_runtime(PropertyRNA *prop,
StringPropertyGetFunc getfunc,
StringPropertyLengthFunc lengthfunc,
StringPropertySetFunc setfunc);
void RNA_def_property_string_search_func_runtime(PropertyRNA *prop,
StringPropertySearchFunc search_fn,
eStringPropertySearchFlag search_flag);
void RNA_def_property_translation_context(PropertyRNA *prop, const char *context);

View File

@ -515,6 +515,55 @@ typedef int (*StringPropertyLengthFunc)(struct PointerRNA *ptr, struct PropertyR
typedef void (*StringPropertySetFunc)(struct PointerRNA *ptr,
struct PropertyRNA *prop,
const char *value);
typedef struct StringPropertySearchVisitParams {
/** Text being searched for (never NULL). */
const char *text;
/** Additional information to display (optional, may be NULL). */
const char *info;
} StringPropertySearchVisitParams;
typedef enum eStringPropertySearchFlag {
/**
* Used so the result of #RNA_property_string_search_flag can be used to check
* if search is supported.
*/
PROP_STRING_SEARCH_SUPPORTED = (1 << 0),
/** Items resulting from the search must be sorted. */
PROP_STRING_SEARCH_SORT = (1 << 1),
/**
* Allow members besides the ones listed to be entered.
*
* \warning disabling this options causes the search callback to run on redraw and should
* only be enabled this doesn't cause performance issues.
*/
PROP_STRING_SEARCH_SUGGESTION = (1 << 2),
} eStringPropertySearchFlag;
/**
* Visit string search candidates, `text` may be freed once this callback has finished,
* so references to it should not be held.
*/
typedef void (*StringPropertySearchVisitFunc)(void *visit_user_data,
const StringPropertySearchVisitParams *params);
/**
* \param C: context, may be NULL (in this case all available items should be shown).
* \param ptr: RNA pointer.
* \param prop: RNA property. This must have it's #StringPropertyRNA.search callback set,
* to check this use `RNA_property_string_search_flag(prop) & PROP_STRING_SEARCH_SUPPORTED`.
* \param edit_text: Optionally use the string being edited by the user as a basis
* for the search results (auto-complete Python attributes for e.g.).
* \param visit_fn: This function is called with every search candidate and is typically
* responsible for storing the search results.
* \param visit_user_data: Caller defined data, passed to `visit_fn`.
*/
typedef void (*StringPropertySearchFunc)(const struct bContext *C,
struct PointerRNA *ptr,
struct PropertyRNA *prop,
const char *edit_text,
StringPropertySearchVisitFunc visit_fn,
void *visit_user_data);
typedef int (*EnumPropertyGetFunc)(struct PointerRNA *ptr, struct PropertyRNA *prop);
typedef void (*EnumPropertySetFunc)(struct PointerRNA *ptr, struct PropertyRNA *prop, int value);
/* same as PropEnumItemFunc */

View File

@ -1039,6 +1039,38 @@ static void rna_clamp_value(FILE *f, PropertyRNA *prop, int array)
}
}
static char *rna_def_property_search_func(FILE *f,
StructRNA *srna,
PropertyRNA *prop,
PropertyDefRNA *UNUSED(dp),
const char *manualfunc)
{
char *func;
if (prop->flag & PROP_IDPROPERTY && manualfunc == NULL) {
return NULL;
}
if (!manualfunc) {
return NULL;
}
func = rna_alloc_function_name(srna->identifier, rna_safe_id(prop->identifier), "search");
fprintf(f,
"void %s("
"const bContext *C, "
"PointerRNA *ptr, "
"PropertyRNA *prop, "
"const char *edit_text, "
"StringPropertySearchVisitFunc visit_fn, "
"void *visit_user_data)\n",
func);
fprintf(f, "{\n");
fprintf(f, "\n %s(C, ptr, prop, edit_text, visit_fn, visit_user_data);\n", manualfunc);
fprintf(f, "}\n\n");
return func;
}
static char *rna_def_property_set_func(
FILE *f, StructRNA *srna, PropertyRNA *prop, PropertyDefRNA *dp, const char *manualfunc)
{
@ -1895,6 +1927,8 @@ static void rna_def_property_funcs(FILE *f, StructRNA *srna, PropertyDefRNA *dp)
sprop->length = (void *)rna_def_property_length_func(
f, srna, prop, dp, (const char *)sprop->length);
sprop->set = (void *)rna_def_property_set_func(f, srna, prop, dp, (const char *)sprop->set);
sprop->search = (void *)rna_def_property_search_func(
f, srna, prop, dp, (const char *)sprop->search);
break;
}
case PROP_POINTER: {
@ -4081,13 +4115,15 @@ static void rna_generate_property(FILE *f, StructRNA *srna, const char *nest, Pr
case PROP_STRING: {
StringPropertyRNA *sprop = (StringPropertyRNA *)prop;
fprintf(f,
"\t%s, %s, %s, %s, %s, %s, %d, ",
"\t%s, %s, %s, %s, %s, %s, %s, %d, %d, ",
rna_function_string(sprop->get),
rna_function_string(sprop->length),
rna_function_string(sprop->set),
rna_function_string(sprop->get_ex),
rna_function_string(sprop->length_ex),
rna_function_string(sprop->set_ex),
rna_function_string(sprop->search),
(int)sprop->search_flag,
sprop->maxlength);
rna_print_c_string(f, sprop->defaultvalue);
fprintf(f, "\n");

View File

@ -3369,6 +3369,34 @@ int RNA_property_string_default_length(PointerRNA *UNUSED(ptr), PropertyRNA *pro
return strlen(sprop->defaultvalue);
}
eStringPropertySearchFlag RNA_property_string_search_flag(PropertyRNA *prop)
{
StringPropertyRNA *sprop = (StringPropertyRNA *)rna_ensure_property(prop);
if (prop->magic != RNA_MAGIC) {
return false;
}
BLI_assert(RNA_property_type(prop) == PROP_STRING);
if (sprop->search) {
BLI_assert(sprop->search_flag & PROP_STRING_SEARCH_SUPPORTED);
}
else {
BLI_assert(sprop->search_flag == 0);
}
return sprop->search_flag;
}
void RNA_property_string_search(const bContext *C,
PointerRNA *ptr,
PropertyRNA *prop,
const char *edit_text,
StringPropertySearchVisitFunc visit_fn,
void *visit_user_data)
{
BLI_assert(RNA_property_string_search_flag(prop) & PROP_STRING_SEARCH_SUPPORTED);
StringPropertyRNA *sprop = (StringPropertyRNA *)rna_ensure_property(prop);
sprop->search(C, ptr, prop, edit_text, visit_fn, visit_user_data);
}
int RNA_property_enum_get(PointerRNA *ptr, PropertyRNA *prop)
{
EnumPropertyRNA *eprop = (EnumPropertyRNA *)prop;

View File

@ -3316,6 +3316,33 @@ void RNA_def_property_string_funcs(PropertyRNA *prop,
}
}
void RNA_def_property_string_search_func(PropertyRNA *prop,
const char *search,
const eStringPropertySearchFlag search_flag)
{
StructRNA *srna = DefRNA.laststruct;
if (!DefRNA.preprocess) {
CLOG_ERROR(&LOG, "only during preprocessing.");
return;
}
switch (prop->type) {
case PROP_STRING: {
StringPropertyRNA *sprop = (StringPropertyRNA *)prop;
sprop->search = (StringPropertySearchFunc)search;
if (search != NULL) {
sprop->search_flag = search_flag | PROP_STRING_SEARCH_SUPPORTED;
}
break;
}
default:
CLOG_ERROR(&LOG, "\"%s.%s\", type is not string.", srna->identifier, prop->identifier);
DefRNA.error = true;
break;
}
}
void RNA_def_property_string_funcs_runtime(PropertyRNA *prop,
StringPropertyGetFunc getfunc,
StringPropertyLengthFunc lengthfunc,
@ -3343,6 +3370,18 @@ void RNA_def_property_string_funcs_runtime(PropertyRNA *prop,
}
}
void RNA_def_property_string_search_func_runtime(PropertyRNA *prop,
StringPropertySearchFunc search_fn,
const eStringPropertySearchFlag search_flag)
{
StringPropertyRNA *sprop = (StringPropertyRNA *)prop;
sprop->search = search_fn;
if (search_fn != NULL) {
sprop->search_flag = search_flag | PROP_STRING_SEARCH_SUPPORTED;
}
}
void RNA_def_property_pointer_funcs(
PropertyRNA *prop, const char *get, const char *set, const char *type_fn, const char *poll)
{

View File

@ -440,6 +440,15 @@ typedef struct StringPropertyRNA {
PropStringLengthFuncEx length_ex;
PropStringSetFuncEx set_ex;
/**
* Optional callback to list candidates for a string.
* This is only for use as suggestions in UI, other values may be assigned.
*
* \note The callback type is public, hence the difference in naming convention.
*/
StringPropertySearchFunc search;
eStringPropertySearchFlag search_flag;
int maxlength; /* includes string terminator! */
const char *defaultvalue;

View File

@ -2599,6 +2599,9 @@ static void rna_def_keyconfig(BlenderRNA *brna)
"rna_wmKeyMapItem_idname_get",
"rna_wmKeyMapItem_idname_length",
"rna_wmKeyMapItem_idname_set");
RNA_def_property_string_search_func(prop,
"WM_operatortype_idname_visit_for_search",
PROP_STRING_SEARCH_SORT | PROP_STRING_SEARCH_SUGGESTION);
RNA_def_struct_name_property(srna, prop);
RNA_def_property_update(prop, 0, "rna_KeyMapItem_update");

View File

@ -191,6 +191,17 @@ static const EnumPropertyItem property_subtype_array_items[] = {
"'XYZ', 'XYZ_LENGTH', 'COLOR_GAMMA', 'COORDINATES', 'LAYER', 'LAYER_MEMBER', 'NONE'].\n" \
" :type subtype: string\n"
static const EnumPropertyItem property_string_search_options_items[] = {
{PROP_STRING_SEARCH_SORT, "SORT", 0, "Sort Search Results", ""},
{PROP_STRING_SEARCH_SUGGESTION,
"SUGGESTION",
0,
"Suggestion",
"Search results are suggestions (other values may be entered)"},
{0, NULL, 0, NULL, NULL},
};
/** \} */
/* -------------------------------------------------------------------- */
@ -257,6 +268,11 @@ struct BPyPropStore {
/** Wrap: #RNA_def_property_poll_runtime */
PyObject *poll_fn;
} pointer_data;
/** #PROP_STRING type. */
struct {
/** Wrap: #RNA_def_property_string_search_func_runtime */
PyObject *search_fn;
} string_data;
};
} py_data;
};
@ -1672,6 +1688,163 @@ static void bpy_prop_string_set_fn(struct PointerRNA *ptr,
}
}
static bool bpy_prop_string_visit_fn_call(PyObject *py_func,
PyObject *item,
StringPropertySearchVisitFunc visit_fn,
void *visit_user_data)
{
const char *text;
const char *info = NULL;
if (PyTuple_CheckExact(item)) {
/* Positional only. */
static const char *_keywords[] = {
"",
"",
NULL,
};
static _PyArg_Parser _parser = {
"s" /* `text` */
"s" /* `info` */
":search",
_keywords,
0,
};
if (!_PyArg_ParseTupleAndKeywordsFast(item, NULL, &_parser, &text, &info)) {
PyC_Err_PrintWithFunc(py_func);
return false;
}
}
else {
text = PyUnicode_AsUTF8(item);
if (UNLIKELY(text == NULL)) {
PyErr_Clear();
PyErr_Format(PyExc_TypeError,
"expected sequence of strings or tuple pairs of strings, not %.200s",
Py_TYPE(item)->tp_name);
PyC_Err_PrintWithFunc(py_func);
return false;
}
}
StringPropertySearchVisitParams visit_params = {NULL};
visit_params.text = text;
visit_params.info = info;
visit_fn(visit_user_data, &visit_params);
return true;
}
static void bpy_prop_string_visit_for_search_fn(const struct bContext *C,
struct PointerRNA *ptr,
struct PropertyRNA *prop,
const char *edit_text,
StringPropertySearchVisitFunc visit_fn,
void *visit_user_data)
{
struct BPyPropStore *prop_store = RNA_property_py_data_get(prop);
PyObject *py_func;
PyObject *args;
PyObject *self;
PyObject *ret;
PyGILState_STATE gilstate;
PyObject *py_edit_text;
BLI_assert(prop_store != NULL);
if (C) {
bpy_context_set((struct bContext *)C, &gilstate);
}
else {
gilstate = PyGILState_Ensure();
}
py_func = prop_store->py_data.string_data.search_fn;
args = PyTuple_New(3);
self = pyrna_struct_as_instance(ptr);
PyTuple_SET_ITEM(args, 0, self);
Py_INCREF(bpy_context_module);
PyTuple_SET_ITEM(args, 1, (PyObject *)bpy_context_module);
py_edit_text = PyUnicode_FromString(edit_text);
PyTuple_SET_ITEM(args, 2, py_edit_text);
ret = PyObject_CallObject(py_func, args);
Py_DECREF(args);
if (ret == NULL) {
PyC_Err_PrintWithFunc(py_func);
}
else {
if (PyIter_Check(ret)) {
/* Iterators / generator types. */
PyObject *it;
PyObject *(*iternext)(PyObject *);
it = PyObject_GetIter(ret);
if (it == NULL) {
PyC_Err_PrintWithFunc(py_func);
}
else {
iternext = *Py_TYPE(it)->tp_iternext;
for (;;) {
PyObject *py_text = iternext(it);
if (py_text == NULL) {
break;
}
const bool ok = bpy_prop_string_visit_fn_call(
py_func, py_text, visit_fn, visit_user_data);
Py_DECREF(py_text);
if (!ok) {
break;
}
}
Py_DECREF(it);
if (PyErr_Occurred()) {
if (PyErr_ExceptionMatches(PyExc_StopIteration)) {
PyErr_Clear();
}
else {
PyC_Err_PrintWithFunc(py_func);
}
}
}
}
else {
/* Sequence (typically list/tuple). */
PyObject *ret_fast = PySequence_Fast(
ret,
"StringProperty(...): "
"return value from search callback was not a sequence, iterator or generator");
if (ret_fast == NULL) {
PyC_Err_PrintWithFunc(py_func);
}
else {
const Py_ssize_t ret_num = PySequence_Fast_GET_SIZE(ret_fast);
PyObject **ret_fast_items = PySequence_Fast_ITEMS(ret_fast);
for (Py_ssize_t i = 0; i < ret_num; i++) {
const bool ok = bpy_prop_string_visit_fn_call(
py_func, ret_fast_items[i], visit_fn, visit_user_data);
if (!ok) {
break;
}
}
Py_DECREF(ret_fast);
}
}
Py_DECREF(ret);
}
if (C) {
bpy_context_clear((struct bContext *)C, &gilstate);
}
else {
PyGILState_Release(gilstate);
}
}
/** \} */
/* -------------------------------------------------------------------- */
@ -2352,11 +2525,14 @@ static void bpy_prop_callback_assign_float_array(struct PropertyRNA *prop,
static void bpy_prop_callback_assign_string(struct PropertyRNA *prop,
PyObject *get_fn,
PyObject *set_fn)
PyObject *set_fn,
PyObject *search_fn,
const eStringPropertySearchFlag search_flag)
{
StringPropertyGetFunc rna_get_fn = NULL;
StringPropertyLengthFunc rna_length_fn = NULL;
StringPropertySetFunc rna_set_fn = NULL;
StringPropertySearchFunc rna_search_fn = NULL;
if (get_fn && get_fn != Py_None) {
struct BPyPropStore *prop_store = bpy_prop_py_data_ensure(prop);
@ -2372,8 +2548,17 @@ static void bpy_prop_callback_assign_string(struct PropertyRNA *prop,
rna_set_fn = bpy_prop_string_set_fn;
ASSIGN_PYOBJECT_INCREF(prop_store->py_data.set_fn, set_fn);
}
if (search_fn) {
struct BPyPropStore *prop_store = bpy_prop_py_data_ensure(prop);
rna_search_fn = bpy_prop_string_visit_for_search_fn;
ASSIGN_PYOBJECT_INCREF(prop_store->py_data.string_data.search_fn, search_fn);
}
RNA_def_property_string_funcs_runtime(prop, rna_get_fn, rna_length_fn, rna_set_fn);
if (rna_search_fn) {
RNA_def_property_string_search_func_runtime(prop, rna_search_fn, search_flag);
}
}
static void bpy_prop_callback_assign_enum(struct PropertyRNA *prop,
@ -2628,6 +2813,24 @@ static int bpy_prop_arg_parse_tag_defines(PyObject *o, void *p)
" This function must take 2 values (self, value) and return None.\n" \
" :type set: function\n"
#define BPY_PROPDEF_SEARCH_DOC \
" :arg search: Function to be called to show candidates for this string (shown in the UI).\n" \
" This function must take 3 values (self, context, edit_text)\n" \
" and return a sequence, iterator or generator where each item must be:\n" \
"\n" \
" - A single string (representing a candidate to display).\n" \
" - A tuple-pair of strings, where the first is a candidate and the second\n" \
" is additional information about the candidate.\n" \
" :type search: function\n" \
" :arg search_options: Set of strings in:\n" \
"\n" \
" - 'SORT' sorts the resulting items.\n" \
" - 'SUGGESTION' lets the user enter values not found in search candidates.\n" \
" **WARNING** disabling this flag causes the search callback to run on redraw,\n" \
" so only disable this flag if it's not likely to cause performance issues.\n" \
"\n" \
" :type search_options: set\n"
#define BPY_PROPDEF_POINTER_TYPE_DOC \
" :arg type: A subclass of :class:`bpy.types.PropertyGroup` or :class:`bpy.types.ID`.\n" \
" :type type: class\n"
@ -3721,7 +3924,9 @@ PyDoc_STRVAR(BPy_StringProperty_doc,
"subtype='NONE', "
"update=None, "
"get=None, "
"set=None)\n"
"set=None, "
"search=None, "
"search_options={'SUGGESTION'})\n"
"\n"
" Returns a new string property definition.\n"
"\n" BPY_PROPDEF_NAME_DOC BPY_PROPDEF_DESC_DOC
@ -3730,7 +3935,7 @@ PyDoc_STRVAR(BPy_StringProperty_doc,
" :arg maxlen: maximum length of the string.\n"
" :type maxlen: int\n" BPY_PROPDEF_OPTIONS_DOC BPY_PROPDEF_OPTIONS_OVERRIDE_DOC
BPY_PROPDEF_TAGS_DOC BPY_PROPDEF_SUBTYPE_STRING_DOC BPY_PROPDEF_UPDATE_DOC
BPY_PROPDEF_GET_DOC BPY_PROPDEF_SET_DOC);
BPY_PROPDEF_GET_DOC BPY_PROPDEF_SET_DOC BPY_PROPDEF_SEARCH_DOC);
static PyObject *BPy_StringProperty(PyObject *self, PyObject *args, PyObject *kw)
{
StructRNA *srna;
@ -3767,6 +3972,11 @@ static PyObject *BPy_StringProperty(PyObject *self, PyObject *args, PyObject *kw
PyObject *update_fn = NULL;
PyObject *get_fn = NULL;
PyObject *set_fn = NULL;
PyObject *search_fn = NULL;
static struct BPy_EnumProperty_Parse search_options_enum = {
.items = property_string_search_options_items,
.value = PROP_STRING_SEARCH_SUGGESTION,
};
static const char *_keywords[] = {
"attr",
@ -3781,6 +3991,8 @@ static PyObject *BPy_StringProperty(PyObject *self, PyObject *args, PyObject *kw
"update",
"get",
"set",
"search",
"search_options",
NULL,
};
static _PyArg_Parser _parser = {
@ -3797,6 +4009,8 @@ static PyObject *BPy_StringProperty(PyObject *self, PyObject *args, PyObject *kw
"O" /* `update` */
"O" /* `get` */
"O" /* `set` */
"O" /* `search` */
"O&" /* `search_options` */
":StringProperty",
_keywords,
0,
@ -3820,7 +4034,10 @@ static PyObject *BPy_StringProperty(PyObject *self, PyObject *args, PyObject *kw
&subtype_enum,
&update_fn,
&get_fn,
&set_fn)) {
&set_fn,
&search_fn,
pyrna_enum_bitfield_parse_set,
&search_options_enum)) {
return NULL;
}
@ -3833,6 +4050,9 @@ static PyObject *BPy_StringProperty(PyObject *self, PyObject *args, PyObject *kw
if (bpy_prop_callback_check(set_fn, "set", 2) == -1) {
return NULL;
}
if (bpy_prop_callback_check(set_fn, "search", 3) == -1) {
return NULL;
}
if (id_data.prop_free_handle != NULL) {
RNA_def_property_free_identifier_deferred_finish(srna, id_data.prop_free_handle);
@ -3858,7 +4078,7 @@ static PyObject *BPy_StringProperty(PyObject *self, PyObject *args, PyObject *kw
bpy_prop_assign_flag_override(prop, override_enum.value);
}
bpy_prop_callback_assign_update(prop, update_fn);
bpy_prop_callback_assign_string(prop, get_fn, set_fn);
bpy_prop_callback_assign_string(prop, get_fn, set_fn, search_fn, search_options_enum.value);
RNA_def_property_duplicate_pointers(srna, prop);
Py_RETURN_NONE;

View File

@ -974,6 +974,14 @@ bool WM_operatortype_remove(const char *idname);
* Remove memory of all previously executed tools.
*/
void WM_operatortype_last_properties_clear_all(void);
void WM_operatortype_idname_visit_for_search(const struct bContext *C,
PointerRNA *ptr,
PropertyRNA *prop,
const char *edit_text,
StringPropertySearchVisitFunc visit_fn,
void *visit_user_data);
/**
* Tag all operator-properties of \a ot defined after calling this, until
* the next #WM_operatortype_props_advanced_end call (if available), with
@ -1077,6 +1085,13 @@ void WM_menutype_freelink(struct MenuType *mt);
void WM_menutype_free(void);
bool WM_menutype_poll(struct bContext *C, struct MenuType *mt);
void WM_menutype_idname_visit_for_search(const struct bContext *C,
struct PointerRNA *ptr,
struct PropertyRNA *prop,
const char *edit_text,
StringPropertySearchVisitFunc visit_fn,
void *visit_user_data);
/* wm_panel_type.c */
/**
@ -1088,6 +1103,13 @@ struct PanelType *WM_paneltype_find(const char *idname, bool quiet);
bool WM_paneltype_add(struct PanelType *pt);
void WM_paneltype_remove(struct PanelType *pt);
void WM_paneltype_idname_visit_for_search(const struct bContext *C,
struct PointerRNA *ptr,
struct PropertyRNA *prop,
const char *edit_text,
StringPropertySearchVisitFunc visit_fn,
void *visit_user_data);
/* wm_gesture_ops.c */
int WM_gesture_box_invoke(struct bContext *C, struct wmOperator *op, const struct wmEvent *event);

View File

@ -99,3 +99,21 @@ bool WM_menutype_poll(bContext *C, MenuType *mt)
}
return true;
}
void WM_menutype_idname_visit_for_search(const bContext *UNUSED(C),
PointerRNA *UNUSED(ptr),
PropertyRNA *UNUSED(prop),
const char *UNUSED(edit_text),
StringPropertySearchVisitFunc visit_fn,
void *visit_user_data)
{
GHashIterator gh_iter;
GHASH_ITER (gh_iter, menutypes_hash) {
MenuType *mt = BLI_ghashIterator_getValue(&gh_iter);
StringPropertySearchVisitParams visit_params = {NULL};
visit_params.text = mt->idname;
visit_params.info = mt->label;
visit_fn(visit_user_data, &visit_params);
}
}

View File

@ -246,6 +246,27 @@ void WM_operatortype_last_properties_clear_all(void)
}
}
void WM_operatortype_idname_visit_for_search(const bContext *UNUSED(C),
PointerRNA *UNUSED(ptr),
PropertyRNA *UNUSED(prop),
const char *UNUSED(edit_text),
StringPropertySearchVisitFunc visit_fn,
void *visit_user_data)
{
GHashIterator gh_iter;
GHASH_ITER (gh_iter, global_ops_hash) {
wmOperatorType *ot = BLI_ghashIterator_getValue(&gh_iter);
char idname_py[OP_MAX_TYPENAME];
WM_operator_py_idname(idname_py, ot->idname);
StringPropertySearchVisitParams visit_params = {NULL};
visit_params.text = idname_py;
visit_params.info = ot->name;
visit_fn(visit_user_data, &visit_params);
}
}
/** \} */
/* -------------------------------------------------------------------- */

View File

@ -1870,7 +1870,14 @@ static void WM_OT_call_menu(wmOperatorType *ot)
ot->flag = OPTYPE_INTERNAL;
RNA_def_string(ot->srna, "name", NULL, BKE_ST_MAXNAME, "Name", "Name of the menu");
PropertyRNA *prop;
prop = RNA_def_string(ot->srna, "name", NULL, BKE_ST_MAXNAME, "Name", "Name of the menu");
RNA_def_property_string_search_func_runtime(
prop,
WM_menutype_idname_visit_for_search,
/* Only a suggestion as menu items may be referenced from add-ons that have been disabled. */
(PROP_STRING_SEARCH_SORT | PROP_STRING_SEARCH_SUGGESTION));
}
static int wm_call_pie_menu_invoke(bContext *C, wmOperator *op, const wmEvent *event)
@ -1902,7 +1909,14 @@ static void WM_OT_call_menu_pie(wmOperatorType *ot)
ot->flag = OPTYPE_INTERNAL;
RNA_def_string(ot->srna, "name", NULL, BKE_ST_MAXNAME, "Name", "Name of the pie menu");
PropertyRNA *prop;
prop = RNA_def_string(ot->srna, "name", NULL, BKE_ST_MAXNAME, "Name", "Name of the pie menu");
RNA_def_property_string_search_func_runtime(
prop,
WM_menutype_idname_visit_for_search,
/* Only a suggestion as menu items may be referenced from add-ons that have been disabled. */
(PROP_STRING_SEARCH_SORT | PROP_STRING_SEARCH_SUGGESTION));
}
static int wm_call_panel_exec(bContext *C, wmOperator *op)
@ -1938,6 +1952,11 @@ static void WM_OT_call_panel(wmOperatorType *ot)
PropertyRNA *prop;
prop = RNA_def_string(ot->srna, "name", NULL, BKE_ST_MAXNAME, "Name", "Name of the menu");
RNA_def_property_string_search_func_runtime(
prop,
WM_paneltype_idname_visit_for_search,
/* Only a suggestion as menu items may be referenced from add-ons that have been disabled. */
(PROP_STRING_SEARCH_SORT | PROP_STRING_SEARCH_SUGGESTION));
RNA_def_property_flag(prop, PROP_SKIP_SAVE);
prop = RNA_def_boolean(ot->srna, "keep_open", true, "Keep Open", "");
RNA_def_property_flag(prop, PROP_SKIP_SAVE);

View File

@ -65,3 +65,21 @@ void WM_paneltype_clear(void)
{
BLI_ghash_free(g_paneltypes_hash, NULL, NULL);
}
void WM_paneltype_idname_visit_for_search(const bContext *UNUSED(C),
PointerRNA *UNUSED(ptr),
PropertyRNA *UNUSED(prop),
const char *UNUSED(edit_text),
StringPropertySearchVisitFunc visit_fn,
void *visit_user_data)
{
GHashIterator gh_iter;
GHASH_ITER (gh_iter, g_paneltypes_hash) {
PanelType *pt = BLI_ghashIterator_getValue(&gh_iter);
StringPropertySearchVisitParams visit_params = {NULL};
visit_params.text = pt->idname;
visit_params.info = pt->label;
visit_fn(visit_user_data, &visit_params);
}
}