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