Settings: unify duplicated code
[lxde/liblxqt.git] / lxqtsettings.cpp
CommitLineData
4ef6cee9 1/* BEGIN_COMMON_COPYRIGHT_HEADER
c4af778e 2 * (c)LGPL2+
4ef6cee9 3 *
b9223fe7 4 * LXQt - a lightweight, Qt based, desktop toolset
4ef6cee9 5 * http://razor-qt.org
6 *
7 * Copyright: 2010-2011 Razor team
8 * Authors:
626f1f31 9 * Alexander Sokoloff <sokoloff.a@gmail.com>
4ef6cee9 10 * Petr Vanek <petr@scribus.info>
11 *
12 * This program or library is free software; you can redistribute it
13 * and/or modify it under the terms of the GNU Lesser General Public
14 * License as published by the Free Software Foundation; either
c4af778e 15 * version 2.1 of the License, or (at your option) any later version.
4ef6cee9 16 *
17 * This library is distributed in the hope that it will be useful,
18 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20 * Lesser General Public License for more details.
21
22 * You should have received a copy of the GNU Lesser General
23 * Public License along with this library; if not, write to the
24 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
25 * Boston, MA 02110-1301 USA
26 *
27 * END_COMMON_COPYRIGHT_HEADER */
28
82830dc2 29#include "lxqtsettings.h"
e6389bfc
HJYP
30#include <QDebug>
31#include <QEvent>
32#include <QDir>
33#include <QStringList>
34#include <QMutex>
35#include <QFileSystemWatcher>
36#include <QSharedData>
67dd270e 37#include <QTimerEvent>
4ef6cee9 38
e6389bfc 39#include <XdgDirs>
afd3b116 40
f05ba5af 41using namespace LXQt;
82830dc2 42
f05ba5af 43class LXQt::SettingsPrivate
4ef6cee9 44{
45public:
82830dc2 46 SettingsPrivate(Settings* parent):
1b34ac93
PK
47 mFileChangeTimer(0),
48 mAppChangeTimer(0),
49 mAddWatchTimer(0),
4ef6cee9 50 mParent(parent)
51 {
430b1ced
PK
52 // HACK: we need to ensure that the user (~/.config/lxqt/<module>.conf)
53 // exists to have functional mWatcher
54 if (!mParent->contains("__userfile__"))
55 {
56 mParent->setValue("__userfile__", true);
57 mParent->sync();
58 }
59 mWatcher.addPath(mParent->fileName());
60 QObject::connect(&(mWatcher), &QFileSystemWatcher::fileChanged, mParent, &Settings::_fileChanged);
4ef6cee9 61 }
62
de53bc2d
AS
63 QString localizedKey(const QString& key) const;
64
7ee52b88 65 QFileSystemWatcher mWatcher;
1b34ac93
PK
66 int mFileChangeTimer;
67 int mAppChangeTimer;
68 int mAddWatchTimer;
7ee52b88 69
4ef6cee9 70private:
82830dc2 71 Settings* mParent;
4ef6cee9 72};
73
74
f05ba5af 75LXQtTheme* LXQtTheme::mInstance = 0;
4ef6cee9 76
f05ba5af 77class LXQt::LXQtThemeData: public QSharedData {
4ef6cee9 78public:
f05ba5af 79 LXQtThemeData(): mValid(false) {}
4ef6cee9 80 QString loadQss(const QString& qssFile) const;
63a934d5 81 QString findTheme(const QString &themeName);
4ef6cee9 82
63a934d5
AS
83 QString mName;
84 QString mPath;
85 QString mPreviewImg;
86 bool mValid;
4ef6cee9 87
63a934d5 88};
4ef6cee9 89
4ef6cee9 90
f05ba5af 91class LXQt::GlobalSettingsPrivate
63a934d5
AS
92{
93public:
82830dc2 94 GlobalSettingsPrivate(GlobalSettings *parent):
2b0063a8
KS
95 mParent(parent),
96 mThemeUpdated(0ull)
4ef6cee9 97 {
63a934d5 98
4ef6cee9 99 }
100
82830dc2 101 GlobalSettings *mParent;
63a934d5 102 QString mIconTheme;
f05ba5af 103 QString mLXQtTheme;
3e771e4e 104 qlonglong mThemeUpdated;
63a934d5
AS
105
106};
4ef6cee9 107
108
109/************************************************
4ef6cee9 110
111 ************************************************/
82830dc2 112Settings::Settings(const QString& module, QObject* parent) :
41e3762a 113 QSettings("lxqt", module, parent),
82830dc2 114 d_ptr(new SettingsPrivate(this))
4ef6cee9 115{
4ef6cee9 116}
117
118
119/************************************************
120
121 ************************************************/
82830dc2 122Settings::Settings(const QString &fileName, QSettings::Format format, QObject *parent):
e6173915 123 QSettings(fileName, format, parent),
82830dc2 124 d_ptr(new SettingsPrivate(this))
e6173915 125{
e6173915
AS
126}
127
128
129/************************************************
130
131 ************************************************/
82830dc2 132Settings::Settings(const QSettings* parentSettings, const QString& subGroup, QObject* parent):
094599aa 133 QSettings(parentSettings->organizationName(), parentSettings->applicationName(), parent),
82830dc2 134 d_ptr(new SettingsPrivate(this))
4ef6cee9 135{
136 beginGroup(subGroup);
137}
138
139
140/************************************************
141
142 ************************************************/
82830dc2 143Settings::Settings(const QSettings& parentSettings, const QString& subGroup, QObject* parent):
094599aa 144 QSettings(parentSettings.organizationName(), parentSettings.applicationName(), parent),
82830dc2 145 d_ptr(new SettingsPrivate(this))
4ef6cee9 146{
147 beginGroup(subGroup);
148}
149
00f0a23a
AS
150
151/************************************************
152
153 ************************************************/
82830dc2 154Settings::~Settings()
4ef6cee9 155{
82830dc2 156 // because in the Settings::Settings(const QString& module, QObject* parent)
4ef6cee9 157 // constructor there is no beginGroup() called...
158 if (!group().isEmpty())
159 endGroup();
160
161 delete d_ptr;
162}
163
82830dc2 164bool Settings::event(QEvent *event)
0de9564b
PV
165{
166 if (event->type() == QEvent::UpdateRequest)
167 {
1b34ac93
PK
168 // delay the settingsChanged* signal emitting for:
169 // - checking in _fileChanged
170 // - merging emitting the signals
171 if(d_ptr->mAppChangeTimer)
172 killTimer(d_ptr->mAppChangeTimer);
173 d_ptr->mAppChangeTimer = startTimer(100);
0de9564b 174 }
67dd270e
HJYP
175 else if (event->type() == QEvent::Timer)
176 {
1b34ac93
PK
177 const int timer = static_cast<QTimerEvent*>(event)->timerId();
178 killTimer(timer);
179 if (timer == d_ptr->mFileChangeTimer)
67dd270e 180 {
1b34ac93 181 d_ptr->mFileChangeTimer = 0;
67dd270e 182 fileChanged(); // invoke the real fileChanged() handler.
1b34ac93
PK
183 } else if (timer == d_ptr->mAppChangeTimer)
184 {
185 d_ptr->mAppChangeTimer = 0;
186 // do emit the signals
187 emit settingsChangedByApp();
188 emit settingsChanged();
189 } else if (timer == d_ptr->mAddWatchTimer)
190 {
191 d_ptr->mAddWatchTimer = 0;
192 //try to re-add filename for watching
193 addWatchedFile(fileName());
67dd270e
HJYP
194 }
195 }
0de9564b
PV
196
197 return QSettings::event(event);
198}
199
82830dc2 200void Settings::fileChanged()
4ef6cee9 201{
7ee52b88 202 sync();
1b34ac93 203 emit settingsChangedFromExternal();
7ee52b88 204 emit settingsChanged();
4ef6cee9 205}
206
67dd270e
HJYP
207void Settings::_fileChanged(QString path)
208{
1b34ac93
PK
209 // check if the file isn't changed by our logic
210 // FIXME: this is poor implementation; should we rather compute some hash of values if changed by external?
211 if (0 == d_ptr->mAppChangeTimer)
212 {
213 // delay the change notification for 100 ms to avoid
214 // unnecessary repeated loading of the same config file if
215 // the file is changed for several times rapidly.
216 if(d_ptr->mFileChangeTimer)
217 killTimer(d_ptr->mFileChangeTimer);
218 d_ptr->mFileChangeTimer = startTimer(1000);
219 }
67dd270e 220
1b34ac93
PK
221 addWatchedFile(path);
222}
223
224void Settings::addWatchedFile(QString const & path)
225{
67dd270e
HJYP
226 // D*mn! yet another Qt 5.4 regression!!!
227 // See the bug report: https://github.com/lxde/lxqt/issues/441
228 // Since Qt 5.4, QSettings uses QSaveFile to save the config files.
229 // https://github.com/qtproject/qtbase/commit/8d15068911d7c0ba05732e2796aaa7a90e34a6a1#diff-e691c0405f02f3478f4f50a27bdaecde
230 // QSaveFile will save the content to a new temp file, and replace the old file later.
231 // Hence the existing config file is not changed. Instead, it's deleted and then replaced.
232 // This new behaviour unfortunately breaks QFileSystemWatcher.
233 // After file deletion, we can no longer receive any new change notifications.
234 // The most ridiculous thing is, QFileSystemWatcher does not provide a
235 // way for us to know if a file is deleted. WT*?
236 // Luckily, I found a workaround: If the file path no longer exists
237 // in the watcher's files(), this file is deleted.
238 if(!d_ptr->mWatcher.files().contains(path))
1b34ac93
PK
239 // in some situations adding fails because of non-existing file (e.g. editting file in external program)
240 if (!d_ptr->mWatcher.addPath(path) && 0 == d_ptr->mAddWatchTimer)
241 d_ptr->mAddWatchTimer = startTimer(100);
242
67dd270e
HJYP
243}
244
4ef6cee9 245
246/************************************************
247
248 ************************************************/
82830dc2 249const GlobalSettings *Settings::globalSettings()
4ef6cee9 250{
251 static QMutex mutex;
82830dc2 252 static GlobalSettings *instance = 0;
4ef6cee9 253 if (!instance)
254 {
255 mutex.lock();
256
257 if (!instance)
82830dc2 258 instance = new GlobalSettings();
4ef6cee9 259
260 mutex.unlock();
261 }
262
263 return instance;
264}
265
266
267/************************************************
85965778
JL
268 LC_MESSAGES value Possible keys in order of matching
269 lang_COUNTRY@MODIFIER lang_COUNTRY@MODIFIER, lang_COUNTRY, lang@MODIFIER, lang,
de53bc2d 270 default value
85965778
JL
271 lang_COUNTRY lang_COUNTRY, lang, default value
272 lang@MODIFIER lang@MODIFIER, lang, default value
273 lang lang, default value
de53bc2d 274 ************************************************/
82830dc2 275QString SettingsPrivate::localizedKey(const QString& key) const
de53bc2d
AS
276{
277
278 QString lang = getenv("LC_MESSAGES");
279
280 if (lang.isEmpty())
281 lang = getenv("LC_ALL");
282
283 if (lang.isEmpty())
284 lang = getenv("LANG");
285
286
287 QString modifier = lang.section('@', 1);
288 if (!modifier.isEmpty())
289 lang.truncate(lang.length() - modifier.length() - 1);
290
291 QString encoding = lang.section('.', 1);
292 if (!encoding.isEmpty())
293 lang.truncate(lang.length() - encoding.length() - 1);
294
295
296 QString country = lang.section('_', 1);
297 if (!country.isEmpty())
298 lang.truncate(lang.length() - country.length() - 1);
299
300
301
302 //qDebug() << "LC_MESSAGES: " << getenv("LC_MESSAGES");
303 //qDebug() << "Lang:" << lang;
304 //qDebug() << "Country:" << country;
305 //qDebug() << "Encoding:" << encoding;
306 //qDebug() << "Modifier:" << modifier;
307
308 if (!modifier.isEmpty() && !country.isEmpty())
309 {
310 QString k = QString("%1[%2_%3@%4]").arg(key, lang, country, modifier);
311 //qDebug() << "\t try " << k << mParent->contains(k);
312 if (mParent->contains(k))
313 return k;
314 }
315
316 if (!country.isEmpty())
317 {
318 QString k = QString("%1[%2_%3]").arg(key, lang, country);
319 //qDebug() << "\t try " << k << mParent->contains(k);
320 if (mParent->contains(k))
321 return k;
322 }
323
324 if (!modifier.isEmpty())
325 {
326 QString k = QString("%1[%2@%3]").arg(key, lang, modifier);
327 //qDebug() << "\t try " << k << mParent->contains(k);
328 if (mParent->contains(k))
329 return k;
330 }
331
332 QString k = QString("%1[%2]").arg(key, lang);
333 //qDebug() << "\t try " << k << mParent->contains(k);
334 if (mParent->contains(k))
335 return k;
336
337
338 //qDebug() << "\t try " << key << mParent->contains(key);
339 return key;
340}
341
de53bc2d
AS
342/************************************************
343
344 ************************************************/
82830dc2 345QVariant Settings::localizedValue(const QString& key, const QVariant& defaultValue) const
de53bc2d 346{
82830dc2 347 Q_D(const Settings);
de53bc2d
AS
348 return value(d->localizedKey(key), defaultValue);
349}
350
351
352/************************************************
353
354 ************************************************/
82830dc2 355void Settings::setLocalizedValue(const QString &key, const QVariant &value)
de53bc2d 356{
82830dc2 357 Q_D(const Settings);
de53bc2d
AS
358 setValue(d->localizedKey(key), value);
359}
360
361
362/************************************************
4ef6cee9 363
364 ************************************************/
f05ba5af
PL
365LXQtTheme::LXQtTheme():
366 d(new LXQtThemeData)
4ef6cee9 367{
4ef6cee9 368}
369
63a934d5
AS
370
371/************************************************
372
373 ************************************************/
f05ba5af
PL
374LXQtTheme::LXQtTheme(const QString &path):
375 d(new LXQtThemeData)
4ef6cee9 376{
63a934d5
AS
377 if (path.isEmpty())
378 return;
379
380 QFileInfo fi(path);
381 if (fi.isAbsolute())
382 {
383 d->mPath = path;
384 d->mName = fi.fileName();
385 d->mValid = fi.isDir();
386 }
387 else
388 {
389 d->mName = path;
390 d->mPath = d->findTheme(path);
391 d->mValid = !(d->mPath.isEmpty());
392 }
393
394 if (QDir(path).exists("preview.png"))
395 d->mPreviewImg = path + "/preview.png";
4ef6cee9 396}
397
398
399/************************************************
400
401 ************************************************/
f05ba5af 402QString LXQtThemeData::findTheme(const QString &themeName)
4ef6cee9 403{
63a934d5 404 if (themeName.isEmpty())
c24f8b85 405 return QString();
63a934d5
AS
406
407 QStringList paths;
26f76c3e
LP
408 QLatin1String fallback(LXQT_INSTALL_PREFIX);
409
63a934d5
AS
410 paths << XdgDirs::dataHome(false);
411 paths << XdgDirs::dataDirs();
26f76c3e
LP
412
413 if (!paths.contains(fallback))
414 paths << fallback;
4ef6cee9 415
a1a7c847 416 foreach(const QString &path, paths)
4ef6cee9 417 {
d0478f03 418 QDir dir(QString("%1/lxqt/themes/%2").arg(path, themeName));
63a934d5
AS
419 if (dir.isReadable())
420 return dir.absolutePath();
421 }
4ef6cee9 422
63a934d5
AS
423 return QString();
424}
4ef6cee9 425
4ef6cee9 426
63a934d5
AS
427/************************************************
428
429 ************************************************/
f05ba5af 430LXQtTheme::LXQtTheme(const LXQtTheme &other):
63a934d5
AS
431 d(other.d)
432{
433}
434
435
436/************************************************
437
438 ************************************************/
f05ba5af 439LXQtTheme::~LXQtTheme()
63a934d5
AS
440{
441}
442
443
444/************************************************
445
446 ************************************************/
f05ba5af 447LXQtTheme& LXQtTheme::operator=(const LXQtTheme &other)
63a934d5
AS
448{
449 d = other.d;
450 return *this;
451}
452
453
454/************************************************
455
456 ************************************************/
f05ba5af 457bool LXQtTheme::isValid() const
63a934d5
AS
458{
459 return d->mValid;
460}
461
462
463/************************************************
464
465 ************************************************/
f05ba5af 466QString LXQtTheme::name() const
63a934d5
AS
467{
468 return d->mName;
469}
470
471/************************************************
472
473 ************************************************/
f05ba5af 474QString LXQtTheme::path() const
63a934d5
AS
475{
476 return d->mPath;
477}
478
479
480/************************************************
481
482 ************************************************/
f05ba5af 483QString LXQtTheme::previewImage() const
63a934d5
AS
484{
485 return d->mPreviewImg;
4ef6cee9 486}
487
488
489/************************************************
490
491 ************************************************/
f05ba5af 492QString LXQtTheme::qss(const QString& module) const
4ef6cee9 493{
83c93de4 494 return d->loadQss(QStringLiteral("%1/%2.qss").arg(d->mPath, module));
4ef6cee9 495}
496
497
498/************************************************
499
500 ************************************************/
f05ba5af 501QString LXQtThemeData::loadQss(const QString& qssFile) const
4ef6cee9 502{
503 QFile f(qssFile);
504 if (! f.open(QIODevice::ReadOnly | QIODevice::Text))
505 {
96acff74 506 return QString();
4ef6cee9 507 }
508
509 QString qss = f.readAll();
510 f.close();
511
512 if (qss.isEmpty())
96acff74 513 return QString();
4ef6cee9 514
515 // handle relative paths
516 QString qssDir = QFileInfo(qssFile).canonicalPath();
517 qss.replace(QRegExp("url.[ \\t\\s]*", Qt::CaseInsensitive, QRegExp::RegExp2), "url(" + qssDir + "/");
518
519 return qss;
520}
521
522
523/************************************************
524
525 ************************************************/
f05ba5af 526QString LXQtTheme::desktopBackground(int screen) const
4ef6cee9 527{
fa20bdad 528 QString wallpaperCfgFileName = QString("%1/wallpaper.cfg").arg(d->mPath);
4ef6cee9 529
fa20bdad 530 if (wallpaperCfgFileName.isEmpty())
96acff74 531 return QString();
4ef6cee9 532
fa20bdad
KS
533 QSettings s(wallpaperCfgFileName, QSettings::IniFormat);
534 QString themeDir = QFileInfo(wallpaperCfgFileName).absolutePath();
4ef6cee9 535 // There is something strange... If I remove next line the wallpapers array is not found...
536 s.childKeys();
537 s.beginReadArray("wallpapers");
538
539 s.setArrayIndex(screen - 1);
540 if (s.contains("file"))
541 return QString("%1/%2").arg(themeDir, s.value("file").toString());
542
543 s.setArrayIndex(0);
544 if (s.contains("file"))
545 return QString("%1/%2").arg(themeDir, s.value("file").toString());
546
547 return QString();
548}
549
550
551/************************************************
552
553 ************************************************/
f05ba5af 554const LXQtTheme &LXQtTheme::currentTheme()
63a934d5 555{
f05ba5af 556 static LXQtTheme theme;
82830dc2 557 QString name = Settings::globalSettings()->value("theme").toString();
63a934d5
AS
558 if (theme.name() != name)
559 {
f05ba5af 560 theme = LXQtTheme(name);
63a934d5
AS
561 }
562 return theme;
563}
564
565
566/************************************************
567
568 ************************************************/
f05ba5af 569QList<LXQtTheme> LXQtTheme::allThemes()
63a934d5 570{
f05ba5af 571 QList<LXQtTheme> ret;
63a934d5
AS
572 QSet<QString> processed;
573
574 QStringList paths;
575 paths << XdgDirs::dataHome(false);
576 paths << XdgDirs::dataDirs();
577
a1a7c847 578 foreach(const QString &path, paths)
63a934d5 579 {
d0478f03 580 QDir dir(QString("%1/lxqt/themes").arg(path));
63a934d5
AS
581 QFileInfoList dirs = dir.entryInfoList(QDir::AllDirs | QDir::NoDotAndDotDot);
582
a1a7c847 583 foreach(const QFileInfo &dir, dirs)
63a934d5 584 {
f8267402 585 if (!processed.contains(dir.fileName()) &&
2ccbc79f 586 QDir(dir.absoluteFilePath()).exists("lxqt-panel.qss"))
63a934d5
AS
587 {
588 processed << dir.fileName();
f05ba5af 589 ret << LXQtTheme(dir.absoluteFilePath());
63a934d5
AS
590 }
591
592 }
593 }
594
595 return ret;
596}
597
598
599/************************************************
600
601 ************************************************/
82830dc2 602SettingsCache::SettingsCache(QSettings &settings) :
4ef6cee9 603 mSettings(settings)
604{
605 loadFromSettings();
606}
607
608
609/************************************************
610
611 ************************************************/
82830dc2 612SettingsCache::SettingsCache(QSettings *settings) :
4ef6cee9 613 mSettings(*settings)
614{
615 loadFromSettings();
616}
617
618
619/************************************************
620
621 ************************************************/
82830dc2 622void SettingsCache::loadFromSettings()
4ef6cee9 623{
f5ca359d
LP
624 const QStringList keys = mSettings.allKeys();
625
626 const int N = keys.size();
627 for (int i = 0; i < N; ++i) {
628 mCache.insert(keys.at(i), mSettings.value(keys.at(i)));
629 }
4ef6cee9 630}
631
632
633/************************************************
634
635 ************************************************/
82830dc2 636void SettingsCache::loadToSettings()
4ef6cee9 637{
638 QHash<QString, QVariant>::const_iterator i = mCache.constBegin();
639
640 while(i != mCache.constEnd())
641 {
642 mSettings.setValue(i.key(), i.value());
643 ++i;
644 }
645
646 mSettings.sync();
647}
648
649
650/************************************************
651
652 ************************************************/
82830dc2 653GlobalSettings::GlobalSettings():
41e3762a 654 Settings("lxqt"),
82830dc2 655 d_ptr(new GlobalSettingsPrivate(this))
4ef6cee9 656{
4ef6cee9 657 if (value("icon_theme").toString().isEmpty())
658 {
d0cf41fa 659 qWarning() << QString::fromLatin1("Icon Theme not set. Fallbacking to Oxygen, if installed");
69c061e3 660 const QString fallback(QLatin1String("oxygen"));
6ec51b5d 661
173f5903 662 const QDir dir(QLatin1String(LXQT_DATA_DIR) + QLatin1String("/icons"));
69c061e3 663 if (dir.exists(fallback))
4ef6cee9 664 {
69c061e3
LP
665 setValue("icon_theme", fallback);
666 sync();
4ef6cee9 667 }
d0cf41fa
LP
668 else
669 {
670 qWarning() << QString::fromLatin1("Fallback Icon Theme (Oxygen) not found");
4ef6cee9 671 }
672 }
673
674 fileChanged();
675}
676
82830dc2 677GlobalSettings::~GlobalSettings()
4ef6cee9 678{
679 delete d_ptr;
680}
681
682
683/************************************************
684
685 ************************************************/
82830dc2 686void GlobalSettings::fileChanged()
4ef6cee9 687{
82830dc2 688 Q_D(GlobalSettings);
4ef6cee9 689 sync();
690
691
692 QString it = value("icon_theme").toString();
693 if (d->mIconTheme != it)
694 {
4ef6cee9 695 emit iconThemeChanged();
696 }
697
63a934d5 698 QString rt = value("theme").toString();
3e771e4e 699 qlonglong themeUpdated = value("__theme_updated__").toLongLong();
f05ba5af 700 if ((d->mLXQtTheme != rt) || (d->mThemeUpdated != themeUpdated))
4ef6cee9 701 {
f05ba5af 702 d->mLXQtTheme = rt;
db4aaddf 703 emit lxqtThemeChanged();
4ef6cee9 704 }
705
1b34ac93 706 emit settingsChangedFromExternal();
c16a53ee 707 emit settingsChanged();
4ef6cee9 708}
63a934d5 709