1// Copyright (C) 2022 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qqmlsettings_p.h"
5
6#include <QtQml/qjsvalue.h>
7#include <QtQml/qqmlfile.h>
8#include <QtQml/qqmlinfo.h>
9
10#include <QtCore/qbasictimer.h>
11#include <QtCore/qcoreapplication.h>
12#include <QtCore/qcoreevent.h>
13#include <QtCore/qdebug.h>
14#include <QtCore/qhash.h>
15#include <QtCore/qloggingcategory.h>
16#include <QtCore/qpointer.h>
17#include <QtCore/qsettings.h>
18
19using namespace std::chrono_literals;
20
21QT_BEGIN_NAMESPACE
22
23/*!
24 \qmltype Settings
25//! \nativetype QQmlSettings
26 \inherits QtObject
27 \inqmlmodule QtCore
28 \since 6.5
29 \brief Provides persistent platform-independent application settings.
30
31 The Settings type provides persistent platform-independent application settings.
32
33 Users normally expect an application to remember its settings (window sizes
34 and positions, options, etc.) across sessions. The Settings type enables you
35 to save and restore such application settings with the minimum of effort.
36
37 Individual setting values are specified by declaring properties within a
38 Settings element. Only value types recognized by QSettings are supported.
39 The recommended approach is to use property aliases in order
40 to get automatic property updates both ways. The following example shows
41 how to use Settings to store and restore the geometry of a window.
42
43 \qml
44 import QtCore
45 import QtQuick
46
47 Window {
48 id: window
49
50 width: 800
51 height: 600
52
53 Settings {
54 property alias x: window.x
55 property alias y: window.y
56 property alias width: window.width
57 property alias height: window.height
58 }
59 }
60 \endqml
61
62 At first application startup, the window gets default dimensions specified
63 as 800x600. Notice that no default position is specified - we let the window
64 manager handle that. Later when the window geometry changes, new values will
65 be automatically stored to the persistent settings. The second application
66 run will get initial values from the persistent settings, bringing the window
67 back to the previous position and size.
68
69 A fully declarative syntax, achieved by using property aliases, comes at the
70 cost of storing persistent settings whenever the values of aliased properties
71 change. Normal properties can be used to gain more fine-grained control over
72 storing the persistent settings. The following example illustrates how to save
73 a setting on component destruction.
74
75 \qml
76 import QtCore
77 import QtQuick
78
79 Item {
80 id: page
81
82 state: settings.state
83
84 states: [
85 State {
86 name: "active"
87 // ...
88 },
89 State {
90 name: "inactive"
91 // ...
92 }
93 ]
94
95 Settings {
96 id: settings
97 property string state: "active"
98 }
99
100 Component.onDestruction: {
101 settings.state = page.state
102 }
103 }
104 \endqml
105
106 Notice how the default value is now specified in the persistent setting property,
107 and the actual property is bound to the setting in order to get the initial value
108 from the persistent settings.
109
110 \section1 Application Identifiers
111
112 Application specific settings are identified by providing application
113 \l {QCoreApplication::applicationName}{name},
114 \l {QCoreApplication::organizationName}{organization} and
115 \l {QCoreApplication::organizationDomain}{domain}, or by specifying
116 \l location.
117
118 \code
119 #include <QGuiApplication>
120 #include <QQmlApplicationEngine>
121
122 int main(int argc, char *argv[])
123 {
124 QGuiApplication app(argc, argv);
125 app.setOrganizationName("Some Company");
126 app.setOrganizationDomain("somecompany.com");
127 app.setApplicationName("Amazing Application");
128
129 QQmlApplicationEngine engine("main.qml");
130 return app.exec();
131 }
132 \endcode
133
134 These are typically specified in C++ in the beginning of \c main(),
135 but can also be controlled in QML via the following properties:
136 \list
137 \li \l {Qt::application}{Qt.application.name},
138 \li \l {Qt::application}{Qt.application.organization} and
139 \li \l {Qt::application}{Qt.application.domain}.
140 \endlist
141
142 \section1 Categories
143
144 Application settings may be divided into logical categories by specifying
145 a category name via the \l category property. Using logical categories not
146 only provides a cleaner settings structure, but also prevents possible
147 conflicts between setting keys.
148
149 If several categories are required, use several Settings objects, each with
150 their own category:
151
152 \qml
153 Item {
154 id: panel
155
156 visible: true
157
158 Settings {
159 category: "OutputPanel"
160 property alias visible: panel.visible
161 // ...
162 }
163
164 Settings {
165 category: "General"
166 property alias fontSize: fontSizeSpinBox.value
167 // ...
168 }
169 }
170 \endqml
171
172 Instead of ensuring that all settings in the application have unique names,
173 the settings can be divided into unique categories that may then contain
174 settings using the same names that are used in other categories - without
175 a conflict.
176
177 \section1 Settings singleton
178
179 It's often useful to have settings available to every QML file as a
180 singleton. For an example of this, see the
181 \l {Qt Quick Controls - To Do List}{To Do List example}. Specifically,
182 \l {https://code.qt.io/cgit/qt/qtdeclarative.git/tree/examples/quickcontrols/ios/todolist/AppSettings.qml}
183 {AppSettings.qml} is the singleton, and in the
184 \l {https://code.qt.io/cgit/qt/qtdeclarative.git/tree/examples/quickcontrols/ios/todolist/CMakeLists.txt}
185 {CMakeLists.txt file},
186 the \c QT_QML_SINGLETON_TYPE property is set to \c TRUE for that file via
187 \c set_source_files_properties.
188
189 \section1 Notes
190
191 The current implementation is based on \l QSettings. This imposes certain
192 limitations, such as missing change notifications. Writing a setting value
193 using one instance of Settings does not update the value in another Settings
194 instance, even if they are referring to the same setting in the same category.
195
196 The information is stored in the system registry on Windows, and in XML
197 preferences files on \macos. On other Unix systems, in the absence of a
198 standard, INI text files are used. See \l QSettings documentation for
199 more details.
200
201 \sa QSettings
202*/
203
204using namespace Qt::StringLiterals;
205
206Q_STATIC_LOGGING_CATEGORY(lcQmlSettings, "qt.core.settings")
207
208static constexpr auto settingsWriteDelay = 500ms;
209
210class QQmlSettingsPrivate
211{
212 Q_DISABLE_COPY_MOVE(QQmlSettingsPrivate)
213 Q_DECLARE_PUBLIC(QQmlSettings)
214
215public:
216 QQmlSettingsPrivate() = default;
217 ~QQmlSettingsPrivate() = default;
218
219 QSettings *instance() const;
220
221 void init();
222 void reset();
223
224 void load();
225 void store();
226
227 void _q_propertyChanged();
228 QVariant readProperty(const QMetaProperty &property) const;
229
230 QQmlSettings *q_ptr = nullptr;
231 QBasicTimer timer;
232 bool initialized = false;
233 QString category = {};
234 QUrl location = {};
235 mutable QPointer<QSettings> settings = nullptr;
236 QHash<const char *, QVariant> changedProperties = {};
237};
238
239QSettings *QQmlSettingsPrivate::instance() const
240{
241 if (settings)
242 return settings;
243
244 QQmlSettings *q = const_cast<QQmlSettings *>(q_func());
245 settings = QQmlFile::isLocalFile(url: location)
246 ? new QSettings(QQmlFile::urlToLocalFileOrQrc(location), QSettings::IniFormat, q)
247 : new QSettings(q);
248
249 if (settings->status() != QSettings::NoError) {
250 // TODO: can't print out the enum due to the following error:
251 // error: C2666: 'QQmlInfo::operator <<': 15 overloads have similar conversions
252 qmlWarning(me: q) << "Failed to initialize QSettings instance. Status code is: " << int(settings->status());
253
254 if (settings->status() == QSettings::AccessError) {
255 QStringList missingIdentifiers = {};
256 if (QCoreApplication::organizationName().isEmpty())
257 missingIdentifiers.append(t: u"organizationName"_s);
258 if (QCoreApplication::organizationDomain().isEmpty())
259 missingIdentifiers.append(t: u"organizationDomain"_s);
260 if (QCoreApplication::applicationName().isEmpty())
261 missingIdentifiers.append(t: u"applicationName"_s);
262
263 if (!missingIdentifiers.isEmpty())
264 qmlWarning(me: q) << "The following application identifiers have not been set: " << missingIdentifiers;
265 }
266
267 return settings;
268 }
269
270 if (!category.isEmpty())
271 settings->beginGroup(prefix: category);
272
273 if (initialized)
274 q->d_func()->load();
275
276 return settings;
277}
278
279void QQmlSettingsPrivate::init()
280{
281 if (initialized)
282 return;
283 load();
284 initialized = true;
285 qCDebug(lcQmlSettings) << "QQmlSettings: stored at" << instance()->fileName();
286}
287
288void QQmlSettingsPrivate::reset()
289{
290 if (initialized && settings && !changedProperties.isEmpty())
291 store();
292 delete settings;
293}
294
295void QQmlSettingsPrivate::load()
296{
297 Q_Q(QQmlSettings);
298 const QMetaObject *mo = q->metaObject();
299 const int offset = QQmlSettings::staticMetaObject.propertyCount();
300 const int count = mo->propertyCount();
301
302 for (int i = offset; i < count; ++i) {
303 QMetaProperty property = mo->property(index: i);
304 const QString propertyName = QString::fromUtf8(utf8: property.name());
305
306 const QVariant previousValue = readProperty(property);
307 const QVariant currentValue = instance()->value(key: propertyName,
308 defaultValue: previousValue);
309
310 if (!currentValue.isNull() && (!previousValue.isValid()
311 || (currentValue.canConvert(targetType: previousValue.metaType())
312 && previousValue != currentValue))) {
313 property.write(obj: q, value: currentValue);
314 qCDebug(lcQmlSettings) << "QQmlSettings: load" << property.name() << "setting:" << currentValue << "default:" << previousValue;
315 }
316
317 // ensure that a non-existent setting gets written
318 // even if the property wouldn't change later
319 if (!instance()->contains(key: propertyName))
320 _q_propertyChanged();
321
322 // setup change notifications on first load
323 if (!initialized && property.hasNotifySignal()) {
324 static const int propertyChangedIndex = mo->indexOfSlot(slot: "_q_propertyChanged()");
325 QMetaObject::connect(sender: q, signal_index: property.notifySignalIndex(), receiver: q, method_index: propertyChangedIndex);
326 }
327 }
328}
329
330void QQmlSettingsPrivate::store()
331{
332 QHash<const char *, QVariant>::const_iterator it = changedProperties.constBegin();
333 while (it != changedProperties.constEnd()) {
334 instance()->setValue(key: QString::fromUtf8(utf8: it.key()), value: it.value());
335 qCDebug(lcQmlSettings) << "QQmlSettings: store" << it.key() << ":" << it.value();
336 ++it;
337 }
338 changedProperties.clear();
339}
340
341void QQmlSettingsPrivate::_q_propertyChanged()
342{
343 Q_Q(QQmlSettings);
344 const QMetaObject *mo = q->metaObject();
345 const int offset = QQmlSettings::staticMetaObject.propertyCount() ;
346 const int count = mo->propertyCount();
347 for (int i = offset; i < count; ++i) {
348 const QMetaProperty &property = mo->property(index: i);
349 const QVariant value = readProperty(property);
350 changedProperties.insert(key: property.name(), value);
351 qCDebug(lcQmlSettings) << "QQmlSettings: cache" << property.name() << ":" << value;
352 }
353 timer.start(duration: settingsWriteDelay, obj: q);
354}
355
356QVariant QQmlSettingsPrivate::readProperty(const QMetaProperty &property) const
357{
358 Q_Q(const QQmlSettings);
359 QVariant var = property.read(obj: q);
360 if (var.metaType() == QMetaType::fromType<QJSValue>())
361 var = var.value<QJSValue>().toVariant();
362 return var;
363}
364
365QQmlSettings::QQmlSettings(QObject *parent)
366 : QObject(parent), d_ptr(new QQmlSettingsPrivate)
367{
368 Q_D(QQmlSettings);
369 d->q_ptr = this;
370}
371
372QQmlSettings::~QQmlSettings()
373{
374 Q_D(QQmlSettings);
375 d->reset(); // flush pending changes
376}
377
378/*!
379 \qmlproperty string Settings::category
380
381 This property holds the name of the settings category.
382
383 Categories can be used to group related settings together.
384
385 \sa QSettings::group
386*/
387QString QQmlSettings::category() const
388{
389 Q_D(const QQmlSettings);
390 return d->category;
391}
392
393void QQmlSettings::setCategory(const QString &category)
394{
395 Q_D(QQmlSettings);
396 if (d->category == category)
397 return;
398 d->reset();
399 d->category = category;
400 if (d->initialized)
401 d->load();
402 Q_EMIT categoryChanged(arg: category);
403}
404
405/*!
406 \qmlproperty url Settings::location
407
408 This property holds the path to the settings file. If the file doesn't
409 already exist, it will be created.
410
411 If this property is empty (the default), then QSettings::defaultFormat()
412 will be used. Otherwise, QSettings::IniFormat will be used.
413
414 \sa QSettings::fileName, QSettings::defaultFormat, QSettings::IniFormat
415*/
416QUrl QQmlSettings::location() const
417{
418 Q_D(const QQmlSettings);
419 return d->location;
420}
421
422void QQmlSettings::setLocation(const QUrl &location)
423{
424 Q_D(QQmlSettings);
425 if (d->location == location)
426 return;
427 d->reset();
428 d->location = location;
429 if (d->initialized)
430 d->load();
431 Q_EMIT locationChanged(arg: location);
432}
433
434/*!
435 \qmlmethod var Settings::value(string key, var defaultValue)
436
437 Returns the value for setting \a key. If the setting doesn't exist,
438 returns \a defaultValue.
439
440 \sa QSettings::value
441*/
442QVariant QQmlSettings::value(const QString &key, const QVariant &defaultValue) const
443{
444 Q_D(const QQmlSettings);
445 return d->instance()->value(key, defaultValue);
446}
447
448/*!
449 \qmlmethod Settings::setValue(string key, var value)
450
451 Sets the value of setting \a key to \a value. If the key already exists,
452 the previous value is overwritten.
453
454 \sa QSettings::setValue
455*/
456void QQmlSettings::setValue(const QString &key, const QVariant &value)
457{
458 Q_D(const QQmlSettings);
459 d->instance()->setValue(key, value);
460 qCDebug(lcQmlSettings) << "QQmlSettings: setValue" << key << ":" << value;
461}
462
463/*!
464 \qmlmethod Settings::sync()
465
466 Writes any unsaved changes to permanent storage, and reloads any
467 settings that have been changed in the meantime by another
468 application.
469
470 This function is called automatically from QSettings's destructor and
471 by the event loop at regular intervals, so you normally don't need to
472 call it yourself.
473
474 \sa QSettings::sync
475*/
476void QQmlSettings::sync()
477{
478 Q_D(QQmlSettings);
479 d->instance()->sync();
480}
481
482void QQmlSettings::classBegin()
483{
484}
485
486void QQmlSettings::componentComplete()
487{
488 Q_D(QQmlSettings);
489 d->init();
490}
491
492void QQmlSettings::timerEvent(QTimerEvent *event)
493{
494 Q_D(QQmlSettings);
495 QObject::timerEvent(event);
496 if (!event->matches(timer: d->timer))
497 return;
498 d->timer.stop();
499 d->store();
500}
501
502QT_END_NAMESPACE
503
504#include "moc_qqmlsettings_p.cpp"
505

source code of qtdeclarative/src/core/qqmlsettings.cpp