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 | |
15 | QT_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 | |
198 | using namespace Qt::StringLiterals; |
199 | |
200 | Q_LOGGING_CATEGORY(lcQmlSettings, "qt.core.settings" ) |
201 | |
202 | static constexpr const int settingsWriteDelay = 500; |
203 | |
204 | class QQmlSettingsPrivate |
205 | { |
206 | Q_DISABLE_COPY_MOVE(QQmlSettingsPrivate) |
207 | Q_DECLARE_PUBLIC(QQmlSettings) |
208 | |
209 | public: |
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 | |
233 | QSettings *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 | |
271 | void QQmlSettingsPrivate::init() |
272 | { |
273 | if (initialized) |
274 | return; |
275 | load(); |
276 | initialized = true; |
277 | qCDebug(lcQmlSettings) << "QQmlSettings: stored at" << instance()->fileName(); |
278 | } |
279 | |
280 | void QQmlSettingsPrivate::reset() |
281 | { |
282 | if (initialized && settings && !changedProperties.isEmpty()) |
283 | store(); |
284 | delete settings; |
285 | } |
286 | |
287 | void 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 | |
326 | void 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 | |
337 | void 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 | |
354 | QVariant 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 | |
363 | QQmlSettings::QQmlSettings(QObject *parent) |
364 | : QObject(parent), d_ptr(new QQmlSettingsPrivate) |
365 | { |
366 | Q_D(QQmlSettings); |
367 | d->q_ptr = this; |
368 | } |
369 | |
370 | QQmlSettings::~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 | */ |
385 | QString QQmlSettings::category() const |
386 | { |
387 | Q_D(const QQmlSettings); |
388 | return d->category; |
389 | } |
390 | |
391 | void 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 | */ |
414 | QUrl QQmlSettings::location() const |
415 | { |
416 | Q_D(const QQmlSettings); |
417 | return d->location; |
418 | } |
419 | |
420 | void 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 | */ |
440 | QVariant 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 | */ |
454 | void 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 | */ |
474 | void QQmlSettings::sync() |
475 | { |
476 | Q_D(QQmlSettings); |
477 | d->instance()->sync(); |
478 | } |
479 | |
480 | void QQmlSettings::classBegin() |
481 | { |
482 | } |
483 | |
484 | void QQmlSettings::componentComplete() |
485 | { |
486 | Q_D(QQmlSettings); |
487 | d->init(); |
488 | } |
489 | |
490 | void 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 | |
501 | QT_END_NAMESPACE |
502 | |
503 | #include "moc_qqmlsettings_p.cpp" |
504 | |