From 92cf9dd2f2d7f4d8d380d4b1dcc17f6c2b5b175d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20T=C3=B6nne?= Date: Fri, 15 Dec 2023 10:20:44 +0100 Subject: [PATCH] ID properties: Support enum values with items Add support for enum values in ID properties. This is needed for the "Menu Switch" node implementation (#113445) which relies on ID properties for the top-level modifier UI. Enums items can optionally be added to the UI data of integer properties. Each property stores a full set of the enum items to keep things simple. Enum items can be added to properties using the `id_properties_ui` function in the python API. A detailed example can be found in the `bl_pyapi_idprop.py` test. There is currently no support yet for editing enum items through the UI. This is because the "Edit Property" feature is implemented entirely through a single operator (`WM_OT_properties_edit`) and its properties. Buttons to add/remove/move items would be operators changing another operator's properties. A refactor of the custom properties UI is likely required to make this work. Pull Request: https://projects.blender.org/blender/blender/pulls/114362 --- source/blender/blenkernel/BKE_idprop.h | 9 + source/blender/blenkernel/intern/idprop.cc | 102 +++++++++++ .../blenkernel/intern/idprop_serialize.cc | 20 ++ .../blender/blenkernel/intern/idprop_utils.cc | 3 + source/blender/blenlib/BLI_serialize.hh | 8 + source/blender/blenlib/intern/serialize.cc | 14 ++ source/blender/makesdna/DNA_ID.h | 18 +- source/blender/makesrna/intern/rna_ID.cc | 5 + source/blender/makesrna/intern/rna_access.cc | 49 +++++ .../python/generic/idprop_py_ui_api.cc | 173 +++++++++++++++++- tests/python/bl_pyapi_idprop.py | 23 +++ 11 files changed, 416 insertions(+), 8 deletions(-) diff --git a/source/blender/blenkernel/BKE_idprop.h b/source/blender/blenkernel/BKE_idprop.h index 9f5c8ec959c..f86d82b082c 100644 --- a/source/blender/blenkernel/BKE_idprop.h +++ b/source/blender/blenkernel/BKE_idprop.h @@ -21,6 +21,7 @@ struct BlendWriter; struct ID; struct IDProperty; struct IDPropertyUIData; +struct IDPropertyUIDataEnumItem; struct Library; typedef union IDPropertyTemplate { @@ -94,6 +95,14 @@ void IDP_AssignStringMaxSize(struct IDProperty *prop, const char *st, size_t st_ void IDP_AssignString(struct IDProperty *prop, const char *st) ATTR_NONNULL(); void IDP_FreeString(struct IDProperty *prop) ATTR_NONNULL(); +/*-------- Enum Type -------*/ + +const struct IDPropertyUIDataEnumItem *IDP_EnumItemFind(const struct IDProperty *prop); + +bool IDP_EnumItemsValidate(const struct IDPropertyUIDataEnumItem *items, + int items_num, + void (*error_fn)(const char *)); + /*-------- ID Type -------*/ typedef void (*IDPWalkFunc)(void *user_data, struct IDProperty *idp); diff --git a/source/blender/blenkernel/intern/idprop.cc b/source/blender/blenkernel/intern/idprop.cc index 7ce44bf2232..29692a0914c 100644 --- a/source/blender/blenkernel/intern/idprop.cc +++ b/source/blender/blenkernel/intern/idprop.cc @@ -13,9 +13,12 @@ #include #include +#include + #include "BLI_endian_switch.h" #include "BLI_listbase.h" #include "BLI_math_base.h" +#include "BLI_set.hh" #include "BLI_string.h" #include "BLI_utildefines.h" @@ -275,6 +278,14 @@ IDPropertyUIData *IDP_ui_data_copy(const IDProperty *prop) const IDPropertyUIDataInt *src = (const IDPropertyUIDataInt *)prop->ui_data; IDPropertyUIDataInt *dst = (IDPropertyUIDataInt *)dst_ui_data; dst->default_array = static_cast(MEM_dupallocN(src->default_array)); + dst->enum_items = static_cast(MEM_dupallocN(src->enum_items)); + for (const int64_t i : blender::IndexRange(src->enum_items_num)) { + const IDPropertyUIDataEnumItem &src_item = src->enum_items[i]; + IDPropertyUIDataEnumItem &dst_item = dst->enum_items[i]; + dst_item.identifier = BLI_strdup(src_item.identifier); + dst_item.name = BLI_strdup_null(src_item.name); + dst_item.description = BLI_strdup_null(src_item.description); + } break; } case IDP_UI_DATA_TYPE_BOOLEAN: { @@ -425,6 +436,78 @@ void IDP_FreeString(IDProperty *prop) MEM_freeN(prop->data.pointer); } } +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Enum Type (IDProperty Enum API) + * \{ */ + +static void IDP_int_ui_data_free_enum_items(IDPropertyUIDataInt *ui_data) +{ + for (const int64_t i : blender::IndexRange(ui_data->enum_items_num)) { + IDPropertyUIDataEnumItem &item = ui_data->enum_items[i]; + MEM_SAFE_FREE(item.identifier); + MEM_SAFE_FREE(item.name); + MEM_SAFE_FREE(item.description); + } + MEM_SAFE_FREE(ui_data->enum_items); +} + +const IDPropertyUIDataEnumItem *IDP_EnumItemFind(const IDProperty *prop) +{ + BLI_assert(prop->type == IDP_INT); + const IDPropertyUIDataInt *ui_data = reinterpret_cast( + prop->ui_data); + + const int value = IDP_Int(prop); + for (const IDPropertyUIDataEnumItem &item : + blender::Span(ui_data->enum_items, ui_data->enum_items_num)) + { + if (item.value == value) { + return &item; + } + } + return nullptr; +} + +bool IDP_EnumItemsValidate(const IDPropertyUIDataEnumItem *items, + const int items_num, + void (*error_fn)(const char *)) +{ + blender::Set used_values; + blender::Set used_identifiers; + used_values.reserve(items_num); + used_identifiers.reserve(items_num); + + bool is_valid = true; + for (const int64_t i : blender::IndexRange(items_num)) { + const IDPropertyUIDataEnumItem &item = items[i]; + if (item.identifier == nullptr || item.identifier[0] == '\0') { + if (error_fn) { + const std::string msg = "Item identifier is empty"; + error_fn(msg.c_str()); + } + is_valid = false; + } + if (!used_identifiers.add(item.identifier)) { + if (error_fn) { + const std::string msg = fmt::format("Item identifier '{}' is already used", + item.identifier); + error_fn(msg.c_str()); + } + is_valid = false; + } + if (!used_values.add(item.value)) { + if (error_fn) { + const std::string msg = fmt::format( + "Item value {} for item '{}' is already used", item.value, item.identifier); + error_fn(msg.c_str()); + } + is_valid = false; + } + } + return is_valid; +} /** \} */ @@ -1017,6 +1100,9 @@ void IDP_ui_data_free_unique_contents(IDPropertyUIData *ui_data, if (ui_data_int->default_array != other_int->default_array) { MEM_SAFE_FREE(ui_data_int->default_array); } + if (ui_data_int->enum_items != other_int->enum_items) { + IDP_int_ui_data_free_enum_items(ui_data_int); + } break; } case IDP_UI_DATA_TYPE_BOOLEAN: { @@ -1055,6 +1141,7 @@ void IDP_ui_data_free(IDProperty *prop) case IDP_UI_DATA_TYPE_INT: { IDPropertyUIDataInt *ui_data_int = (IDPropertyUIDataInt *)prop->ui_data; MEM_SAFE_FREE(ui_data_int->default_array); + IDP_int_ui_data_free_enum_items(ui_data_int); break; } case IDP_UI_DATA_TYPE_BOOLEAN: { @@ -1198,6 +1285,14 @@ static void write_ui_data(const IDProperty *prop, BlendWriter *writer) BLO_write_int32_array( writer, uint(ui_data_int->default_array_len), (int32_t *)ui_data_int->default_array); } + BLO_write_struct_array( + writer, IDPropertyUIDataEnumItem, ui_data_int->enum_items_num, ui_data_int->enum_items); + for (const int64_t i : blender::IndexRange(ui_data_int->enum_items_num)) { + IDPropertyUIDataEnumItem &item = ui_data_int->enum_items[i]; + BLO_write_string(writer, item.identifier); + BLO_write_string(writer, item.name); + BLO_write_string(writer, item.description); + } BLO_write_struct(writer, IDPropertyUIDataInt, ui_data); break; } @@ -1325,6 +1420,13 @@ static void read_ui_data(IDProperty *prop, BlendDataReader *reader) BLO_read_int32_array( reader, ui_data_int->default_array_len, (int **)&ui_data_int->default_array); } + BLO_read_data_address(reader, &ui_data_int->enum_items); + for (const int64_t i : blender::IndexRange(ui_data_int->enum_items_num)) { + IDPropertyUIDataEnumItem &item = ui_data_int->enum_items[i]; + BLO_read_data_address(reader, &item.identifier); + BLO_read_data_address(reader, &item.name); + BLO_read_data_address(reader, &item.description); + } break; } case IDP_UI_DATA_TYPE_BOOLEAN: { diff --git a/source/blender/blenkernel/intern/idprop_serialize.cc b/source/blender/blenkernel/intern/idprop_serialize.cc index 6675114c60f..c2a52f1733a 100644 --- a/source/blender/blenkernel/intern/idprop_serialize.cc +++ b/source/blender/blenkernel/intern/idprop_serialize.cc @@ -140,6 +140,11 @@ struct DictionaryEntryParser { return get_double(IDP_KEY_VALUE); } + std::optional get_enum_value() const + { + return get_enum(IDP_KEY_VALUE); + } + const ArrayValue *get_array_value() const { return get_array(IDP_KEY_VALUE); @@ -206,6 +211,21 @@ struct DictionaryEntryParser { return value->as_int_value()->value(); } + std::optional get_enum(StringRef key) const + { + const DictionaryValue::LookupValue *value_ptr = lookup.lookup_ptr(key); + if (value_ptr == nullptr) { + return std::nullopt; + } + const DictionaryValue::LookupValue &value = *value_ptr; + + if (value->type() != eValueType::Int) { + return std::nullopt; + } + + return value->as_int_value()->value(); + } + std::optional get_double(StringRef key) const { const DictionaryValue::LookupValue *value_ptr = lookup.lookup_ptr(key); diff --git a/source/blender/blenkernel/intern/idprop_utils.cc b/source/blender/blenkernel/intern/idprop_utils.cc index 7d65643226b..712294f82ff 100644 --- a/source/blender/blenkernel/intern/idprop_utils.cc +++ b/source/blender/blenkernel/intern/idprop_utils.cc @@ -100,6 +100,9 @@ static void idp_repr_fn_recursive(ReprState *state, const IDProperty *prop) break; } case IDP_INT: { + if (const IDPropertyUIDataEnumItem *item = IDP_EnumItemFind(prop)) { + STR_APPEND_FMT("%s", item->name); + } STR_APPEND_FMT("%d", IDP_Int(prop)); break; } diff --git a/source/blender/blenlib/BLI_serialize.hh b/source/blender/blenlib/BLI_serialize.hh index 1ade17877d9..19376e5765b 100644 --- a/source/blender/blenlib/BLI_serialize.hh +++ b/source/blender/blenlib/BLI_serialize.hh @@ -80,6 +80,7 @@ enum class eValueType { Boolean, Double, Dictionary, + Enum, }; class Value; @@ -89,6 +90,7 @@ template class PrimitiveValue; using IntValue = PrimitiveValue; using DoubleValue = PrimitiveValue; using BooleanValue = PrimitiveValue; +using EnumValue = PrimitiveValue; class ArrayValue; /** @@ -146,6 +148,12 @@ class Value { */ const BooleanValue *as_boolean_value() const; + /** + * Casts to a EnumValue. + * Will return nullptr when it is a different type. + */ + const EnumValue *as_enum_value() const; + /** * Casts to an ArrayValue. * Will return nullptr when it is a different type. diff --git a/source/blender/blenlib/intern/serialize.cc b/source/blender/blenlib/intern/serialize.cc index 506453c8e35..f7c55705558 100644 --- a/source/blender/blenlib/intern/serialize.cc +++ b/source/blender/blenlib/intern/serialize.cc @@ -41,6 +41,14 @@ const BooleanValue *Value::as_boolean_value() const return static_cast(this); } +const EnumValue *Value::as_enum_value() const +{ + if (type_ != eValueType::Enum) { + return nullptr; + } + return static_cast(this); +} + const ArrayValue *Value::as_array_value() const { if (type_ != eValueType::Array) { @@ -121,6 +129,12 @@ static void convert_to_json(nlohmann::ordered_json &j, const Value &value) case eValueType::Double: { j = value.as_double_value()->value(); + break; + } + + case eValueType::Enum: { + j = value.as_enum_value()->value(); + break; } } } diff --git a/source/blender/makesdna/DNA_ID.h b/source/blender/makesdna/DNA_ID.h index 9ed5c7bbde6..18b0356166c 100644 --- a/source/blender/makesdna/DNA_ID.h +++ b/source/blender/makesdna/DNA_ID.h @@ -54,12 +54,25 @@ typedef struct IDPropertyUIData { char _pad[4]; } IDPropertyUIData; +/* DNA version of #EnumPropertyItem. */ +typedef struct IDPropertyUIDataEnumItem { + /* Unique identifier, used for string lookup. */ + char *identifier; + /* UI name of the item. */ + char *name; + /* Optional description. */ + char *description; + /* Unique integer value, should never change. */ + int value; + /* Optional icon. */ + int icon; +} IDPropertyUIDataEnumItem; + /* IDP_UI_DATA_TYPE_INT */ typedef struct IDPropertyUIDataInt { IDPropertyUIData base; int *default_array; /* Only for array properties. */ int default_array_len; - char _pad[4]; int min; int max; @@ -67,6 +80,9 @@ typedef struct IDPropertyUIDataInt { int soft_max; int step; int default_value; + + int enum_items_num; + IDPropertyUIDataEnumItem *enum_items; } IDPropertyUIDataInt; /** For #IDP_UI_DATA_TYPE_BOOLEAN Use `int8_t` because DNA does not support `bool`. */ diff --git a/source/blender/makesrna/intern/rna_ID.cc b/source/blender/makesrna/intern/rna_ID.cc index f28c49f2c53..7b3b7c4819f 100644 --- a/source/blender/makesrna/intern/rna_ID.cc +++ b/source/blender/makesrna/intern/rna_ID.cc @@ -1585,6 +1585,11 @@ static void rna_def_ID_properties(BlenderRNA *brna) RNA_def_property_flag(prop, PROP_IDPROPERTY); RNA_def_property_array(prop, 1); + /* IDP_ENUM */ + prop = RNA_def_property(srna, "enum", PROP_ENUM, PROP_NONE); + RNA_def_property_enum_items(prop, rna_enum_dummy_DEFAULT_items); + RNA_def_property_flag(prop, PROP_IDPROPERTY); + /* IDP_GROUP */ prop = RNA_def_property(srna, "group", PROP_POINTER, PROP_NONE); RNA_def_property_flag(prop, PROP_IDPROPERTY); diff --git a/source/blender/makesrna/intern/rna_access.cc b/source/blender/makesrna/intern/rna_access.cc index c4e1bd8e050..fb3bb01b006 100644 --- a/source/blender/makesrna/intern/rna_access.cc +++ b/source/blender/makesrna/intern/rna_access.cc @@ -519,6 +519,14 @@ void rna_property_rna_or_id_get(PropertyRNA *prop, r_prop_rna_or_id->array_len = idprop_evaluated != nullptr ? uint(idprop_evaluated->len) : 0; } else { + /* Special case for int properties with enum items, these are displayed as a PROP_ENUM. */ + if (idprop->type == IDP_INT) { + const IDPropertyUIDataInt *ui_data_int = reinterpret_cast( + idprop->ui_data); + if (ui_data_int && ui_data_int->enum_items_num > 0) { + r_prop_rna_or_id->rnaprop = &rna_PropertyGroupItem_enum; + } + } r_prop_rna_or_id->rnaprop = typemap[int(idprop->type)]; } } @@ -560,6 +568,14 @@ PropertyRNA *rna_ensure_property(PropertyRNA *prop) if (idprop->type == IDP_ARRAY) { return arraytypemap[int(idprop->subtype)]; } + /* Special case for int properties with enum items, these are displayed as a PROP_ENUM. */ + if (idprop->type == IDP_INT) { + const IDPropertyUIDataInt *ui_data_int = reinterpret_cast( + idprop->ui_data); + if (ui_data_int && ui_data_int->enum_items_num > 0) { + return &rna_PropertyGroupItem_enum; + } + } return typemap[int(idprop->type)]; } } @@ -1501,6 +1517,39 @@ void RNA_property_enum_items_ex(bContext *C, int *r_totitem, bool *r_free) { + if (!use_static && prop->magic != RNA_MAGIC) { + const IDProperty *idprop = (IDProperty *)prop; + if (idprop->type == IDP_INT) { + IDPropertyUIDataInt *ui_data = reinterpret_cast(idprop->ui_data); + + int totitem = 0; + EnumPropertyItem *result = nullptr; + if (ui_data) { + for (const IDPropertyUIDataEnumItem &idprop_item : + blender::Span(ui_data->enum_items, ui_data->enum_items_num)) + { + BLI_assert(idprop_item.identifier != nullptr); + BLI_assert(idprop_item.name != nullptr); + const EnumPropertyItem item = {idprop_item.value, + idprop_item.identifier, + idprop_item.icon, + idprop_item.name, + idprop_item.description ? idprop_item.description : ""}; + RNA_enum_item_add(&result, &totitem, &item); + } + } + + RNA_enum_item_end(&result, &totitem); + *r_item = result; + if (r_totitem) { + /* Exclude the terminator item. */ + *r_totitem = totitem - 1; + } + *r_free = true; + return; + } + } + EnumPropertyRNA *eprop = (EnumPropertyRNA *)rna_ensure_property(prop); *r_free = false; diff --git a/source/blender/python/generic/idprop_py_ui_api.cc b/source/blender/python/generic/idprop_py_ui_api.cc index c04fa040c3e..2a6f869a3a7 100644 --- a/source/blender/python/generic/idprop_py_ui_api.cc +++ b/source/blender/python/generic/idprop_py_ui_api.cc @@ -66,6 +66,105 @@ static bool idprop_ui_data_update_base(IDPropertyUIData *ui_data, return true; } +/* Utility function for parsing ints in an if statement. */ +static bool py_long_as_int(PyObject *py_long, int *r_int) +{ + if (PyLong_CheckExact(py_long)) { + *r_int = int(PyLong_AS_LONG(py_long)); + return true; + } + return false; +} + +/** + * Similar to #enum_items_from_py, which parses enum items for RNA properties. + * This function is simpler, since it doesn't have to parse a default value or handle the case of + * enum flags (PROP_ENUM_FLAG). + */ +static bool try_parse_enum_item(PyObject *py_item, const int index, IDPropertyUIDataEnumItem &item) +{ + if (!PyTuple_CheckExact(py_item)) { + return false; + } + Py_ssize_t item_size = PyTuple_GET_SIZE(py_item); + if (item_size < 3 || item_size > 5) { + return false; + } + + Py_ssize_t identifier_len; + Py_ssize_t name_len; + Py_ssize_t description_len; + const char *identifier = PyUnicode_AsUTF8AndSize(PyTuple_GET_ITEM(py_item, 0), &identifier_len); + const char *name = PyUnicode_AsUTF8AndSize(PyTuple_GET_ITEM(py_item, 1), &name_len); + const char *description = PyUnicode_AsUTF8AndSize(PyTuple_GET_ITEM(py_item, 2), + &description_len); + if (!identifier || !name || !description) { + return false; + } + + const char *icon_name = nullptr; + if (item_size <= 3) { + item.value = index; + } + else if (item_size == 4) { + if (!py_long_as_int(PyTuple_GET_ITEM(py_item, 3), &item.value)) { + return false; + } + } + else if (item_size == 5) { + /* Must have icon value or name. */ + if (!py_long_as_int(PyTuple_GET_ITEM(py_item, 3), &item.icon) && + !(icon_name = PyUnicode_AsUTF8(PyTuple_GET_ITEM(py_item, 3)))) + { + return false; + } + if (!py_long_as_int(PyTuple_GET_ITEM(py_item, 4), &item.value)) { + return false; + } + } + + item.identifier = BLI_strdup(identifier); + item.name = BLI_strdup(name); + item.description = BLI_strdup_null(description); + if (icon_name) { + RNA_enum_value_from_identifier(rna_enum_icon_items, icon_name, &item.icon); + } + return true; +} + +static IDPropertyUIDataEnumItem *idprop_enum_items_from_py(PyObject *seq_fast, int &r_items_num) +{ + IDPropertyUIDataEnumItem *items; + + const Py_ssize_t seq_len = PySequence_Fast_GET_SIZE(seq_fast); + PyObject **seq_fast_items = PySequence_Fast_ITEMS(seq_fast); + int i; + + items = MEM_cnew_array(seq_len, __func__); + r_items_num = seq_len; + + for (i = 0; i < seq_len; i++) { + IDPropertyUIDataEnumItem item = {nullptr, nullptr, nullptr, 0, 0}; + PyObject *py_item = seq_fast_items[i]; + if (try_parse_enum_item(py_item, i, item)) { + items[i] = item; + } + else if (py_item == Py_None) { + items[i].identifier = nullptr; + } + else { + MEM_freeN(items); + PyErr_SetString(PyExc_TypeError, + "expected a tuple containing " + "(identifier, name, description) and optionally an " + "icon name and unique number"); + return nullptr; + } + } + + return items; +} + /** * \note The default value needs special handling because for array IDProperties it can * be a single value or an array, but for non-array properties it can only be a value. @@ -114,11 +213,22 @@ static bool idprop_ui_data_update_int(IDProperty *idprop, PyObject *args, PyObje const char *description = nullptr; int min, max, soft_min, soft_max, step; PyObject *default_value = nullptr; + PyObject *items = nullptr; const char *kwlist[] = { - "min", "max", "soft_min", "soft_max", "step", "default", "subtype", "description", nullptr}; + "min", + "max", + "soft_min", + "soft_max", + "step", + "default", + "items", + "subtype", + "description", + nullptr, + }; if (!PyArg_ParseTupleAndKeywords(args, kwargs, - "|$iiiiiOzz:update", + "|$iiiiiOOzz:update", (char **)kwlist, &min, &max, @@ -126,6 +236,7 @@ static bool idprop_ui_data_update_int(IDProperty *idprop, PyObject *args, PyObje &soft_max, &step, &default_value, + &items, &rna_subtype, &description)) { @@ -173,6 +284,32 @@ static bool idprop_ui_data_update_int(IDProperty *idprop, PyObject *args, PyObje } } + if (!ELEM(items, nullptr, Py_None)) { + PyObject *items_fast; + if (!(items_fast = PySequence_Fast(items, "expected a sequence of tuples for the enum items"))) + { + return false; + } + + int idprop_items_num = 0; + IDPropertyUIDataEnumItem *idprop_items = idprop_enum_items_from_py(items_fast, + idprop_items_num); + if (!idprop_items) { + Py_DECREF(items_fast); + return false; + } + if (!IDP_EnumItemsValidate(idprop_items, idprop_items_num, [](const char *msg) { + PyErr_SetString(PyExc_ValueError, msg); + })) + { + Py_DECREF(items_fast); + return false; + } + Py_DECREF(items_fast); + ui_data.enum_items = idprop_items; + ui_data.enum_items_num = idprop_items_num; + } + /* Write back to the property's UI data. */ IDP_ui_data_free_unique_contents(&ui_data_orig->base, IDP_ui_data_type(idprop), &ui_data.base); *ui_data_orig = ui_data; @@ -482,6 +619,7 @@ PyDoc_STRVAR(BPy_IDPropertyUIManager_update_doc, "step=None, " "default=None, " "id_type=None, " + "items=None, " "description=None)\n" "\n" " Update the RNA information of the IDProperty used for interaction and\n" @@ -567,6 +705,27 @@ static void idprop_ui_data_to_dict_int(IDProperty *property, PyObject *dict) PyDict_SetItemString(dict, "default", item = PyLong_FromLong(ui_data->default_value)); Py_DECREF(item); } + + if (ui_data->enum_items_num > 0) { + PyObject *items_list = PyList_New(ui_data->enum_items_num); + for (int i = 0; i < ui_data->enum_items_num; ++i) { + const IDPropertyUIDataEnumItem &item = ui_data->enum_items[i]; + BLI_assert(item.identifier != nullptr); + BLI_assert(item.name != nullptr); + + PyObject *item_tuple = PyTuple_New(5); + PyTuple_SET_ITEM(item_tuple, 0, PyUnicode_FromString(item.identifier)); + PyTuple_SET_ITEM(item_tuple, 1, PyUnicode_FromString(item.name)); + PyTuple_SET_ITEM( + item_tuple, 2, PyUnicode_FromString(item.description ? item.description : "")); + PyTuple_SET_ITEM(item_tuple, 3, PyLong_FromLong(item.icon)); + PyTuple_SET_ITEM(item_tuple, 4, PyLong_FromLong(item.value)); + + PyList_SET_ITEM(items_list, i, item_tuple); + } + PyDict_SetItemString(dict, "items", items_list); + Py_DECREF(items_list); + } } static void idprop_ui_data_to_dict_bool(IDProperty *property, PyObject *dict) @@ -636,17 +795,17 @@ static void idprop_ui_data_to_dict_id(IDProperty *property, PyObject *dict) short id_type_value = ui_data->id_type; if (id_type_value == 0) { - /* While UI exposed custom properties do not allow the 'all ID types' `0` value, in py-defined - * IDProperties it is accepted. So force defining a valid id_type value when this function is - * called. */ + /* While UI exposed custom properties do not allow the 'all ID types' `0` value, in + * py-defined IDProperties it is accepted. So force defining a valid id_type value when this + * function is called. */ ID *id = IDP_Id(property); id_type_value = id ? GS(id->name) : ID_OB; } const char *id_type = nullptr; if (!RNA_enum_identifier(rna_enum_id_type_items, id_type_value, &id_type)) { - /* Same fall-back as above, in case it is an unknown ID type (from a future version of Blender - * e.g.). */ + /* Same fall-back as above, in case it is an unknown ID type (from a future version of + * Blender e.g.). */ RNA_enum_identifier(rna_enum_id_type_items, ID_OB, &id_type); } PyObject *item = PyUnicode_FromString(id_type); diff --git a/tests/python/bl_pyapi_idprop.py b/tests/python/bl_pyapi_idprop.py index d6d78f248ba..41881884cc2 100644 --- a/tests/python/bl_pyapi_idprop.py +++ b/tests/python/bl_pyapi_idprop.py @@ -69,6 +69,11 @@ class TestIdPropertyCreation(TestHelper, unittest.TestCase): self.assertEqual(self.id["a"], b"Hello World") self.assertTrue(isinstance(self.id["a"], bytes)) + def test_enum(self): + self.id["a"] = 5 + self.assertEqual(self.id["a"], 5) + self.assertTrue(isinstance(self.id["a"], int)) + def test_sequence_double_list(self): mylist = [1.2, 3.4, 5.6] self.id["a"] = mylist @@ -349,6 +354,24 @@ class TestRNAData(TestHelper, unittest.TestCase): rna_data = ui_data_test_prop_array.as_dict() self.assertEqual(rna_data["default"], [1, 2]) + # Test RNA data for enum property. + test_object.id_properties_clear() + test_object["test_enum_prop"] = 2 + ui_data_test_prop_enum = test_object.id_properties_ui("test_enum_prop") + ui_data_test_prop_enum_items = [ + ("TOMATOES", "Tomatoes", "Solanum lycopersicum"), + ("CUCUMBERS", "Cucumbers", "Cucumis sativus"), + ("RADISHES", "Radishes", "Raphanus raphanistrum"), + ] + ui_data_test_prop_enum.update(items=ui_data_test_prop_enum_items) + ui_data_test_prop_enum_items_full = [ + ("TOMATOES", "Tomatoes", "Solanum lycopersicum", 0, 0), + ("CUCUMBERS", "Cucumbers", "Cucumis sativus", 0, 1), + ("RADISHES", "Radishes", "Raphanus raphanistrum", 0, 2), + ] + rna_data = ui_data_test_prop_enum.as_dict() + self.assertEqual(rna_data["items"], ui_data_test_prop_enum_items_full) + if __name__ == '__main__': import sys