Revision control

++ // 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"
++ #include "qqstylekitvariation_p.h"
++
++ QT_BEGIN_NAMESPACE
++
++ /*!
++ \qmltype StyleKitDebug
++ \inqmlmodule Qt.labs.StyleKit
++ \brief Traces how style properties are resolved for a control.
++
++ StyleKitDebug is a diagnostic in-app tool that logs style property reads to the
++ debug output. It is useful for understanding why a control ends up with a
++ particular appearance, especially in complex styles that involve multiple
++ \l {Theme}{themes} or \l {StyleVariation}{style variations}.
++
++ It is accessed through \l {StyleKit.debug}.
++
++ \note This tool is experimental. Enabling it will severely degrade performance and should only
++ be used for debugging. The output format may change in future versions.
++
++ To start tracing, assign the control you want to introspect to the \l control property:
++
++ \snippet StyleKitDebug.qml trace
++
++ Each resolved property is printed as a single line showing where the value
++ came from and what it resolved to:
++
++ \code
++ [read] StyleReader[Hovered].button.background.color -> Style.Theme(Dark).button[Hovered] = #add8e6
++ \endcode
++
++ The \c StyleReader in the output refers to the \l StyleReader that the \l Button
++ uses internally to read its style property values.
++
++ Tracing the property reads of a StyleReader can produce a large amount of output. Use \l filter to
++ limit the output to properties of interest.
++
++ \labs
++
++ \sa StyleKit::debug, {StyleKit Property Resolution}
++ */
++
++ /*!
++ \qmlproperty Item StyleKitDebug::control
++
++ The \l{The id Attribute}{id} of the \l Control or \l StyleReader to trace.
++
++ When set, StyleKit logs all style property reads
++ for this item to the debug output. Set to \c null to stop tracing.
++
++ \sa filter
++ */
++
++ /*!
++ \qmlproperty string StyleKitDebug::filter
++
++ A regular expression used to filter the debug output. Only lines matching
++ the pattern are printed. By default, all output is shown.
++
++ For example, to only trace the background color:
++
++ \code
++ StyleKit.debug.filter: "background.color"
++ \endcode
++
++ To trace all background colors, including properties
++ such as \l {DelegateStyle.color}{background.color},
++ \l {BorderStyle.color}{background.border.color},
++ \l {ImageStyle.color}{background.image.color},
++ and \l {ShadowStyle.color}{background.shadow.color},
++ use a regular expression:
++
++ \code
++ StyleKit.debug.filter: "background.*color"
++ \endcode
++
++ \sa control
++ */
++
++ 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) {
++ if (!obj->objectName().isEmpty())
++ return obj->objectName();
++
++ 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());
++ }
++
++ return str;
++ }
++
++ QString QQStyleKitDebug::stateToString(const QQSK::State state)
++ {
++ return "["_L1 + enumToString(state) + "]"_L1;
++ }
++
++ QString QQStyleKitDebug::styleReaderToString(const QQStyleKitReader *reader)
++ {
++ return "StyleKitReader"_L1 + stateToString(reader->controlState());
++ }
++
++ QString QQStyleKitDebug::controlToString(const QQStyleKitControlProperties *control)
++ {
++ if (!control->objectName().isEmpty())
++ return control->objectName();
++
++ 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 = QQStyleKitControls::staticMetaObject.propertyOffset();
++ const int endIndex = QQStyleKitControls::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>"_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
++ path.prepend("Theme("_L1 + theme->style()->m_effectiveThemeName + ")"_L1);
++ } else if (auto *variation = qobject_cast<const QQStyleKitVariation *>(obj)) {
++ path.prepend("StyleVariation("_L1 + variation->name() + ")"_L1);
++ } 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 QQStyleKitPropertyPath 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 = property.toString();
++ 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 when 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 = property.toString();
++ 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 QQStyleKitPropertyPath 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 = property.toString();
++ 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 QQStyleKitPropertyPath 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 = property.toString();
++ 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"
++