diff --git a/src/private/dquickcontrolpalette.cpp b/src/private/dquickcontrolpalette.cpp index a8ea3dcc0..50c42b5be 100644 --- a/src/private/dquickcontrolpalette.cpp +++ b/src/private/dquickcontrolpalette.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) #include #endif @@ -458,6 +459,7 @@ void DQuickControlColorSelector::setControl(QQuickItem *newControl) return; // Clean for old + disconnect(m_controlPaletteConnection); if (m_control) { m_control->disconnect(this); if (auto w = m_control->window()) @@ -469,14 +471,16 @@ void DQuickControlColorSelector::setControl(QQuickItem *newControl) if (m_control) { #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) auto palette = m_control->property("palette").value(); - connect(palette, &QQuickPalette::changed, this, &DQuickControlColorSelector::updateControlTheme); + if (palette) + m_controlPaletteConnection = connect(palette, &QQuickPalette::changed, + this, &DQuickControlColorSelector::scheduleControlThemeUpdate); #endif connect(m_control, &QObject::destroyed, this, [this] { m_controlWindow = nullptr; m_control = nullptr; }); if (m_control->metaObject()->indexOfSignal("paletteChanged()") != -1) { - connect(m_control, SIGNAL(paletteChanged()), this, SLOT(updateControlTheme())); + connect(m_control, SIGNAL(paletteChanged()), this, SLOT(scheduleControlThemeUpdate())); } if (m_control->metaObject()->indexOfSignal("hoveredChanged()") != -1) { connect(m_control, SIGNAL(hoveredChanged()), this, SLOT(updateControlState())); @@ -1010,6 +1014,20 @@ void DQuickControlColorSelector::updateFamilyForChildrenRecu(QQuickItem *parent) } } +void DQuickControlColorSelector::scheduleControlThemeUpdate() +{ + if (m_controlThemeUpdatePending) + return; + + m_controlThemeUpdatePending = true; + // Palette changes can originate from a binding to one of our colors. + // Wait until that binding has finished before notifying it again. + QMetaObject::invokeMethod(this, [this] { + m_controlThemeUpdatePending = false; + updateControlTheme(); + }, Qt::QueuedConnection); +} + void DQuickControlColorSelector::updateControlTheme() { if (!m_control) diff --git a/src/private/dquickcontrolpalette_p.h b/src/private/dquickcontrolpalette_p.h index fb1631989..49bf8d8af 100644 --- a/src/private/dquickcontrolpalette_p.h +++ b/src/private/dquickcontrolpalette_p.h @@ -352,6 +352,7 @@ class DQuickControlColorSelector : public QObject QByteArray findPalettePropertyName(const DQuickControlPalette *palette) const; void clearAndInheritParentProperties(); + Q_SLOT void scheduleControlThemeUpdate(); Q_SLOT void updateControlTheme(); Q_SLOT bool updateControlState(); Q_SLOT void updateAllColorProperties(); @@ -371,6 +372,8 @@ class DQuickControlColorSelector : public QObject void setFamilyPropertyParent(DQuickControlColorSelector *parent); QPointer m_control; + QMetaObject::Connection m_controlPaletteConnection; + bool m_controlThemeUpdatePending = false; QPointer m_controlWindow; QPointer m_superColorSelector; QPointer m_parentOfFamilyProperty; diff --git a/tests/ut_colorselector.cpp b/tests/ut_colorselector.cpp index 3a7547ebe..ce749ccd4 100644 --- a/tests/ut_colorselector.cpp +++ b/tests/ut_colorselector.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: LGPL-3.0-or-later @@ -8,6 +8,13 @@ #include #include #include +#include +#include +#include + +#include "dquickcontrolpalette_p.h" + +DQUICK_USE_NAMESPACE class ut_ColorSelector : public ::testing::Test { @@ -27,6 +34,10 @@ void ut_ColorSelector::TearDown() TEST_F(ut_ColorSelector, getColorFromProperty) { ControlHelper helper("qrc:/qml/ColorSelector_GetColorFromProperty.qml"); + const auto cleanup = qScopeGuard([&helper] { + delete helper.object; + helper.object = nullptr; + }); ASSERT_TRUE(helper.object); auto listView = helper.object->property("view").value(); @@ -41,7 +52,128 @@ TEST_F(ut_ColorSelector, getColorFromProperty) QQmlEngine &engine = helper.engine; engine.globalObject().setProperty("background", engine.newQObject(background)); - ThemeTypeGuard themeGurad(DGuiApplicationHelper::LightType); - Q_UNUSED(themeGurad); + ASSERT_TRUE(QQmlProperty::write(helper.object, "palette.window", QColor(Qt::white))); + QTest::qWaitFor([&] { + return background->property("color") + == engine.evaluate("background.color1.normal.common.color()").toVariant(); + }); ASSERT_EQ(background->property("color"), engine.evaluate("background.color1.normal.common.color()").toVariant()); } + +// Use a template control to exercise palette feedback without unrelated style +// assets. The palette is both an input and an output of the color selector. +static const char paletteFeedbackQml[] = +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + "import QtQuick\nimport QtQuick.Templates as T\n" +#else + "import QtQuick 2.11\nimport QtQuick.Templates 2.4 as T\n" +#endif +R"( +import org.deepin.dtk 1.0 as D + +T.Button { + property D.Palette textColor: D.Palette { + normal: ("red") + normalDark: ("blue") + } + palette.window: "white" + palette.windowText: D.ColorSelector.textColor +} +)"; + +TEST_F(ut_ColorSelector, paletteUpdatesAreDeferredAndCoalesced) +{ + QList warnings; + ControlHelper helper; + QObject::connect(&helper.engine, &QQmlEngine::warnings, &helper.engine, + [&warnings](const QList &errors) { warnings.append(errors); }); + ASSERT_TRUE(helper.setData(paletteFeedbackQml)); + const auto cleanup = qScopeGuard([&helper] { + delete helper.object; + helper.object = nullptr; + }); + auto selector = qobject_cast( + qmlAttachedPropertiesObject(helper.object)); + ASSERT_NE(selector, nullptr); + QCoreApplication::processEvents(); + QSignalSpy themeChanges(selector, &DQuickControlColorSelector::controlThemeChanged); + QSignalSpy colorChanges(selector, &DQuickControlColorSelector::colorPropertyChanged); + + ASSERT_TRUE(QQmlProperty::write(helper.object, "palette.window", QColor(Qt::black))); + ASSERT_TRUE(QQmlProperty::write(helper.object, "palette.window", QColor(Qt::white))); + EXPECT_EQ(themeChanges.count(), 0); + QCoreApplication::processEvents(); + EXPECT_EQ(themeChanges.count(), 0); + EXPECT_EQ(colorChanges.count(), 0); + + ASSERT_TRUE(QQmlProperty::write(helper.object, "palette.window", QColor(Qt::black))); + EXPECT_EQ(themeChanges.count(), 0); + QCoreApplication::processEvents(); + EXPECT_EQ(themeChanges.count(), 1); + EXPECT_EQ(QQmlProperty::read(helper.object, "palette.windowText").value(), QColor(Qt::blue)); + + // The palette writeback must settle rather than continuously queue updates. + const int settledChanges = colorChanges.count(); + for (int i = 0; i < 5; ++i) + QCoreApplication::processEvents(); + EXPECT_EQ(colorChanges.count(), settledChanges); + EXPECT_FALSE(selector->m_controlThemeUpdatePending); + for (const auto &warning : warnings) + EXPECT_FALSE(warning.description().contains("Binding loop")) << warning.toString().toStdString(); +} + +TEST_F(ut_ColorSelector, destroyWithPendingPaletteUpdate) +{ + ControlHelper helper; + ASSERT_TRUE(helper.setData(paletteFeedbackQml)); + QPointer selector = qmlAttachedPropertiesObject(helper.object); + ASSERT_TRUE(QQmlProperty::write(helper.object, "palette.window", QColor(Qt::black))); + delete helper.object; + helper.object = nullptr; + EXPECT_TRUE(selector.isNull()); + QCoreApplication::processEvents(); +} + +TEST_F(ut_ColorSelector, styledControlsPaletteFeedback) +{ + for (const QByteArray &type : {QByteArray("Button"), QByteArray("ToolButton"), QByteArray("MenuItem")}) { + SCOPED_TRACE(type.constData()); + QList warnings; + ControlHelper helper; + QObject::connect(&helper.engine, &QQmlEngine::warnings, &helper.engine, + [&warnings](const QList &errors) { warnings.append(errors); }); + ASSERT_TRUE(helper.setData("import QtQuick 2.11\nimport org.deepin.dtk 1.0 as D\nD." + + type + " { text: \"Palette feedback\" }")); + const auto cleanup = qScopeGuard([&helper] { + delete helper.object; + helper.object = nullptr; + }); + auto selector = qobject_cast( + qmlAttachedPropertiesObject(helper.object)); + ASSERT_NE(selector, nullptr); + QSignalSpy changes(selector, &DQuickControlColorSelector::colorPropertyChanged); + for (bool dark : {false, true}) { + ASSERT_TRUE(QQmlProperty::write(helper.object, "palette.window", + QColor(dark ? Qt::black : Qt::white))); + for (bool inactive : {true, false}) { + selector->setInactived(inactive); + for (int state = 0; state < 5; ++state) { + selector->setHovered(state == 1); + selector->setPressed(state == 2); + helper.object->setEnabled(state != 3); + helper.object->setProperty("checked", state == 4); + for (int i = 0; i < 5; ++i) + QCoreApplication::processEvents(); + EXPECT_EQ(QQmlProperty::read(helper.object, "palette.windowText").value(), + selector->property("textColor").value()); + const int settledChanges = changes.count(); + QCoreApplication::processEvents(); + EXPECT_EQ(changes.count(), settledChanges); + EXPECT_FALSE(selector->m_controlThemeUpdatePending); + } + } + } + for (const auto &warning : warnings) + EXPECT_FALSE(warning.description().contains("Binding loop")) << warning.toString().toStdString(); + } +}