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

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