Settings: unify duplicated code
[lxde/liblxqt.git] / lxqtsettings.cpp
1 /* BEGIN_COMMON_COPYRIGHT_HEADER
2 * (c)LGPL2+
3 *
4 * LXQt - a lightweight, Qt based, desktop toolset
5 * http://razor-qt.org
6 *
7 * Copyright: 2010-2011 Razor team
8 * Authors:
9 * Alexander Sokoloff <sokoloff.a@gmail.com>
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
15 * version 2.1 of the License, or (at your option) any later version.
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
29 #include "lxqtsettings.h"
30 #include <QDebug>
31 #include <QEvent>
32 #include <QDir>
33 #include <QStringList>
34 #include <QMutex>
35 #include <QFileSystemWatcher>
36 #include <QSharedData>
37 #include <QTimerEvent>
38
39 #include <XdgDirs>
40
41 using namespace LXQt;
42
43 class LXQt::SettingsPrivate
44 {
45 public:
46 SettingsPrivate(Settings* parent):
47 mFileChangeTimer(0),
48 mAppChangeTimer(0),
49 mAddWatchTimer(0),
50 mParent(parent)
51 {
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);
61 }
62
63 QString localizedKey(const QString& key) const;
64
65 QFileSystemWatcher mWatcher;
66 int mFileChangeTimer;
67 int mAppChangeTimer;
68 int mAddWatchTimer;
69
70 private:
71 Settings* mParent;
72 };
73
74
75 LXQtTheme* LXQtTheme::mInstance = 0;
76
77 class LXQt::LXQtThemeData: public QSharedData {
78 public:
79 LXQtThemeData(): mValid(false) {}
80 QString loadQss(const QString& qssFile) const;
81 QString findTheme(const QString &themeName);
82
83 QString mName;
84 QString mPath;
85 QString mPreviewImg;
86 bool mValid;
87
88 };
89
90
91 class LXQt::GlobalSettingsPrivate
92 {
93 public:
94 GlobalSettingsPrivate(GlobalSettings *parent):
95 mParent(parent),
96 mThemeUpdated(0ull)
97 {
98
99 }
100
101 GlobalSettings *mParent;
102 QString mIconTheme;
103 QString mLXQtTheme;
104 qlonglong mThemeUpdated;
105
106 };
107
108
109 /************************************************
110
111 ************************************************/
112 Settings::Settings(const QString& module, QObject* parent) :
113 QSettings("lxqt", module, parent),
114 d_ptr(new SettingsPrivate(this))
115 {
116 }
117
118
119 /************************************************
120
121 ************************************************/
122 Settings::Settings(const QString &fileName, QSettings::Format format, QObject *parent):
123 QSettings(fileName, format, parent),
124 d_ptr(new SettingsPrivate(this))
125 {
126 }
127
128
129 /************************************************
130
131 ************************************************/
132 Settings::Settings(const QSettings* parentSettings, const QString& subGroup, QObject* parent):
133 QSettings(parentSettings->organizationName(), parentSettings->applicationName(), parent),
134 d_ptr(new SettingsPrivate(this))
135 {
136 beginGroup(subGroup);
137 }
138
139
140 /************************************************
141
142 ************************************************/
143 Settings::Settings(const QSettings& parentSettings, const QString& subGroup, QObject* parent):
144 QSettings(parentSettings.organizationName(), parentSettings.applicationName(), parent),
145 d_ptr(new SettingsPrivate(this))
146 {
147 beginGroup(subGroup);
148 }
149
150
151 /************************************************
152
153 ************************************************/
154 Settings::~Settings()
155 {
156 // because in the Settings::Settings(const QString& module, QObject* parent)
157 // constructor there is no beginGroup() called...
158 if (!group().isEmpty())
159 endGroup();
160
161 delete d_ptr;
162 }
163
164 bool Settings::event(QEvent *event)
165 {
166 if (event->type() == QEvent::UpdateRequest)
167 {
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);
174 }
175 else if (event->type() == QEvent::Timer)
176 {
177 const int timer = static_cast<QTimerEvent*>(event)->timerId();
178 killTimer(timer);
179 if (timer == d_ptr->mFileChangeTimer)
180 {
181 d_ptr->mFileChangeTimer = 0;
182 fileChanged(); // invoke the real fileChanged() handler.
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());
194 }
195 }
196
197 return QSettings::event(event);
198 }
199
200 void Settings::fileChanged()
201 {
202 sync();
203 emit settingsChangedFromExternal();
204 emit settingsChanged();
205 }
206
207 void Settings::_fileChanged(QString path)
208 {
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 }
220
221 addWatchedFile(path);
222 }
223
224 void Settings::addWatchedFile(QString const & path)
225 {
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))
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
243 }
244
245
246 /************************************************
247
248 ************************************************/
249 const GlobalSettings *Settings::globalSettings()
250 {
251 static QMutex mutex;
252 static GlobalSettings *instance = 0;
253 if (!instance)
254 {
255 mutex.lock();
256
257 if (!instance)
258 instance = new GlobalSettings();
259
260 mutex.unlock();
261 }
262
263 return instance;
264 }
265
266
267 /************************************************
268 LC_MESSAGES value Possible keys in order of matching
269 lang_COUNTRY@MODIFIER lang_COUNTRY@MODIFIER, lang_COUNTRY, lang@MODIFIER, lang,
270 default value
271 lang_COUNTRY lang_COUNTRY, lang, default value
272 lang@MODIFIER lang@MODIFIER, lang, default value
273 lang lang, default value
274 ************************************************/
275 QString SettingsPrivate::localizedKey(const QString& key) const
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
342 /************************************************
343
344 ************************************************/
345 QVariant Settings::localizedValue(const QString& key, const QVariant& defaultValue) const
346 {
347 Q_D(const Settings);
348 return value(d->localizedKey(key), defaultValue);
349 }
350
351
352 /************************************************
353
354 ************************************************/
355 void Settings::setLocalizedValue(const QString &key, const QVariant &value)
356 {
357 Q_D(const Settings);
358 setValue(d->localizedKey(key), value);
359 }
360
361
362 /************************************************
363
364 ************************************************/
365 LXQtTheme::LXQtTheme():
366 d(new LXQtThemeData)
367 {
368 }
369
370
371 /************************************************
372
373 ************************************************/
374 LXQtTheme::LXQtTheme(const QString &path):
375 d(new LXQtThemeData)
376 {
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";
396 }
397
398
399 /************************************************
400
401 ************************************************/
402 QString LXQtThemeData::findTheme(const QString &themeName)
403 {
404 if (themeName.isEmpty())
405 return QString();
406
407 QStringList paths;
408 QLatin1String fallback(LXQT_INSTALL_PREFIX);
409
410 paths << XdgDirs::dataHome(false);
411 paths << XdgDirs::dataDirs();
412
413 if (!paths.contains(fallback))
414 paths << fallback;
415
416 foreach(const QString &path, paths)
417 {
418 QDir dir(QString("%1/lxqt/themes/%2").arg(path, themeName));
419 if (dir.isReadable())
420 return dir.absolutePath();
421 }
422
423 return QString();
424 }
425
426
427 /************************************************
428
429 ************************************************/
430 LXQtTheme::LXQtTheme(const LXQtTheme &other):
431 d(other.d)
432 {
433 }
434
435
436 /************************************************
437
438 ************************************************/
439 LXQtTheme::~LXQtTheme()
440 {
441 }
442
443
444 /************************************************
445
446 ************************************************/
447 LXQtTheme& LXQtTheme::operator=(const LXQtTheme &other)
448 {
449 d = other.d;
450 return *this;
451 }
452
453
454 /************************************************
455
456 ************************************************/
457 bool LXQtTheme::isValid() const
458 {
459 return d->mValid;
460 }
461
462
463 /************************************************
464
465 ************************************************/
466 QString LXQtTheme::name() const
467 {
468 return d->mName;
469 }
470
471 /************************************************
472
473 ************************************************/
474 QString LXQtTheme::path() const
475 {
476 return d->mPath;
477 }
478
479
480 /************************************************
481
482 ************************************************/
483 QString LXQtTheme::previewImage() const
484 {
485 return d->mPreviewImg;
486 }
487
488
489 /************************************************
490
491 ************************************************/
492 QString LXQtTheme::qss(const QString& module) const
493 {
494 return d->loadQss(QStringLiteral("%1/%2.qss").arg(d->mPath, module));
495 }
496
497
498 /************************************************
499
500 ************************************************/
501 QString LXQtThemeData::loadQss(const QString& qssFile) const
502 {
503 QFile f(qssFile);
504 if (! f.open(QIODevice::ReadOnly | QIODevice::Text))
505 {
506 return QString();
507 }
508
509 QString qss = f.readAll();
510 f.close();
511
512 if (qss.isEmpty())
513 return QString();
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 ************************************************/
526 QString LXQtTheme::desktopBackground(int screen) const
527 {
528 QString wallpaperCfgFileName = QString("%1/wallpaper.cfg").arg(d->mPath);
529
530 if (wallpaperCfgFileName.isEmpty())
531 return QString();
532
533 QSettings s(wallpaperCfgFileName, QSettings::IniFormat);
534 QString themeDir = QFileInfo(wallpaperCfgFileName).absolutePath();
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 ************************************************/
554 const LXQtTheme &LXQtTheme::currentTheme()
555 {
556 static LXQtTheme theme;
557 QString name = Settings::globalSettings()->value("theme").toString();
558 if (theme.name() != name)
559 {
560 theme = LXQtTheme(name);
561 }
562 return theme;
563 }
564
565
566 /************************************************
567
568 ************************************************/
569 QList<LXQtTheme> LXQtTheme::allThemes()
570 {
571 QList<LXQtTheme> ret;
572 QSet<QString> processed;
573
574 QStringList paths;
575 paths << XdgDirs::dataHome(false);
576 paths << XdgDirs::dataDirs();
577
578 foreach(const QString &path, paths)
579 {
580 QDir dir(QString("%1/lxqt/themes").arg(path));
581 QFileInfoList dirs = dir.entryInfoList(QDir::AllDirs | QDir::NoDotAndDotDot);
582
583 foreach(const QFileInfo &dir, dirs)
584 {
585 if (!processed.contains(dir.fileName()) &&
586 QDir(dir.absoluteFilePath()).exists("lxqt-panel.qss"))
587 {
588 processed << dir.fileName();
589 ret << LXQtTheme(dir.absoluteFilePath());
590 }
591
592 }
593 }
594
595 return ret;
596 }
597
598
599 /************************************************
600
601 ************************************************/
602 SettingsCache::SettingsCache(QSettings &settings) :
603 mSettings(settings)
604 {
605 loadFromSettings();
606 }
607
608
609 /************************************************
610
611 ************************************************/
612 SettingsCache::SettingsCache(QSettings *settings) :
613 mSettings(*settings)
614 {
615 loadFromSettings();
616 }
617
618
619 /************************************************
620
621 ************************************************/
622 void SettingsCache::loadFromSettings()
623 {
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 }
630 }
631
632
633 /************************************************
634
635 ************************************************/
636 void SettingsCache::loadToSettings()
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 ************************************************/
653 GlobalSettings::GlobalSettings():
654 Settings("lxqt"),
655 d_ptr(new GlobalSettingsPrivate(this))
656 {
657 if (value("icon_theme").toString().isEmpty())
658 {
659 qWarning() << QString::fromLatin1("Icon Theme not set. Fallbacking to Oxygen, if installed");
660 const QString fallback(QLatin1String("oxygen"));
661
662 const QDir dir(QLatin1String(LXQT_DATA_DIR) + QLatin1String("/icons"));
663 if (dir.exists(fallback))
664 {
665 setValue("icon_theme", fallback);
666 sync();
667 }
668 else
669 {
670 qWarning() << QString::fromLatin1("Fallback Icon Theme (Oxygen) not found");
671 }
672 }
673
674 fileChanged();
675 }
676
677 GlobalSettings::~GlobalSettings()
678 {
679 delete d_ptr;
680 }
681
682
683 /************************************************
684
685 ************************************************/
686 void GlobalSettings::fileChanged()
687 {
688 Q_D(GlobalSettings);
689 sync();
690
691
692 QString it = value("icon_theme").toString();
693 if (d->mIconTheme != it)
694 {
695 emit iconThemeChanged();
696 }
697
698 QString rt = value("theme").toString();
699 qlonglong themeUpdated = value("__theme_updated__").toLongLong();
700 if ((d->mLXQtTheme != rt) || (d->mThemeUpdated != themeUpdated))
701 {
702 d->mLXQtTheme = rt;
703 emit lxqtThemeChanged();
704 }
705
706 emit settingsChangedFromExternal();
707 emit settingsChanged();
708 }
709