aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFriedemann Kleint <Friedemann.Kleint@qt.io>2025-06-11 11:06:31 +0200
committerFriedemann Kleint <Friedemann.Kleint@qt.io>2025-06-17 12:04:42 +0200
commit4aa48368667bee64e48f7c9d6b3a935411d5f23c (patch)
treec19be3cd8624322bcee32272f1d4d410218e6abf
parent7ae471b49d0f38d7ac1c367bcfa3378f309c6b94 (diff)
Enable using @QEnum/@QFlag-decorated enums as custom widget properties
Create a dynamic QMetaType for @QEnum-decorated enumerations so that Qt Widgets Designer is able to recognize the properties. [ChangeLog][PySide6] It is now possible to use @QEnum/@QFlag-decorated enumerations as properties of custom widgets in Qt Widgets Designer. Task-number: PYSIDE-2840 Change-Id: I58a16002f89678856b7f33d687cf99f00c6f0cc7 Reviewed-by: Cristian Maureira-Fredes <cristian.maureira-fredes@qt.io>
-rw-r--r--sources/pyside6/libpyside/dynamicqmetaobject.cpp17
-rw-r--r--sources/pyside6/libpyside/pysideqenum.cpp108
-rw-r--r--sources/pyside6/libpyside/pysideqenum.h6
-rw-r--r--sources/pyside6/tests/QtCore/CMakeLists.txt1
-rw-r--r--sources/pyside6/tests/QtCore/qenum_designer_test.py83
5 files changed, 209 insertions, 6 deletions
diff --git a/sources/pyside6/libpyside/dynamicqmetaobject.cpp b/sources/pyside6/libpyside/dynamicqmetaobject.cpp
index a3b718d83..54e814523 100644
--- a/sources/pyside6/libpyside/dynamicqmetaobject.cpp
+++ b/sources/pyside6/libpyside/dynamicqmetaobject.cpp
@@ -63,8 +63,8 @@ public:
int addProperty(const QByteArray &property, PyObject *data);
void addInfo(const QByteArray &key, const QByteArray &value);
void addInfo(const QMap<QByteArray, QByteArray> &info);
- void addEnumerator(const char *name, bool flag, bool scoped,
- const MetaObjectBuilder::EnumValues &entries);
+ QMetaEnumBuilder addEnumerator(const char *name, bool flag, bool scoped,
+ const MetaObjectBuilder::EnumValues &entries);
void removeProperty(int index);
const QMetaObject *update();
@@ -384,8 +384,9 @@ void MetaObjectBuilder::addEnumerator(const char *name, bool flag, bool scoped,
m_d->addEnumerator(name, flag, scoped, entries);
}
-void MetaObjectBuilderPrivate::addEnumerator(const char *name, bool flag, bool scoped,
- const MetaObjectBuilder::EnumValues &entries)
+QMetaEnumBuilder
+ MetaObjectBuilderPrivate::addEnumerator(const char *name, bool flag, bool scoped,
+ const MetaObjectBuilder::EnumValues &entries)
{
auto *builder = ensureBuilder();
int have_already = builder->indexOfEnumerator(name);
@@ -398,6 +399,7 @@ void MetaObjectBuilderPrivate::addEnumerator(const char *name, bool flag, bool s
for (const auto &item : entries)
enumbuilder.addKey(item.first, item.second);
m_dirty = true;
+ return enumbuilder;
}
void MetaObjectBuilderPrivate::removeProperty(int index)
@@ -686,6 +688,11 @@ void MetaObjectBuilderPrivate::parsePythonType(PyTypeObject *type)
auto ivalue = PyLong_AsSsize_t(value);
entries.push_back(std::make_pair(ckey, int(ivalue)));
}
- addEnumerator(name, isFlag, true, entries);
+ auto enumBuilder = addEnumerator(name, isFlag, true, entries);
+ QByteArray qualifiedName = ensureBuilder()->className() + "::"_ba + name;
+ auto metaType =
+ PySide::QEnum::createGenericEnumMetaType(qualifiedName,
+ reinterpret_cast<PyTypeObject *>(obEnumType));
+ enumBuilder.setMetaType(metaType);
}
}
diff --git a/sources/pyside6/libpyside/pysideqenum.cpp b/sources/pyside6/libpyside/pysideqenum.cpp
index 8fc33d5db..70dd1d6da 100644
--- a/sources/pyside6/libpyside/pysideqenum.cpp
+++ b/sources/pyside6/libpyside/pysideqenum.cpp
@@ -4,6 +4,7 @@
#include "pysideqenum.h"
#include <autodecref.h>
+#include <sbkconverter.h>
#include <sbkenum.h>
#include <sbkstaticstrings.h>
#include <sbkstring.h>
@@ -11,6 +12,8 @@
#include <map>
#include <QtCore/qmetatype.h>
+#include <QtCore/qdebug.h>
+#include <QtCore/qlist.h>
///////////////////////////////////////////////////////////////
//
@@ -101,6 +104,59 @@ static bool is_module_code()
} // extern "C"
+// Helper code for dynamically creating QMetaType's for @QEnum
+
+template <class UnderlyingInt>
+static void defaultCtr(const QtPrivate::QMetaTypeInterface *, void *addr)
+{
+ auto *i = reinterpret_cast<UnderlyingInt *>(addr);
+ *i = 0;
+}
+
+template <class UnderlyingInt>
+static void debugOp(const QtPrivate::QMetaTypeInterface *mti, QDebug &debug, const void *addr)
+{
+ const auto value = *reinterpret_cast<const UnderlyingInt *>(addr);
+ QDebugStateSaver saver(debug);
+ debug << mti->name << '(';
+ if constexpr (std::is_unsigned<UnderlyingInt>()) {
+ debug << Qt::showbase << Qt::hex;
+ } else {
+ if (value >= 0)
+ debug << Qt::showbase << Qt::hex;
+ }
+ debug << value << ')';
+}
+
+template <class UnderlyingInt>
+QMetaType createEnumMetaTypeHelper(const QByteArray &name)
+{
+ auto *mti = new QtPrivate::QMetaTypeInterface {
+ 1, // revision
+ ushort(std::alignment_of<UnderlyingInt>()),
+ sizeof(UnderlyingInt),
+ uint(QMetaType::fromType<UnderlyingInt>().flags() | QMetaType::IsEnumeration),
+ {}, // typeId
+ nullptr, // metaObjectFn
+ qstrdup(name.constData()),
+ defaultCtr<UnderlyingInt>,
+ nullptr, // copyCtr
+ nullptr, // moveCtr
+ nullptr, // dtor
+ QtPrivate::QEqualityOperatorForType<UnderlyingInt>::equals,
+ QtPrivate::QLessThanOperatorForType<UnderlyingInt>::lessThan,
+ debugOp<UnderlyingInt>,
+ nullptr, // dataStreamOut
+ nullptr, // dataStreamIn
+ nullptr // legacyRegisterOp
+ };
+
+ QMetaType metaType(mti);
+
+ metaType.id(); // enforce registration
+ return metaType;
+}
+
namespace PySide::QEnum {
static std::map<int, PyObject *> enumCollector;
@@ -210,7 +266,57 @@ QByteArray getTypeName(PyTypeObject *type)
? result : QByteArray{};
}
-} // namespace Shiboken::Enum
+using GenericEnumType = int;
+
+using GenericEnumTypeList = QList<PyTypeObject *>;
+
+Q_GLOBAL_STATIC(GenericEnumTypeList, genericEnumTypeList)
+
+} // namespace PySide::QEnum
+
+extern "C"
+{
+
+static void genericEnumPythonToCpp(PyObject *pyIn, void *cppOut)
+{
+ const auto value = static_cast<PySide::QEnum::GenericEnumType>(Shiboken::Enum::getValue(pyIn));
+ *reinterpret_cast<int *>(cppOut) = value;
+}
+
+static PythonToCppFunc isGenericEnumToCppConvertible(PyObject *pyIn)
+{
+
+ if (PySide::QEnum::genericEnumTypeList()->contains(Py_TYPE(pyIn)))
+ return genericEnumPythonToCpp;
+ return {};
+}
+
+static PyObject *genericEnumCppToPython(PyTypeObject *pyType, const void *cppIn)
+{
+ const auto value = *reinterpret_cast<const PySide::QEnum::GenericEnumType *>(cppIn);
+ return Shiboken::Enum::newItem(pyType, value);
+}
+
+} // extern "C"
+
+namespace PySide::QEnum
+{
+
+QMetaType createGenericEnumMetaType(const QByteArray &name, PyTypeObject *pyType)
+{
+ SbkConverter *converter = Shiboken::Conversions::createConverter(pyType,
+ genericEnumCppToPython);
+ Shiboken::Conversions::addPythonToCppValueConversion(converter,
+ genericEnumPythonToCpp,
+ isGenericEnumToCppConvertible);
+ Shiboken::Conversions::registerConverterName(converter, name.constData());
+ Shiboken::Enum::setTypeConverter(pyType, converter, nullptr);
+
+ genericEnumTypeList()->append(pyType);
+ return createEnumMetaTypeHelper<GenericEnumType>(name);
+}
+
+} // namespace PySide::QEnum
//
///////////////////////////////////////////////////////////////
diff --git a/sources/pyside6/libpyside/pysideqenum.h b/sources/pyside6/libpyside/pysideqenum.h
index cfc361004..edc15dc9e 100644
--- a/sources/pyside6/libpyside/pysideqenum.h
+++ b/sources/pyside6/libpyside/pysideqenum.h
@@ -12,6 +12,8 @@
#include <QtCore/qbytearray.h>
+QT_FORWARD_DECLARE_CLASS(QMetaType)
+
namespace PySide::QEnum {
// PYSIDE-957: Support the QEnum macro
@@ -25,6 +27,10 @@ PYSIDE_API void init();
// Ignore flags here; their underlying enums are of Python type flags anyways.
PYSIDE_API QByteArray getTypeName(PyTypeObject *type);
+// Create a QMetaType for a decorated Python enum (int), enabling
+// modification of properties by Qt Widgets Designer.
+QMetaType createGenericEnumMetaType(const QByteArray &name, PyTypeObject *pyType);
+
} // namespace PySide::QEnum
#endif
diff --git a/sources/pyside6/tests/QtCore/CMakeLists.txt b/sources/pyside6/tests/QtCore/CMakeLists.txt
index a74216ccc..e21e8c064 100644
--- a/sources/pyside6/tests/QtCore/CMakeLists.txt
+++ b/sources/pyside6/tests/QtCore/CMakeLists.txt
@@ -73,6 +73,7 @@ PYSIDE_TEST(qdate_test.py)
PYSIDE_TEST(qdir_test.py)
PYSIDE_TEST(qeasingcurve_test.py)
PYSIDE_TEST(qenum_test.py)
+PYSIDE_TEST(qenum_designer_test.py)
PYSIDE_TEST(qevent_test.py)
PYSIDE_TEST(qfileinfo_test.py)
PYSIDE_TEST(qfile_test.py)
diff --git a/sources/pyside6/tests/QtCore/qenum_designer_test.py b/sources/pyside6/tests/QtCore/qenum_designer_test.py
new file mode 100644
index 000000000..0f35bab16
--- /dev/null
+++ b/sources/pyside6/tests/QtCore/qenum_designer_test.py
@@ -0,0 +1,83 @@
+#!/usr/bin/python
+# Copyright (C) 2025 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+'''Test cases for QEnum and QFlags within Qt Widgets Designer'''
+
+import os
+import sys
+import unittest
+
+from enum import Enum, Flag
+
+from pathlib import Path
+sys.path.append(os.fspath(Path(__file__).resolve().parents[1]))
+from init_paths import init_test_paths
+init_test_paths(False)
+
+from PySide6.QtCore import QObject, Property, QEnum, QFlag
+
+
+class CustomWidgetBase(QObject):
+ @QEnum
+ class TestEnum(Enum):
+ EnumValue0 = 0
+ EnumValue1 = 1
+ EnumValue2 = 2
+ EnumValue3 = 3
+
+ @QFlag
+ class TestFlag(Flag):
+ FlagValue0 = 1
+ FlagValue1 = 2
+ FlagValue2 = 4
+ FlagValue3 = 8
+
+
+class CustomWidget(CustomWidgetBase):
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self._testEnum = CustomWidget.TestEnum.EnumValue1
+ self._testFlag = (CustomWidget.TestFlag.FlagValue0
+ | CustomWidget.TestFlag.FlagValue1)
+
+ def testEnum(self):
+ return self._testEnum
+
+ def setTestEnum(self, new_val):
+ self._testEnum = new_val
+
+ def getTestFlag(self):
+ return self._testFlag
+
+ def setTestFlag(self, new_val):
+ self._testFlag = new_val
+
+ testEnum = Property(CustomWidgetBase.TestEnum, testEnum, setTestEnum)
+ testFlag = Property(CustomWidgetBase.TestFlag, getTestFlag, setTestFlag)
+
+
+class TestDesignerEnum(unittest.TestCase):
+ """PYSIDE-2840: Test whether a custom widget with decorated enum/flag properties
+ allows for modifying the values from C++."""
+
+ def testEnum(self):
+ cw = CustomWidget()
+ # Emulate Qt Widgets Designer setting a property
+ cw.setProperty("testEnum", 3)
+ self.assertEqual(cw.testEnum, CustomWidget.TestEnum.EnumValue3)
+ # Emulate uic generated code
+ cw.setProperty("testEnum", CustomWidgetBase.TestEnum.EnumValue2)
+ self.assertEqual(cw.testEnum, CustomWidget.TestEnum.EnumValue2)
+
+ # Emulate Qt Widgets Designer setting a property
+ cw.setProperty("testFlag", 12)
+ self.assertEqual(cw.testFlag, (CustomWidget.TestFlag.FlagValue2
+ | CustomWidget.TestFlag.FlagValue3))
+ # Emulate uic generated code
+ cw.setProperty("testFlag", CustomWidgetBase.TestFlag.FlagValue1)
+ self.assertEqual(cw.testFlag, CustomWidget.TestFlag.FlagValue1)
+
+
+if __name__ == '__main__':
+ unittest.main()