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