diff --git a/src/private/dconfigwrapper.cpp b/src/private/dconfigwrapper.cpp index a492ffec7..4c3cd4c70 100644 --- a/src/private/dconfigwrapper.cpp +++ b/src/private/dconfigwrapper.cpp @@ -12,6 +12,7 @@ #include #include +#include #ifndef QT_DEBUG Q_LOGGING_CATEGORY(cfLog, "dtk.dsg.config" , QtInfoMsg); @@ -31,7 +32,8 @@ static DefalutProperties propertyAndValues(const QObject* obj) const int count = mo->propertyCount(); static const QStringList ReservedPropertyNames { "name", - "subpath" + "subpath", + "async" }; for (int i = offset; i < count; ++i) { @@ -63,7 +65,7 @@ class DConfigWrapperMetaObject : public QQmlOpenMetaObject { { const QByteArray &proName = name(index); qCDebug(cfLog) << "propertyWriteValue" << proName << value; - owner->impl->setValue(proName, value); + owner->setValue(proName, value); // Pre judgment returns the set value first. // If the value is different, `valueChanged` will be triggered again to update the value, // there are problems when the service is unavailable. @@ -73,7 +75,8 @@ class DConfigWrapperMetaObject : public QQmlOpenMetaObject { int metaCall(QObject *o, QMetaObject::Call _c, int _id, void **_a) override { if (_c == QMetaObject::ResetProperty) { - owner->impl->reset(name(_id - type()->propertyOffset())); + const auto key = name(_id - type()->propertyOffset()); + owner->resetValue(key); } return QQmlOpenMetaObject::metaCall(o, _c, _id, _a); @@ -111,8 +114,8 @@ QString DConfigWrapper::name() const void DConfigWrapper::setName(const QString &name) { - if (!m_name.isEmpty()) { - qWarning() << "name is existed." << m_name; + if (mo) { + qCWarning(cfLog) << name << ": This name can't be changed after initialized"; return; } @@ -130,8 +133,8 @@ QString DConfigWrapper::subpath() const void DConfigWrapper::setSubpath(const QString &subpath) { - if (!m_subpath.isEmpty()) { - qWarning() << "subpath is existed." << m_subpath; + if (mo) { + qCWarning(cfLog) << subpath << ": This subpath can't be changed after initialized"; return; } @@ -147,7 +150,7 @@ QStringList DConfigWrapper::keyList() const if (!impl) return QStringList(); - return impl->keyList(); + return configKeyList; } /*! @@ -159,7 +162,13 @@ bool DConfigWrapper::isValid() const if (!impl) return false; - return impl->isValid(); + // If is invalid, will delete the impl object + return true; +} + +bool DConfigWrapper::isDefaultValue(const QString &key) const +{ + return !nonDefaultValueKeyList.contains(key); } /*! @@ -168,10 +177,8 @@ bool DConfigWrapper::isValid() const */ QVariant DConfigWrapper::value(const QString &key, const QVariant &fallback) const { - if (!impl) - return fallback; - - return impl->value(key, fallback); + const auto &value = property(key.toLatin1().constData()); + return value.isValid() ? value : fallback; } /*! @@ -183,7 +190,13 @@ void DConfigWrapper::setValue(const QString &key, const QVariant &value) if (!impl) return; - impl->setValue(key, value); + if (m_async) { + QMetaObject::invokeMethod(impl.get(), [this, key, value] { + impl->setValue(key, value); + }); + } else { + impl->setValue(key, value); + } } void DConfigWrapper::resetValue(const QString &key) @@ -191,7 +204,13 @@ void DConfigWrapper::resetValue(const QString &key) if (!impl) return; - impl->reset(key); + if (m_async) { + QMetaObject::invokeMethod(impl.get(), [this, key] { + impl->reset(key); + }); + } else { + impl->reset(key); + } } void DConfigWrapper::classBegin() @@ -199,6 +218,40 @@ void DConfigWrapper::classBegin() } +class Q_DECL_HIDDEN DConfigWrapperThread : public QThread { +public: + DConfigWrapperThread() + : QThread() + { + setObjectName("DConfigWrapperThread"); + moveToThread(this); + } + ~DConfigWrapperThread() override + { + quit(); + wait(); + } +}; + +static QThread *globalThread() { + static QThread *thread = nullptr; + if (!thread) { + thread = new DConfigWrapperThread(); + thread->start(); + } + return thread; +} + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +DTK_CORE_NAMESPACE::DThreadUtils *globalThreadUtils() { + static DTK_CORE_NAMESPACE::DThreadUtils *threadUtils = nullptr; + if (!threadUtils) { + threadUtils = new DTK_CORE_NAMESPACE::DThreadUtils(globalThread()); + } + return threadUtils; +} +#endif + /*! \brief Initialize `DConfig` and redirect method of property's get and set. `DConfig` can only be initialized after \property name and \property subpath initialization @@ -207,51 +260,149 @@ void DConfigWrapper::classBegin() */ void DConfigWrapper::componentComplete() { - impl = new DTK_CORE_NAMESPACE::DConfig(m_name, m_subpath, this); + Q_ASSERT(!impl); + + // Get the dynamic properties and previous values defined in qml. + // Muse before new DConfigWrapperMetaObject + initializeConfigs = propertyAndValues(this); + qCDebug(cfLog) << "Initialize Properties:" << initializeConfigs; + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + auto objectType = new QQmlOpenMetaObjectType(&DConfigWrapper::staticMetaObject); +#else + auto objectType = new QQmlOpenMetaObjectType(&DConfigWrapper::staticMetaObject, qmlEngine(this)); +#endif + + mo = new DConfigWrapperMetaObject(this, objectType); + mo->setCached(true); + + if (m_async) { + // Init properties + for (auto iter = initializeConfigs.begin(); iter != initializeConfigs.end(); iter++) { + mo->setValue(iter.key(), iter.value()); + } + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + globalThreadUtils()->run(this, &DConfigWrapper::initializeProperties); +#else + QMetaObject::invokeMethod(globalThread(), [this] { + initializeProperties(); + }); +#endif + } else { + initializeProperties(); + } +} + +template +typename std::result_of::type(Args...)>::type +callInGuiThread(DConfigWrapper *wrapper, Fun fun, Args&&... args) { + if (QThread::currentThread() == qApp->thread()) { + return fun(std::forward(args)...); + } + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + return DThreadUtils::gui().exec(wrapper, fun, std::forward(args)...); +#else + return DThreadUtil::runInMainThread(wrapper, fun, std::forward(args)...); +#endif +} + +// in config thread, don't set the member variable directly. +// Must ensure the member variable is set in the main thread. +// So there have a "const" flag. +void DConfigWrapper::initializeProperties() const +{ + auto impl = new DTK_CORE_NAMESPACE::DConfig(m_name, m_subpath); if (!impl->isValid()) { qCWarning(cfLog) << QString("create dconfig failed, valid:%1, name:%2, subpath:%3, backend:%4") - .arg(impl->isValid()) - .arg(impl->name()) - .arg(impl->subpath()) - .arg(impl->backendName()); - impl->deleteLater(); + .arg(impl->isValid()) + .arg(impl->name()) + .arg(impl->subpath()) + .arg(impl->backendName()); + delete impl; impl = nullptr; return; } qInfo() << QString("create dconfig successful, valid:%1, name:%2, subpath:%3, backend:%4") - .arg(impl->isValid()) - .arg(impl->name()) - .arg(impl->subpath()) - .arg(impl->backendName()); + .arg(impl->isValid()) + .arg(impl->name()) + .arg(impl->subpath()) + .arg(impl->backendName()); + + const auto keyList = impl->keyList(); + QStringList nonDefaultValueKeyList; + for (const auto &key : keyList) { + if (!impl->isDefaultValue(key)) { + nonDefaultValueKeyList.append(key); + } + } - // Get the dynamic properties and previous values defined in qml. - const DefalutProperties &properties = propertyAndValues(this); - qCDebug(cfLog) << "properties" << properties; + auto wrapper = const_cast(this); + callInGuiThread(wrapper, [wrapper, keyList, nonDefaultValueKeyList, impl] { + wrapper->impl.reset(impl); + wrapper->configKeyList = keyList; + wrapper->nonDefaultValueKeyList = nonDefaultValueKeyList; + }); + + for (const auto &key : keyList) { + const QVariant currentValue = callInGuiThread(wrapper, [wrapper, key] { + return wrapper->property(key.toLocal8Bit()); + }); + + const auto initialValue = initializeConfigs.value(key.toLocal8Bit()); + if (currentValue.isValid() && currentValue != initialValue) { + // This key has been set value in QML by user, so we should update the value to DConfig. + qCDebug(cfLog) << "Update value from user on initialize, key:" << key + << "value:" << currentValue << "initialize value:" << initialValue + << "config side value:" << impl->value(key, QVariant()); + impl->setValue(key, currentValue); + } else { + // Must fallback to the initial value, in the sync mode, the DConfigWrapperMetaObject's + // properties is not initialize. + const auto value = impl->value(key, initialValue); + callInGuiThread(wrapper, [wrapper, key, value] { + if (value.isValid()) + wrapper->mo->setValue(key.toLocal8Bit(), value); + }); + } + } -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - auto objectType = new QQmlOpenMetaObjectType(&DConfigWrapper::staticMetaObject, qmlEngine(this)); -#else - auto objectType = new QQmlOpenMetaObjectType(&DConfigWrapper::staticMetaObject); -#endif - auto mo = new DConfigWrapperMetaObject(this, objectType); - mo->setCached(true); + // Using QueuedConnection because impl->setValue maybe emit sync signal in `propertyWriteValue`. + connect(impl, &DTK_CORE_NAMESPACE::DConfig::valueChanged, wrapper, [wrapper, impl](const QString &key) { + const QByteArray &propName = key.toLocal8Bit(); + + qCDebug(cfLog) << "update value from DConfig by 'valueChanged', key:" << propName; + const auto value = impl->value(propName, QVariant()); + const bool isDefault = impl->isDefaultValue(propName); + callInGuiThread(wrapper, [wrapper, propName, value, isDefault] { + if (isDefault) { + wrapper->nonDefaultValueKeyList.removeOne(propName); + } else if (!wrapper->nonDefaultValueKeyList.contains(propName)) { + wrapper->nonDefaultValueKeyList.append(propName); + } + if (value.isValid()) + wrapper->mo->setValue(propName, value); + }); + + QMetaObject::invokeMethod(wrapper, [wrapper, key] { + Q_EMIT wrapper->valueChanged(key); + }); + }, Qt::DirectConnection); + + QMetaObject::invokeMethod(wrapper, &DConfigWrapper::initialized); +} - for (auto iter = properties.begin(); iter != properties.end(); iter++) { - // it's need to emit signal, because other qml object maybe read the old value - // when binding the property before the component completed, also it has a performance problem. - // sync backend's value to `Wrapper`, we only use Wrapper's value(defined in qml) as fallback value. - mo->setValue(iter.key(), impl->value(iter.key(), iter.value())); - } +bool DConfigWrapper::async() const +{ + return m_async; +} - // Using QueuedConnection because impl->setValue maybe emit sync signal in `propertyWriteValue`. - connect(impl, &DTK_CORE_NAMESPACE::DConfig::valueChanged, this, [this, mo, properties](const QString &key){ - const QByteArray &proName = key.toLocal8Bit(); - if (properties.contains(proName)) { - qCDebug(cfLog) << "update value from DConfig by 'valueChanged', key:" << proName; - mo->setValue(proName, impl->value(proName, properties.value(proName))); - } - Q_EMIT valueChanged(key); - }, Qt::QueuedConnection); +void DConfigWrapper::setAsync(bool newAsync) +{ + m_async = newAsync; + if (mo) + qCWarning(cfLog) << "Async can't be changed after initialized"; } diff --git a/src/private/dconfigwrapper_p.h b/src/private/dconfigwrapper_p.h index fa1d2c56b..0c1b3ac23 100644 --- a/src/private/dconfigwrapper_p.h +++ b/src/private/dconfigwrapper_p.h @@ -11,6 +11,7 @@ DCORE_BEGIN_NAMESPACE class DConfig; +class DThreadUtils; DCORE_END_NAMESPACE class DConfigWrapperMetaObject; @@ -20,6 +21,7 @@ class DConfigWrapper : public QObject, public QQmlParserStatus Q_INTERFACES(QQmlParserStatus) Q_PROPERTY(QString name READ name WRITE setName) Q_PROPERTY(QString subpath READ subpath WRITE setSubpath) + Q_PROPERTY(bool async READ async WRITE setAsync) #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) QML_NAMED_ELEMENT(Config) #endif @@ -33,25 +35,38 @@ class DConfigWrapper : public QObject, public QQmlParserStatus QString subpath() const; void setSubpath(const QString &subpath); + bool async() const; + void setAsync(bool newAsync); + public Q_SLOTS: QVariant value(const QString &key, const QVariant &fallback = QVariant()) const; void setValue(const QString &key, const QVariant &value); void resetValue(const QString &key); QStringList keyList() const; bool isValid() const; + bool isDefaultValue(const QString &key) const; Q_SIGNALS: void valueChanged(const QString &key); + void initialized(); -public: +private: virtual void classBegin() override; virtual void componentComplete() override; -private: + void initializeProperties() const; + friend DConfigWrapperMetaObject; - DTK_CORE_NAMESPACE::DConfig *impl; + DConfigWrapperMetaObject *mo = nullptr; + std::unique_ptr impl; + QStringList configKeyList; + // If the key was set value, add it to the list + QStringList nonDefaultValueKeyList; + QMap initializeConfigs; + QString m_name; QString m_subpath; + bool m_async = false; Q_DISABLE_COPY(DConfigWrapper) }; diff --git a/src/private/dsettingscontainer.cpp b/src/private/dsettingscontainer.cpp index 2b40f60d8..86a64a2e7 100644 --- a/src/private/dsettingscontainer.cpp +++ b/src/private/dsettingscontainer.cpp @@ -277,7 +277,7 @@ void SettingsOption::setConfig(DConfigWrapper *config) if (propertyIndex < 0) { connect(m_config, &DConfigWrapper::valueChanged, this, [this](const QString &key){ if (key == m_key) { - setValue(m_config->value(key)); + setValue(m_config->value(key), false); m_valueInitialized = true; } }); @@ -305,17 +305,22 @@ SettingsOption *SettingsOption::qmlAttachedProperties(QObject *object) void SettingsOption::onConfigValueChanged() { - setValue(m_config->value(m_key)); + setValue(m_config->value(m_key), false); m_valueInitialized = true; } void SettingsOption::setValue(QVariant value) +{ + setValue(value, true); +} + +void SettingsOption::setValue(const QVariant &value, bool updateConfig) { if (value == m_value) return; m_value = value; - if (m_config) + if (updateConfig && m_config) m_config->setValue(m_key, value); Q_EMIT valueChanged(value); diff --git a/src/private/dsettingscontainer_p.h b/src/private/dsettingscontainer_p.h index 3b6bce1d5..36405c77e 100644 --- a/src/private/dsettingscontainer_p.h +++ b/src/private/dsettingscontainer_p.h @@ -54,6 +54,8 @@ private Q_SLOTS: void onConfigValueChanged(); private: + void setValue(const QVariant &value, bool updateConfig); + QString m_key; QString m_name; QVariant m_value; diff --git a/tests/qml/Config.qml b/tests/qml/Config.qml index e7008bf39..9a8b2322e 100644 --- a/tests/qml/Config.qml +++ b/tests/qml/Config.qml @@ -12,6 +12,7 @@ Item { id: exampleConfig name: "example" subpath: "" + async: false property string key2 property string key3 : "1" onKey3Changed: control.key3Changed() diff --git a/tests/ut_dconfigwrapper.cpp b/tests/ut_dconfigwrapper.cpp index 4f4fa1f66..3894ed718 100644 --- a/tests/ut_dconfigwrapper.cpp +++ b/tests/ut_dconfigwrapper.cpp @@ -38,6 +38,7 @@ TEST_F(ut_DConfigWrapper, componentComplete) { QScopedPointer config(new DConfigWrapper()); + config->setAsync(false); config->classBegin(); config->setName("example"); config->setSubpath(""); @@ -50,6 +51,7 @@ TEST_F(ut_DConfigWrapper, setValue) { QScopedPointer config(new DConfigWrapper()); + config->setAsync(false); config->classBegin(); config->setName("example"); config->componentComplete();