Sets Oxygen as the only fallback icon theme
[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 <XdgIcon>
40 #include <XdgDirs>
41
42 using namespace LXQt;
43
44 class LXQt::SettingsPrivate
45 {
46 public:
47 SettingsPrivate(Settings* parent):
48 mFileChangeTimer(0),
49 mAppChangeTimer(0),
50 mAddWatchTimer(0),
51 mParent(parent)
52 {
53 }
54
55 QString localizedKey(const QString& key) const;
56
57 QFileSystemWatcher mWatcher;
58 int mFileChangeTimer;
59 int mAppChangeTimer;
60 int mAddWatchTimer;
61
62 private:
63 Settings* mParent;
64 };
65
66
67 LXQtTheme* LXQtTheme::mInstance = 0;
68
69 class LXQt::LXQtThemeData: public QSharedData {
70 public:
71 LXQtThemeData(): mValid(false) {}
72 QString loadQss(const QString& qssFile) const;
73 QString findTheme(const QString &themeName);
74
75 QString mName;
76 QString mPath;
77 QString mPreviewImg;
78 bool mValid;
79
80 };
81
82
83 class LXQt::GlobalSettingsPrivate
84 {
85 public:
86 GlobalSettingsPrivate(GlobalSettings *parent):
87 mParent(parent),
88 mThemeUpdated(0ull)
89 {
90
91 }
92
93 GlobalSettings *mParent;
94 QString mIconTheme;
95 QString mLXQtTheme;
96 qlonglong mThemeUpdated;
97
98 };
99
100
101 /************************************************
102
103 ************************************************/
104 Settings::Settings(const QString& module, QObject* parent) :
105 QSettings("lxqt", module, parent),
106 d_ptr(new SettingsPrivate(this))
107 {
108 // HACK: we need to ensure that the user (~/.config/lxqt/<module>.conf)
109 // exists to have functional mWatcher
110 if (!contains("__userfile__"))
111 {
112 setValue("__userfile__", true);
113 sync();
114 }
115 d_ptr->mWatcher.addPath(this->fileName());
116 connect(&(d_ptr->mWatcher), &QFileSystemWatcher::fileChanged, this, &Settings::_fileChanged);
117 }
118
119
120 /************************************************
121
122 ************************************************/
123 Settings::Settings(const QString &fileName, QSettings::Format format, QObject *parent):
124 QSettings(fileName, format, parent),
125 d_ptr(new SettingsPrivate(this))
126 {
127 // HACK: we need to ensure that the user (~/.config/lxqt/<module>.conf)
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());
135 connect(&(d_ptr->mWatcher), &QFileSystemWatcher::fileChanged, this, &Settings::_fileChanged);
136 }
137
138
139 /************************************************
140
141 ************************************************/
142 Settings::Settings(const QSettings* parentSettings, const QString& subGroup, QObject* parent):
143 QSettings(parentSettings->organizationName(), parentSettings->applicationName(), parent),
144 d_ptr(new SettingsPrivate(this))
145 {
146 beginGroup(subGroup);
147 }
148
149
150 /************************************************
151
152 ************************************************/
153 Settings::Settings(const QSettings& parentSettings, const QString& subGroup, QObject* parent):
154 QSettings(parentSettings.organizationName(), parentSettings.applicationName(), parent),
155 d_ptr(new SettingsPrivate(this))
156 {
157 beginGroup(subGroup);
158 }
159
160
161 /************************************************
162
163 ************************************************/
164 Settings::~Settings()
165 {
166 // because in the Settings::Settings(const QString& module, QObject* parent)
167 // constructor there is no beginGroup() called...
168 if (!group().isEmpty())
169 endGroup();
170
171 delete d_ptr;
172 }
173
174 bool Settings::event(QEvent *event)
175 {
176 if (event->type() == QEvent::UpdateRequest)
177 {
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);
184 }
185 else if (event->type() == QEvent::Timer)
186 {
187 const int timer = static_cast<QTimerEvent*>(event)->timerId();
188 killTimer(timer);
189 if (timer == d_ptr->mFileChangeTimer)
190 {
191 d_ptr->mFileChangeTimer = 0;
192 fileChanged(); // invoke the real fileChanged() handler.
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());
204 }
205 }
206
207 return QSettings::event(event);
208 }
209
210 void Settings::fileChanged()
211 {
212 sync();
213 emit settingsChangedFromExternal();
214 emit settingsChanged();
215 }
216
217 void Settings::_fileChanged(QString path)
218 {
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 }
230
231 addWatchedFile(path);
232 }
233
234 void Settings::addWatchedFile(QString const & path)
235 {
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))
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
253 }
254
255
256 /************************************************
257
258 ************************************************/
259 const GlobalSettings *Settings::globalSettings()
260 {
261 static QMutex mutex;
262 static GlobalSettings *instance = 0;
263 if (!instance)
264 {
265 mutex.lock();
266
267 if (!instance)
268 instance = new GlobalSettings();
269
270 mutex.unlock();
271 }
272
273 return instance;
274 }
275
276
277 /************************************************
278 LC_MESSAGES value Possible keys in order of matching
279 lang_COUNTRY@MODIFIER lang_COUNTRY@MODIFIER, lang_COUNTRY, lang@MODIFIER, lang,
280 default value
281 lang_COUNTRY lang_COUNTRY, lang, default value
282 lang@MODIFIER lang@MODIFIER, lang, default value
283 lang lang, default value
284 ************************************************/
285 QString SettingsPrivate::localizedKey(const QString& key) const
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
352 /************************************************
353
354 ************************************************/
355 QVariant Settings::localizedValue(const QString& key, const QVariant& defaultValue) const
356 {
357 Q_D(const Settings);
358 return value(d->localizedKey(key), defaultValue);
359 }
360
361
362 /************************************************
363
364 ************************************************/
365 void Settings::setLocalizedValue(const QString &key, const QVariant &value)
366 {
367 Q_D(const Settings);
368 setValue(d->localizedKey(key), value);
369 }
370
371
372 /************************************************
373
374 ************************************************/
375 LXQtTheme::LXQtTheme():
376 d(new LXQtThemeData)
377 {
378 }
379
380
381 /************************************************
382
383 ************************************************/
384 LXQtTheme::LXQtTheme(const QString &path):
385 d(new LXQtThemeData)
386 {
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";
406 }
407
408
409 /************************************************
410
411 ************************************************/
412 QString LXQtThemeData::findTheme(const QString &themeName)
413 {
414 if (themeName.isEmpty())
415 return QString();
416
417 QStringList paths;
418 QLatin1String fallback(LXQT_INSTALL_PREFIX);
419
420 paths << XdgDirs::dataHome(false);
421 paths << XdgDirs::dataDirs();
422
423 if (!paths.contains(fallback))
424 paths << fallback;
425
426 foreach(QString path, paths)
427 {
428 QDir dir(QString("%1/lxqt/themes/%2").arg(path, themeName));
429 if (dir.isReadable())
430 return dir.absolutePath();
431 }
432
433 return QString();
434 }
435
436
437 /************************************************
438
439 ************************************************/
440 LXQtTheme::LXQtTheme(const LXQtTheme &other):
441 d(other.d)
442 {
443 }
444
445
446 /************************************************
447
448 ************************************************/
449 LXQtTheme::~LXQtTheme()
450 {
451 }
452
453
454 /************************************************
455
456 ************************************************/
457 LXQtTheme& LXQtTheme::operator=(const LXQtTheme &other)
458 {
459 d = other.d;
460 return *this;
461 }
462
463
464 /************************************************
465
466 ************************************************/
467 bool LXQtTheme::isValid() const
468 {
469 return d->mValid;
470 }
471
472
473 /************************************************
474
475 ************************************************/
476 QString LXQtTheme::name() const
477 {
478 return d->mName;
479 }
480
481 /************************************************
482
483 ************************************************/
484 QString LXQtTheme::path() const
485 {
486 return d->mPath;
487 }
488
489
490 /************************************************
491
492 ************************************************/
493 QString LXQtTheme::previewImage() const
494 {
495 return d->mPreviewImg;
496 }
497
498
499 /************************************************
500
501 ************************************************/
502 QString LXQtTheme::qss(const QString& module) const
503 {
504 return d->loadQss(QStringLiteral("%1/%2.qss").arg(d->mPath, module));
505 }
506
507
508 /************************************************
509
510 ************************************************/
511 QString LXQtThemeData::loadQss(const QString& qssFile) const
512 {
513 QFile f(qssFile);
514 if (! f.open(QIODevice::ReadOnly | QIODevice::Text))
515 {
516 return QString();
517 }
518
519 QString qss = f.readAll();
520 f.close();
521
522 if (qss.isEmpty())
523 return QString();
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 ************************************************/
536 QString LXQtTheme::desktopBackground(int screen) const
537 {
538 QString wallpaperCfgFileName = QString("%1/wallpaper.cfg").arg(d->mPath);
539
540 if (wallpaperCfgFileName.isEmpty())
541 return QString();
542
543 QSettings s(wallpaperCfgFileName, QSettings::IniFormat);
544 QString themeDir = QFileInfo(wallpaperCfgFileName).absolutePath();
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 ************************************************/
564 const LXQtTheme &LXQtTheme::currentTheme()
565 {
566 static LXQtTheme theme;
567 QString name = Settings::globalSettings()->value("theme").toString();
568 if (theme.name() != name)
569 {
570 theme = LXQtTheme(name);
571 }
572 return theme;
573 }
574
575
576 /************************************************
577
578 ************************************************/
579 QList<LXQtTheme> LXQtTheme::allThemes()
580 {
581 QList<LXQtTheme> ret;
582 QSet<QString> processed;
583
584 QStringList paths;
585 paths << XdgDirs::dataHome(false);
586 paths << XdgDirs::dataDirs();
587
588 foreach(QString path, paths)
589 {
590 QDir dir(QString("%1/lxqt/themes").arg(path));
591 QFileInfoList dirs = dir.entryInfoList(QDir::AllDirs | QDir::NoDotAndDotDot);
592
593 foreach(QFileInfo dir, dirs)
594 {
595 if (!processed.contains(dir.fileName()) &&
596 QDir(dir.absoluteFilePath()).exists("lxqt-panel.qss"))
597 {
598 processed << dir.fileName();
599 ret << LXQtTheme(dir.absoluteFilePath());
600 }
601
602 }
603 }
604
605 return ret;
606 }
607
608
609 /************************************************
610
611 ************************************************/
612 SettingsCache::SettingsCache(QSettings &settings) :
613 mSettings(settings)
614 {
615 loadFromSettings();
616 }
617
618
619 /************************************************
620
621 ************************************************/
622 SettingsCache::SettingsCache(QSettings *settings) :
623 mSettings(*settings)
624 {
625 loadFromSettings();
626 }
627
628
629 /************************************************
630
631 ************************************************/
632 void SettingsCache::loadFromSettings()
633 {
634 foreach (QString key, mSettings.allKeys())
635 {
636 mCache.insert(key, mSettings.value(key));
637 }
638 }
639
640
641 /************************************************
642
643 ************************************************/
644 void SettingsCache::loadToSettings()
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 ************************************************/
661 GlobalSettings::GlobalSettings():
662 Settings("lxqt"),
663 d_ptr(new GlobalSettingsPrivate(this))
664 {
665 if (value("icon_theme").toString().isEmpty())
666 {
667 const QString fallback(QLatin1String("oxygen"));
668
669 QDir dir("/usr/share/icons/");
670 if (dir.exists(fallback))
671 {
672 setValue("icon_theme", fallback);
673 sync();
674 }
675 }
676
677 fileChanged();
678 }
679
680 GlobalSettings::~GlobalSettings()
681 {
682 delete d_ptr;
683 }
684
685
686 /************************************************
687
688 ************************************************/
689 void GlobalSettings::fileChanged()
690 {
691 Q_D(GlobalSettings);
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
703 QString rt = value("theme").toString();
704 qlonglong themeUpdated = value("__theme_updated__").toLongLong();
705 if ((d->mLXQtTheme != rt) || (d->mThemeUpdated != themeUpdated))
706 {
707 d->mLXQtTheme = rt;
708 emit lxqtThemeChanged();
709 }
710
711 emit settingsChangedFromExternal();
712 emit settingsChanged();
713 }
714