aboutsummaryrefslogtreecommitdiffstats
path: root/src/labs/stylekit/qqstylekitdebug.cpp
diff options
context:
space:
mode:
authorRichard Moe Gustavsen <richard.gustavsen@qt.io>2025-11-19 15:49:47 +0100
committerRichard Moe Gustavsen <richard.gustavsen@qt.io>2025-11-27 12:22:49 +0100
commitc056aaa055187af7290d363d5ef02a4948cd8813 (patch)
treed5df750ffefe1d9dbb366177587a9b151d113c78 /src/labs/stylekit/qqstylekitdebug.cpp
parent8890b4a2d56ebdd51bf550d307f48d60ccf9a632 (diff)
StyleKit: add StyleKit to labs
This patch introduces a new styling API for Controls called StyleKit. StyleKit provides a higher-level, key–value–based approach to styling applications, serving as an alternative to working directly with the lower-level Templates API. The primary goal of StyleKit is to offer a unified API for styling both Controls and Widgets. The current Templates-based approach relies heavily on JavaScript, which makes it unsuitable for use with Widgets. This initial version supports Controls only; support for Widgets will be added in a future update. StyleKit is designed to make it easier for designers and UI developers to: - Focus on visual styling rather than Template logic (such as geometry, delegate positioning, and handle placement). - Allow style properties to propagate, enabling you to factor out common styling into shared control types and override only what differs in the more specific control types. - Style controls independently in each of their states, without needing nested ternary expressions to check state. - Define and apply multiple themes with minimal effort. - Provide different style variations depending on context. For example, styling a Switch differently when it appears inside a ToolBar. [ChangeLog][Qt labs] Introduced new QML module 'StyleKit'. StyleKit provides a flexible styling framework for Qt Quick Controls, enabling developers to define reusable styles and themes using a simple key-value property format. Change-Id: Iae25324486aea7a7b9b2ce52135327ec7e9b6f59 Reviewed-by: Mitch Curtis <mitch.curtis@qt.io>
Diffstat (limited to 'src/labs/stylekit/qqstylekitdebug.cpp')
-rw-r--r--src/labs/stylekit/qqstylekitdebug.cpp361
1 files changed, 361 insertions, 0 deletions
diff --git a/src/labs/stylekit/qqstylekitdebug.cpp b/src/labs/stylekit/qqstylekitdebug.cpp
new file mode 100644
index 0000000000..dcd191959d
--- /dev/null
+++ b/src/labs/stylekit/qqstylekitdebug.cpp
@@ -0,0 +1,361 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include "qqstylekitdebug_p.h"
+#include "qqstylekitcontrol_p.h"
+#include "qqstylekitcontrols_p.h"
+#include "qqstylekitstyle_p.h"
+#include "qqstylekittheme_p.h"
+
+QT_BEGIN_NAMESPACE
+
+const QQStyleKitPropertyGroup *QQStyleKitDebug::groupBeingRead = nullptr;
+QPointer<QQuickItem> QQStyleKitDebug::m_item;
+QString QQStyleKitDebug::m_filter;
+int QQStyleKitDebug::m_outputCount = 0;
+
+static const QChar kDot = '.'_L1;
+
+template <typename EnumType>
+QString QQStyleKitDebug::enumToString(EnumType enumValue)
+{
+ auto propertyMetaEnum = QMetaEnum::fromType<EnumType>();
+ return QString::fromUtf8(propertyMetaEnum.valueToKeys(quint64(enumValue)));
+}
+
+QString QQStyleKitDebug::objectName(const QObject *obj) {
+ QString str = QString::fromLatin1(obj->metaObject()->className());
+ int idx = str.indexOf("_QMLTYPE"_L1);
+ if (idx != -1)
+ str = str.left(idx);
+ else {
+ const QString prefix("QQStyleKit"_L1);
+ if (str.startsWith(prefix))
+ str = str.mid(prefix.length());
+ }
+ const QString name = obj->objectName();
+ if (!name.isEmpty())
+ str = str + "("_L1 + name + ")"_L1;
+ return str;
+}
+
+QString QQStyleKitDebug::stateToString(const QQSK::State state)
+{
+ const QStringList list = enumToString(state).split('|'_L1);
+ return "["_L1 + list.join(','_L1) + "]"_L1;
+}
+
+QString QQStyleKitDebug::styleReaderToString(const QQStyleKitReader *reader)
+{
+ return "StyleKitReader"_L1 + stateToString(reader->controlState());
+}
+
+QString QQStyleKitDebug::propertyPath(const QQStyleKitPropertyGroup *group, const PropertyPathId property)
+{
+ QString path = enumToString(property.property());
+ path[0] = path[0].toLower();
+ const QQStyleKitPropertyGroup *childGroup = group;
+ const int startIndex = QQStyleKitPropertyGroup::staticMetaObject.propertyOffset();
+
+ while (childGroup) {
+ if (childGroup->isControlProperties())
+ break;
+ const QQStyleKitPropertyGroup *parentGroup =
+ qobject_cast<const QQStyleKitPropertyGroup *>(childGroup->parent());
+ if (!parentGroup)
+ break;
+
+ // Resolve group name by inspecting which property in the parent group it belongs to
+ const QMetaObject* parentMeta = parentGroup->metaObject();
+ for (int i = startIndex; i < parentMeta->propertyCount(); ++i) {
+ const QMetaProperty prop = parentMeta->property(i);
+ const QMetaObject* typeMeta = QMetaType::fromName(prop.typeName()).metaObject();
+ if (!typeMeta || !typeMeta->inherits(&QQStyleKitPropertyGroup::staticMetaObject))
+ continue;
+ QObject *propGroup = prop.read(parentGroup).value<QQStyleKitPropertyGroup *>();
+ if (propGroup == childGroup) {
+ path.prepend(QString::fromUtf8(prop.name()) + kDot);
+ break;
+ }
+ }
+
+ childGroup = parentGroup;
+ }
+
+ return path;
+}
+
+QString QQStyleKitDebug::controlToString(const QQStyleKitControlProperties *control)
+{
+ const QObject *parentObj = control->parent();
+ if (!parentObj)
+ return "<no parent>"_L1;
+ auto *controls = qobject_cast<const QQStyleKitControls *>(parentObj);
+ if (!controls) {
+ return "<"_L1 + QString::fromUtf8(parentObj->metaObject()->className()) + ">"_L1;
+ }
+
+ const int startIndex = QQStyleKitControlProperties::staticMetaObject.propertyOffset();
+ const int endIndex = QQStyleKitControlProperties::staticMetaObject.propertyCount();
+
+ const QMetaObject* parentMeta = parentObj->metaObject();
+ for (int i = startIndex; i < endIndex; ++i) {
+ const QMetaProperty prop = parentMeta->property(i);
+ const QMetaObject* typeMeta = QMetaType::fromName(prop.typeName()).metaObject();
+ if (!typeMeta || !typeMeta->inherits(&QQStyleKitControl::staticMetaObject))
+ continue;
+
+ QObject *propObj = prop.read(parentObj).value<QObject *>();
+ if (propObj == control)
+ return QString::fromUtf8(prop.name());
+ }
+ return "<unknown control: no property found>"_L1;
+}
+
+QString QQStyleKitDebug::objectPath(const QQStyleKitControlProperties *properties, QObject *from)
+{
+ QString path;
+ const QObject *obj = properties;
+
+ while (obj) {
+ if (!path.isEmpty())
+ path.prepend(kDot);
+
+ if (auto *theme = qobject_cast<const QQStyleKitCustomTheme *>(obj)) {
+ path.prepend(theme->name() + kDot);
+ } else if (auto *theme = qobject_cast<const QQStyleKitTheme *>(obj)) {
+ // Note: only one theme is instantiated at a time
+ if (auto style = theme->style())
+ path.prepend(style->themeName());
+ else
+ path.prepend(objectName(obj));
+ } else if (auto *control = qobject_cast<const QQStyleKitControl *>(obj)) {
+ path.prepend(controlToString(control));
+ } else if (auto *reader = qobject_cast<const QQStyleKitReader *>(obj)) {
+ path.prepend(styleReaderToString(reader));
+ } else {
+ path.prepend(objectName(obj));
+ }
+
+ if (obj == from)
+ break;
+
+ obj = obj->parent();
+ }
+
+ return path;
+}
+
+void QQStyleKitDebug::notifyPropertyRead(
+ const PropertyPathId property,
+ const QQStyleKitControlProperties *storage,
+ const QQSK::State state,
+ const QVariant &value)
+{
+ Q_ASSERT(enabled());
+
+ const QQStyleKitControlProperties *reader = QQStyleKitDebug::groupBeingRead->controlProperties();
+ if (reader->subclass() == QQSK::Subclass::QQStyleKitState) {
+ /* The reader is in the UnfiedStyle, and not in the users application (which can happen
+ * when e.g resolving local bindings between properties in the style). Those are not
+ * interesting to print out when inspecting control-to-style mappings. Ignore. */
+ return;
+ }
+
+ if (!insideControl(reader)) {
+ // We should only debug reads that targets m_item. So return.
+ return;
+ }
+
+ const QString _readerPath = objectPath(reader, m_item);
+ const QString _readPropertyPath = propertyPath(QQStyleKitDebug::groupBeingRead, property);
+ const QString queriedPath = _readerPath + kDot +_readPropertyPath;
+
+ QString storagePath;
+ if (storage->subclass() == QQSK::Subclass::QQStyleKitReader) {
+ /* We read an interpolated value stored directly in the reader itself. While this
+ * can be interesting to print out whe debugging the styling engine itself, it
+ * comes across as noise when inspecting control-to-style mappings. Ignore. */
+#if 0
+ storagePath = "[local storage] "_L1;
+#else
+ return;
+#endif
+ } else {
+ const QString _controlPathInStyle = objectPath(storage, storage->style());
+ const QString _statePath = stateToString(state);
+ storagePath = _controlPathInStyle + _statePath;
+ }
+
+ QString valueString = value.toString();
+ if (!value.isValid()) // value was set, but probably to undefined
+ valueString = "<undefined>"_L1;
+ else if (valueString.isEmpty())
+ valueString = "<object>"_L1;
+
+ const QString output = queriedPath + " -> "_L1 + storagePath + " = "_L1 + valueString;
+
+ if (!QRegularExpression(m_filter).match(output).hasMatch())
+ return;
+
+ qDebug().nospace().noquote() << m_outputCount++ << " | [read] "_L1 << output;
+}
+
+void QQStyleKitDebug::notifyPropertyWrite(
+ const QQStyleKitPropertyGroup *group,
+ const QQSK::Property property,
+ const QQStyleKitControlProperties *storage,
+ const QQSK::State state,
+ const PropertyStorageId key,
+ const QVariant &value)
+{
+#if 1
+ Q_UNUSED(group);
+ Q_UNUSED(property);
+ Q_UNUSED(storage);
+ Q_UNUSED(state);
+ Q_UNUSED(key);
+ Q_UNUSED(value);
+#else
+ /* Note: in order to catch _all_ writes, we cannot depend on enabling writes from
+ * QML using a property, as that would resolve to 'true' too late. */
+ QString storagePath;
+ if (storage->subclass() == QQSK::Subclass::QQStyleKitReader) {
+ storagePath = "[local storage]"_L1;
+ } else {
+ const QString _controlPathInStyle = objectPath(storage, storage->style());
+ const QString _statePath = stateToString(state);
+ storagePath = _controlPathInStyle + _statePath;
+ }
+
+ QString valueString = value.toString();
+ if (!value.isValid()) // value was set, but probably to undefined
+ valueString = "<undefined>"_L1;
+ else if (valueString.isEmpty())
+ valueString = "<object>"_L1;
+
+ const QString path = propertyPath(group, property);
+ const QString output = storagePath + kDot + path + " (storage key:"_L1 + QString::number(key) + ") = "_L1 + valueString;
+
+ qDebug().nospace().noquote() << m_outputCount++ << " | [write] "_L1 << output;
+#endif
+}
+
+void QQStyleKitDebug::notifyPropertyNotResolved(const PropertyPathId property)
+{
+ const QQStyleKitControlProperties *reader = QQStyleKitDebug::groupBeingRead->controlProperties();
+ if (!insideControl(reader)) {
+ // We should only debug reads that targets m_item. So return.
+ return;
+ }
+
+ const QString _readerPath = objectPath(reader, m_item);
+ const QString _propertyPath = propertyPath(QQStyleKitDebug::groupBeingRead, property);
+ const QString queriedPath = _readerPath + kDot +_propertyPath;
+ const QString output = queriedPath + " -> <property not set>"_L1;
+
+ if (!QRegularExpression(m_filter).match(output).hasMatch())
+ return;
+
+ qDebug().nospace().noquote() << m_outputCount++ << " | [read] "_L1 << output;
+}
+
+void QQStyleKitDebug::trace(
+ const PropertyPathId property,
+ const QQStyleKitControlProperties *storage,
+ const QQSK::State state,
+ const PropertyStorageId key)
+{
+#if 1
+ Q_UNUSED(property);
+ Q_UNUSED(storage);
+ Q_UNUSED(state);
+ Q_UNUSED(key);
+#else
+ const QQStyleKitControlProperties *reader = QQStyleKitDebug::groupBeingRead->controlProperties();
+ if (reader->subclass() == QQSK::Subclass::QQStyleKitState) {
+ /* The reader is in the UnfiedStyle, and not in the users application (which can happen
+ * when e.g resolving local bindings between properties in the style). Those are not
+ * interesting to print out when inspecting control-to-style mappings. Ignore. */
+ return;
+ }
+
+ if (!insideControl(reader)) {
+ // We should only debug reads that targets m_item. So return.
+ return;
+ }
+
+ const QString _readerPath = objectPath(reader, m_item);
+ const QString _readPropertyPath = propertyPath(QQStyleKitDebug::groupBeingRead, property);
+ const QString queriedPath = _readerPath + kDot +_readPropertyPath;
+
+ QString storagePath;
+ if (storage->subclass() == QQSK::Subclass::QQStyleKitReader) {
+ /* We read an interpolated value stored directly in the reader itself. While this
+ * can be interesting to print out whe debugging the styling engine itself, it
+ * comes across as noise when inspecting control-to-style mappings. Ignore. */
+#if 0
+ storagePath = "[local storage]"_L1;
+#else
+ return;
+#endif
+ } else {
+ const QString _controlPathInStyle = objectPath(storage, storage->style());
+ const QString _statePath = stateToString(state);
+ storagePath = _controlPathInStyle + _statePath;
+ }
+
+ const QString output = queriedPath + ", checking "_L1 + storagePath + " (storage key:"_L1 + QString::number(key)+ ")"_L1;
+
+ if (!QRegularExpression(m_filter).match(output).hasMatch())
+ return;
+
+ qDebug().nospace().noquote() << m_outputCount++ << " | [trace] "_L1 << output;
+#endif
+}
+
+QQuickItem *QQStyleKitDebug::control() const
+{
+ return m_item;
+}
+
+void QQStyleKitDebug::setControl(QQuickItem *item)
+{
+ if (m_item == item)
+ return;
+
+ m_item = item;
+ emit controlChanged();
+}
+
+QString QQStyleKitDebug::filter() const
+{
+ return m_filter;
+}
+
+void QQStyleKitDebug::setFilter(const QString &filter)
+{
+ if (m_filter == filter)
+ return;
+
+ m_filter = filter;
+ emit filterChanged();
+}
+
+bool QQStyleKitDebug::insideControl(const QObject *child)
+{
+ if (!m_item)
+ return false;
+ const QObject *obj = child;
+ do {
+ if (obj == m_item)
+ return true;
+ obj = obj->parent();
+ } while (obj);
+ return false;
+}
+
+QT_END_NAMESPACE
+
+#include "moc_qqstylekitdebug_p.cpp"
+