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