Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCampbell Barton <campbell@blender.org>2022-05-20 07:30:17 +0300
committerCampbell Barton <campbell@blender.org>2022-05-26 05:16:35 +0300
commit3f3d82cfe9cefe4bfd9da3d283dec4a1923ec22d (patch)
tree532de0110686d29f63c550c4784f8d2acc86e110 /source/blender/python
parent11480763b62e6ca72ae869391b8e5495e57410a6 (diff)
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
Diffstat (limited to 'source/blender/python')
-rw-r--r--source/blender/python/intern/bpy_props.c230
1 files changed, 225 insertions, 5 deletions
diff --git a/source/blender/python/intern/bpy_props.c b/source/blender/python/intern/bpy_props.c
index a6aa1f46b0c..f4ebc68b5ef 100644
--- a/source/blender/python/intern/bpy_props.c
+++ b/source/blender/python/intern/bpy_props.c
@@ -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;