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
This commit is contained in:
Lukas Tönne
2023-12-15 10:20:44 +01:00
parent 7346727cfc
commit 92cf9dd2f2
11 changed files with 416 additions and 8 deletions

View File

@@ -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<IDPropertyUIDataEnumItem>(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);