2
\$\begingroup\$

Qt.labs.settings 1.0 module from QtDeclarative (depricated) allows to use QSettings to make some of QML properties persistent. It is quite outdated and inconvenient. E.g. to get access to value x in group a and to value y in subgroup b of group a one need to specify full path in second case:

Settings {
    category: "a"
    property int x
}
Settings {
    category: "a/b"
    property int y
}

For deeper nesting it require more boilerplate code.

It is not big deal, but it would be better, if tree structure of QSettings would be explicitly exposed in QML:

Settings {
    prefix: "a"
    property int x
    SettingsGroup {
        prefix: "b"
        property int y
    }
}

To meet these ideas I reinvent Settings and SettingsGroup QML types. Both of them have (maybe empty) property prefix to specify (sub)group name. Settings default value is QML list of SettingsGroup. SettingsGroup default value is QML list of SettingsGroup.

After some property of the "settings tree" is changed, notification signal triggers timer and after a period of time (default 500 ms) all values of changed properties are flushed on QSettings' storage if no new property changing notification signal is emitted. This allows to perform batching of save operation in case of changing a multiple of mutually dependent QML properties. On destruction all values of changed properties are also flushed.

// settings.hpp
#pragma once

#include <QtQml>

class SettingsGroup
        : public QObject
{

    Q_OBJECT

    Q_PROPERTY(QString prefix MEMBER prefix NOTIFY prefixChanged)
    Q_PROPERTY(QQmlListProperty< SettingsGroup > groups READ getGroups)

    Q_CLASSINFO("DefaultProperty", "groups")

public :

    explicit SettingsGroup(QObject * const root = Q_NULLPTR);

    ~SettingsGroup() Q_DECL_OVERRIDE Q_DECL_EQ_DEFAULT;

    QQmlListProperty< SettingsGroup > getGroups();

Q_SIGNALS :

    void prefixChanged(QString prefix);

private :

    QHash< int, const char * > propertyNames;
    QHash< const char *, QVariant > changedProperties; // it is essential to save property value, because in destructor property("propertyName") became null

protected :

    void init(QSettings & settings, QTimer * const timer);

    void store(QSettings & settings);

    void invalidate();

private Q_SLOTS :

    void onPropertyChanged();

private :

    QString prefix;
    QVector< SettingsGroup * > groups;

};

class Settings
        : public SettingsGroup
        , public QQmlParserStatus
{

    Q_OBJECT

    Q_INTERFACES(QQmlParserStatus)

public :

    explicit Settings(QObject * const parent = Q_NULLPTR);

    ~Settings() Q_DECL_OVERRIDE;

private :

    void classBegin() Q_DECL_OVERRIDE;
    void componentComplete() Q_DECL_OVERRIDE;

    QSettings settings;

};

//settings.cpp
#include "settings.hpp"

SettingsGroup::SettingsGroup(QObject * const parent)
    : QObject{parent}
{ ; }

QQmlListProperty< SettingsGroup > SettingsGroup::getGroups()
{
    using SettingsList = QQmlListProperty< SettingsGroup >;
    const SettingsList::AppendFunction appendFunction = [] (SettingsList * listProperty, SettingsGroup * value)
    {
        const auto self = qobject_cast< SettingsGroup * >(listProperty->object);
        Q_ASSERT(self);
        if (qobject_cast< Settings * >(self)) {
            return;
        }
    #ifdef QT_DEBUG
            const auto prefix = value->property("prefix");
            for (const auto group : self->groups) {
                if (group->property("prefix") == prefix) {
                    QQmlError error;
                    error.setMessageType(QtWarningMsg);
                    error.setUrl(QString::fromLatin1(Q_FUNC_INFO));
                    error.setObject(value);
                    error.setDescription(tr("Group with prefix '%1' is already listed in '%2'").arg(prefix.toString(), self->prefix));
                    qmlWarning(self, error);
                }
            }
    #endif
        self->groups.append(value);
    };
    const SettingsList::CountFunction countFunction = [] (SettingsList * listProperty) -> int
    {
        const auto self = qobject_cast< SettingsGroup * >(listProperty->object);
        Q_ASSERT(self);
        return self->groups.count();
    };
    const SettingsList::AtFunction atFunction = [] (SettingsList * listProperty, int index) -> SettingsGroup *
    {
        const auto self = qobject_cast< SettingsGroup * >(listProperty->object);
        Q_ASSERT(self);
        return self->groups.at(index);
    };
    const SettingsList::ClearFunction clearFunction = [] (SettingsList * listProperty)
    {
        const auto self = qobject_cast< SettingsGroup * >(listProperty->object);
        Q_ASSERT(self);
        self->groups.clear();
    };
    return {this, Q_NULLPTR, appendFunction, countFunction, atFunction, clearFunction};
}

void SettingsGroup::init(QSettings & settings, QTimer * const timer)
{
    if (!prefix.isEmpty()) {
        settings.beginGroup(prefix);
    }
    {
        const auto onPrefixChanged = [this, timer]
        {
            invalidate();
            timer->start();
        };
        connect(this, &SettingsGroup::prefixChanged, timer, onPrefixChanged);
        static const int offset = staticMetaObject.propertyCount();
        const auto mo = metaObject();
        const int count = mo->propertyCount();
        for (int i = offset; i < count; ++i) {
            const auto mp = mo->property(i);
            auto oldValue = mp.read(this);
            if (mp.userType() == qMetaTypeId< QJSValue >()) {
                oldValue = oldValue.value< QJSValue >().toVariant();
            }
            const auto propertyName = QString::fromLatin1(mp.name());
            if (settings.contains(propertyName)) {
                const auto newValue = settings.value(propertyName, oldValue);
                if (!newValue.isNull() && (!oldValue.isValid() || newValue.canConvert(oldValue.type())) && (oldValue != newValue)) {
                    if (!mp.write(this, newValue)) {
                        settings.setValue(propertyName, oldValue); // force renew the type of saved value
                    }
                }
            } else {
                settings.setValue(propertyName, oldValue);
            }
            if (mp.hasNotifySignal()) {
                const int signalIndex = mp.notifySignalIndex();
                if (!propertyNames.contains(signalIndex)) {
                    propertyNames.insert(signalIndex, mp.name());
                    static const int onPropertyChangedIndex = staticMetaObject.indexOfSlot("onPropertyChanged()");
                    QMetaObject::connect(this, signalIndex, this, onPropertyChangedIndex);
                    static const int timerStartIndex = timer->staticMetaObject.indexOfSlot("start()");
                    QMetaObject::connect(this, signalIndex, timer, timerStartIndex);
                }
            }
        }
    }
    for (const auto group : groups) {
        group->init(settings, timer);
    }
    if (!prefix.isEmpty()) {
        settings.endGroup();
    }
}

void SettingsGroup::store(QSettings & settings)
{
    if (!prefix.isEmpty()) {
        settings.beginGroup(prefix);
    }
    if (!changedProperties.isEmpty()) {
        QHashIterator changedProperty{changedProperties};
        while (changedProperty.hasNext()) {
            changedProperty.next();
            settings.setValue(QString::fromLatin1(changedProperty.key()), changedProperty.value());
        }
        changedProperties.clear();
    }
    for (const auto group : groups) {
        group->store(settings);
    }
    if (!prefix.isEmpty()) {
        settings.endGroup();
    }
}

void SettingsGroup::invalidate()
{
    for (const auto & propertyName : propertyNames) {
        changedProperties.insert(propertyName, property(propertyName));
    }
    for (const auto group : groups) {
        group->invalidate();
    }
}

void SettingsGroup::onPropertyChanged()
{
    Q_ASSERT(sender() == this);
    const int signalIndex = senderSignalIndex();
    Q_ASSERT(!(signalIndex < 0));
    Q_ASSERT(propertyNames.contains(signalIndex));
    const auto propertyName = propertyNames[signalIndex];
    changedProperties.insert(propertyName, property(propertyName));
}

Settings::Settings(QObject * const parent)
    : SettingsGroup{parent}
{ ; }

Settings::~Settings()
{
    store(settings);
}

void Settings::classBegin()
{
    // no op
}

void Settings::componentComplete()
{
    const auto timer = ::new QTimer{this};
    Q_CHECK_PTR(timer);
    timer->setSingleShot(true);
    timer->setInterval(settings.value("saveDelay", 500).toInt());
    connect(timer, &QTimer::timeout, this, [this] { store(settings); settings.sync(); });
    init(settings, timer);
    settings.sync();
}

It is possible to further improve Settings type to use not only default-constructed QSettings object (in fact it was a main goal). It seems, that it is currently impossible to correctly add new SettingsGroup objects into list default properties of existent Settings/SettingsGroup objects dynamically.

What are architectural misconceptions or style errors? Can they be improved?

(tested on Qt 5.9.1, g++ (Ubuntu 7.1.0-5ubuntu2~16.04) 7.1.0)

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.