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