1/*
2 * Copyright 2006-2007 Aaron Seigo <aseigo@kde.org>
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU Library General Public License as
6 * published by the Free Software Foundation; either version 2, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details
13 *
14 * You should have received a copy of the GNU Library General Public
15 * License along with this program; if not, write to the
16 * Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 */
19
20#include "theme.h"
21
22#include <QApplication>
23#include <QFile>
24#include <QFileInfo>
25#include <QMutableListIterator>
26#include <QPair>
27#include <QStringBuilder>
28#include <QTimer>
29#ifdef Q_WS_X11
30#include <QX11Info>
31#include "private/effectwatcher_p.h"
32#endif
33
34#include <kcolorscheme.h>
35#include <kcomponentdata.h>
36#include <kconfiggroup.h>
37#include <kdebug.h>
38#include <kdirwatch.h>
39#include <kglobal.h>
40#include <kglobalsettings.h>
41#include <kmanagerselection.h>
42#include <kimagecache.h>
43#include <ksharedconfig.h>
44#include <kstandarddirs.h>
45#include <kwindowsystem.h>
46
47
48#include "animations/animationscriptengine_p.h"
49#include "libplasma-theme-global.h"
50#include "private/packages_p.h"
51#include "windoweffects.h"
52
53namespace Plasma
54{
55
56//NOTE: Default wallpaper can be set from the theme configuration
57#define DEFAULT_WALLPAPER_THEME "default"
58#define DEFAULT_WALLPAPER_SUFFIX ".png"
59static const int DEFAULT_WALLPAPER_WIDTH = 1920;
60static const int DEFAULT_WALLPAPER_HEIGHT = 1200;
61
62enum styles {
63 DEFAULTSTYLE,
64 SVGSTYLE
65};
66
67enum CacheType {
68 NoCache = 0,
69 PixmapCache = 1,
70 SvgElementsCache = 2
71};
72Q_DECLARE_FLAGS(CacheTypes, CacheType)
73Q_DECLARE_OPERATORS_FOR_FLAGS(CacheTypes)
74
75class ThemePrivate
76{
77public:
78 ThemePrivate(Theme *theme)
79 : q(theme),
80 colorScheme(QPalette::Active, KColorScheme::Window, KSharedConfigPtr(0)),
81 buttonColorScheme(QPalette::Active, KColorScheme::Button, KSharedConfigPtr(0)),
82 viewColorScheme(QPalette::Active, KColorScheme::View, KSharedConfigPtr(0)),
83 defaultWallpaperTheme(DEFAULT_WALLPAPER_THEME),
84 defaultWallpaperSuffix(DEFAULT_WALLPAPER_SUFFIX),
85 defaultWallpaperWidth(DEFAULT_WALLPAPER_WIDTH),
86 defaultWallpaperHeight(DEFAULT_WALLPAPER_HEIGHT),
87 pixmapCache(0),
88 cachesToDiscard(NoCache),
89 locolor(false),
90 compositingActive(KWindowSystem::self()->compositingActive()),
91 blurActive(false),
92 isDefault(false),
93 useGlobal(true),
94 hasWallpapers(false),
95 useNativeWidgetStyle(false)
96 {
97 generalFont = QApplication::font();
98 ThemeConfig config;
99 cacheTheme = config.cacheTheme();
100
101 saveTimer = new QTimer(q);
102 saveTimer->setSingleShot(true);
103 saveTimer->setInterval(600);
104 QObject::connect(saveTimer, SIGNAL(timeout()), q, SLOT(scheduledCacheUpdate()));
105
106 updateNotificationTimer = new QTimer(q);
107 updateNotificationTimer->setSingleShot(true);
108 updateNotificationTimer->setInterval(500);
109 QObject::connect(updateNotificationTimer, SIGNAL(timeout()), q, SLOT(notifyOfChanged()));
110
111 if (QPixmap::defaultDepth() > 8) {
112 QObject::connect(KWindowSystem::self(), SIGNAL(compositingChanged(bool)), q, SLOT(compositingChanged(bool)));
113#ifdef Q_WS_X11
114 //watch for blur effect property changes as well
115 if (!s_blurEffectWatcher) {
116 s_blurEffectWatcher = new EffectWatcher("_KDE_NET_WM_BLUR_BEHIND_REGION");
117 }
118
119 QObject::connect(s_blurEffectWatcher, SIGNAL(effectChanged(bool)), q, SLOT(blurBehindChanged(bool)));
120#endif
121 }
122 }
123
124 ~ThemePrivate()
125 {
126 delete pixmapCache;
127 }
128
129 KConfigGroup &config()
130 {
131 if (!cfg.isValid()) {
132 QString groupName = "Theme";
133
134 if (!useGlobal) {
135 QString app = KGlobal::mainComponent().componentName();
136
137 if (!app.isEmpty()) {
138 kDebug() << "using theme for app" << app;
139 groupName.append("-").append(app);
140 }
141 }
142
143 cfg = KConfigGroup(KSharedConfig::openConfig(themeRcFile), groupName);
144 }
145
146 return cfg;
147 }
148
149 QString findInTheme(const QString &image, const QString &theme, bool cache = true);
150 void compositingChanged(bool active);
151 void discardCache(CacheTypes caches);
152 void scheduledCacheUpdate();
153 void scheduleThemeChangeNotification(CacheTypes caches);
154 void notifyOfChanged();
155 void colorsChanged();
156 void blurBehindChanged(bool blur);
157 bool useCache();
158 void settingsFileChanged(const QString &);
159 void setThemeName(const QString &themeName, bool writeSettings);
160 void onAppExitCleanup();
161 void processWallpaperSettings(KConfigBase *metadata);
162 void processAnimationSettings(const QString &theme, KConfigBase *metadata);
163
164 const QString processStyleSheet(const QString &css);
165
166 static const char *defaultTheme;
167 static const char *systemColorsTheme;
168 static const char *themeRcFile;
169 static PackageStructure::Ptr packageStructure;
170#ifdef Q_WS_X11
171 static EffectWatcher *s_blurEffectWatcher;
172#endif
173
174 Theme *q;
175 QString themeName;
176 QList<QString> fallbackThemes;
177 KSharedConfigPtr colors;
178 KColorScheme colorScheme;
179 KColorScheme buttonColorScheme;
180 KColorScheme viewColorScheme;
181 KConfigGroup cfg;
182 QFont generalFont;
183 QString defaultWallpaperTheme;
184 QString defaultWallpaperSuffix;
185 int defaultWallpaperWidth;
186 int defaultWallpaperHeight;
187 KImageCache *pixmapCache;
188 KSharedConfigPtr svgElementsCache;
189 QHash<QString, QSet<QString> > invalidElements;
190 QHash<QString, QPixmap> pixmapsToCache;
191 QHash<QString, QString> keysToCache;
192 QHash<QString, QString> idsToCache;
193 QHash<QString, QString> animationMapping;
194 QHash<styles, QString> cachedStyleSheets;
195 QHash<QString, QString> discoveries;
196 QTimer *saveTimer;
197 QTimer *updateNotificationTimer;
198 int toolTipDelay;
199 CacheTypes cachesToDiscard;
200 QString themeVersion;
201 QString themeMetadataPath;
202
203 bool locolor : 1;
204 bool compositingActive : 1;
205 bool blurActive : 1;
206 bool isDefault : 1;
207 bool useGlobal : 1;
208 bool hasWallpapers : 1;
209 bool cacheTheme : 1;
210 bool useNativeWidgetStyle :1;
211};
212
213PackageStructure::Ptr ThemePrivate::packageStructure(0);
214const char *ThemePrivate::defaultTheme = "default";
215
216const char *ThemePrivate::themeRcFile = "plasmarc";
217// the system colors theme is used to cache unthemed svgs with colorization needs
218// these svgs do not follow the theme's colors, but rather the system colors
219const char *ThemePrivate::systemColorsTheme = "internal-system-colors";
220#ifdef Q_WS_X11
221EffectWatcher *ThemePrivate::s_blurEffectWatcher = 0;
222#endif
223
224bool ThemePrivate::useCache()
225{
226 bool cachesTooOld = false;
227
228 if (cacheTheme && !pixmapCache) {
229 const bool isRegularTheme = themeName != systemColorsTheme;
230 QString cacheFile = "plasma_theme_" + themeName;
231
232 // clear any cached values from the previous theme cache
233 themeVersion.clear();
234
235 if (!themeMetadataPath.isEmpty()) {
236 KDirWatch::self()->removeFile(themeMetadataPath);
237 }
238 themeMetadataPath = KStandardDirs::locate("data", "desktoptheme/" + themeName + "/metadata.desktop");
239
240 if (isRegularTheme) {
241 const QString cacheFileBase = cacheFile + "*.kcache";
242
243 // if the path is empty, then we haven't found the theme and so
244 // we will leave currentCacheFileName empty, resulting in the deletion of
245 // *all* matching cache files
246 QString currentCacheFileName;
247 if (!themeMetadataPath.isEmpty()) {
248 // now we record the theme version, if we can
249 const KPluginInfo pluginInfo(themeMetadataPath);
250 themeVersion = pluginInfo.version();
251 if (!themeVersion.isEmpty()) {
252 cacheFile += "_v" + themeVersion;
253 currentCacheFileName = cacheFile + ".kcache";
254 }
255
256 // watch the metadata file for changes at runtime
257 KDirWatch::self()->addFile(themeMetadataPath);
258 QObject::connect(KDirWatch::self(), SIGNAL(created(QString)),
259 q, SLOT(settingsFileChanged(QString)),
260 Qt::UniqueConnection);
261 QObject::connect(KDirWatch::self(), SIGNAL(dirty(QString)),
262 q, SLOT(settingsFileChanged(QString)),
263 Qt::UniqueConnection);
264 }
265
266 // now we check for (and remove) old caches
267 foreach (const QString &file, KGlobal::dirs()->findAllResources("cache", cacheFileBase)) {
268 if (currentCacheFileName.isEmpty() ||
269 !file.endsWith(currentCacheFileName)) {
270 QFile::remove(file);
271 }
272 }
273 }
274
275 // now we do a sanity check: if the metadata.desktop file is newer than the cache, drop
276 // the cache
277 if (isRegularTheme && !themeMetadataPath.isEmpty()) {
278 // now we check to see if the theme metadata file itself is newer than the pixmap cache
279 // this is done before creating the pixmapCache object since that can change the mtime
280 // on the cache file
281
282 // FIXME: when using the system colors, if they change while the application is not running
283 // the cache should be dropped; we need a way to detect system color change when the
284 // application is not running.
285 // check for expired cache
286 const QString cacheFilePath = KStandardDirs::locateLocal("cache", cacheFile);
287 if (!cacheFilePath.isEmpty()) {
288 const QFileInfo cacheFileInfo(cacheFilePath);
289 const QFileInfo metadataFileInfo(themeMetadataPath);
290 cachesTooOld = cacheFileInfo.lastModified().toTime_t() > metadataFileInfo.lastModified().toTime_t();
291 }
292 }
293
294 ThemeConfig config;
295 pixmapCache = new KImageCache(cacheFile, config.themeCacheKb() * 1024);
296
297 if (cachesTooOld) {
298 discardCache(PixmapCache | SvgElementsCache);
299 }
300 }
301
302 if (cacheTheme && !svgElementsCache) {
303 const QString svgElementsFileNameBase = "plasma-svgelements-" + themeName;
304 QString svgElementsFileName = svgElementsFileNameBase;
305 if (!themeVersion.isEmpty()) {
306 svgElementsFileName += "_v" + themeVersion;
307 }
308
309 // now we check for (and remove) old caches
310 foreach (const QString &file, KGlobal::dirs()->findAllResources("cache", svgElementsFileNameBase + "*")) {
311 if (cachesTooOld || !file.endsWith(svgElementsFileName)) {
312 QFile::remove(file);
313 }
314 }
315
316 const QString svgElementsFile = KStandardDirs::locateLocal("cache", svgElementsFileName);
317 svgElementsCache = KSharedConfig::openConfig(svgElementsFile);
318 }
319
320 return cacheTheme;
321}
322
323void ThemePrivate::onAppExitCleanup()
324{
325 pixmapsToCache.clear();
326 delete pixmapCache;
327 pixmapCache = 0;
328 cacheTheme = false;
329}
330
331QString ThemePrivate::findInTheme(const QString &image, const QString &theme, bool cache)
332{
333 if (cache && discoveries.contains(image)) {
334 return discoveries[image];
335 }
336
337 QString search;
338
339 if (locolor) {
340 search = QLatin1Literal("desktoptheme/") % theme % QLatin1Literal("/locolor/") % image;
341 search = KStandardDirs::locate("data", search);
342 } else if (!compositingActive) {
343 search = QLatin1Literal("desktoptheme/") % theme % QLatin1Literal("/opaque/") % image;
344 search = KStandardDirs::locate("data", search);
345 } else if (WindowEffects::isEffectAvailable(WindowEffects::BlurBehind)) {
346 search = QLatin1Literal("desktoptheme/") % theme % QLatin1Literal("/translucent/") % image;
347 search = KStandardDirs::locate("data", search);
348 }
349
350 //not found or compositing enabled
351 if (search.isEmpty()) {
352 search = QLatin1Literal("desktoptheme/") % theme % QLatin1Char('/') % image;
353 search = KStandardDirs::locate("data", search);
354 }
355
356 if (cache && !search.isEmpty()) {
357 discoveries.insert(image, search);
358 }
359
360 return search;
361}
362
363void ThemePrivate::compositingChanged(bool active)
364{
365#ifdef Q_WS_X11
366 if (compositingActive != active) {
367 compositingActive = active;
368 //kDebug() << QTime::currentTime();
369 scheduleThemeChangeNotification(PixmapCache | SvgElementsCache);
370 }
371#endif
372}
373
374void ThemePrivate::discardCache(CacheTypes caches)
375{
376 if (caches & PixmapCache) {
377 pixmapsToCache.clear();
378 saveTimer->stop();
379 if (pixmapCache) {
380 pixmapCache->clear();
381 }
382 } else {
383 // This deletes the object but keeps the on-disk cache for later use
384 delete pixmapCache;
385 pixmapCache = 0;
386 }
387
388 cachedStyleSheets.clear();
389
390 if (caches & SvgElementsCache) {
391 discoveries.clear();
392 invalidElements.clear();
393 svgElementsCache = 0;
394 }
395}
396
397void ThemePrivate::scheduledCacheUpdate()
398{
399 if (useCache()) {
400 QHashIterator<QString, QPixmap> it(pixmapsToCache);
401 while (it.hasNext()) {
402 it.next();
403 pixmapCache->insertPixmap(idsToCache[it.key()], it.value());
404 }
405 }
406
407 pixmapsToCache.clear();
408 keysToCache.clear();
409 idsToCache.clear();
410}
411
412void ThemePrivate::colorsChanged()
413{
414 colorScheme = KColorScheme(QPalette::Active, KColorScheme::Window, colors);
415 buttonColorScheme = KColorScheme(QPalette::Active, KColorScheme::Button, colors);
416 viewColorScheme = KColorScheme(QPalette::Active, KColorScheme::View, colors);
417 scheduleThemeChangeNotification(PixmapCache);
418}
419
420void ThemePrivate::blurBehindChanged(bool blur)
421{
422 if (blurActive != blur) {
423 blurActive = blur;
424 scheduleThemeChangeNotification(PixmapCache | SvgElementsCache);
425 }
426}
427
428void ThemePrivate::scheduleThemeChangeNotification(CacheTypes caches)
429{
430 cachesToDiscard |= caches;
431 updateNotificationTimer->start();
432}
433
434void ThemePrivate::notifyOfChanged()
435{
436 //kDebug() << cachesToDiscard;
437 discardCache(cachesToDiscard);
438 cachesToDiscard = NoCache;
439 emit q->themeChanged();
440}
441
442const QString ThemePrivate::processStyleSheet(const QString &css)
443{
444 QString stylesheet;
445 if (css.isEmpty()) {
446 stylesheet = cachedStyleSheets.value(DEFAULTSTYLE);
447 if (stylesheet.isEmpty()) {
448 stylesheet = QString("\n\
449 body {\n\
450 color: %textcolor;\n\
451 font-size: %fontsize;\n\
452 font-family: %fontfamily;\n\
453 }\n\
454 a:active { color: %activatedlink; }\n\
455 a:link { color: %link; }\n\
456 a:visited { color: %visitedlink; }\n\
457 a:hover { color: %hoveredlink; text-decoration: none; }\n\
458 ");
459 stylesheet = processStyleSheet(stylesheet);
460 cachedStyleSheets.insert(DEFAULTSTYLE, stylesheet);
461 }
462
463 return stylesheet;
464 } else if (css == "SVG") {
465 stylesheet = cachedStyleSheets.value(SVGSTYLE);
466 if (stylesheet.isEmpty()) {
467 QString skel = ".ColorScheme-%1{color:%2;}";
468
469 stylesheet += skel.arg("Text","%textcolor");
470 stylesheet += skel.arg("Background","%backgroundcolor");
471
472 stylesheet += skel.arg("ButtonText","%buttontextcolor");
473 stylesheet += skel.arg("ButtonBackground","%buttonbackgroundcolor");
474 stylesheet += skel.arg("ButtonHover","%buttonhovercolor");
475 stylesheet += skel.arg("ButtonFocus","%buttonfocuscolor");
476
477 stylesheet += skel.arg("ViewText","%viewtextcolor");
478 stylesheet += skel.arg("ViewBackground","%viewbackgroundcolor");
479 stylesheet += skel.arg("ViewHover","%viewhovercolor");
480 stylesheet += skel.arg("ViewFocus","%viewfocuscolor");
481
482 stylesheet = processStyleSheet(stylesheet);
483 cachedStyleSheets.insert(SVGSTYLE, stylesheet);
484 }
485
486 return stylesheet;
487 } else {
488 stylesheet = css;
489 }
490
491 QHash<QString, QString> elements;
492 // If you add elements here, make sure their names are sufficiently unique to not cause
493 // clashes between element keys
494 elements["%textcolor"] = q->color(Theme::TextColor).name();
495 elements["%backgroundcolor"] = q->color(Theme::BackgroundColor).name();
496 elements["%visitedlink"] = q->color(Theme::VisitedLinkColor).name();
497 elements["%activatedlink"] = q->color(Theme::HighlightColor).name();
498 elements["%hoveredlink"] = q->color(Theme::HighlightColor).name();
499 elements["%link"] = q->color(Theme::LinkColor).name();
500 elements["%buttontextcolor"] = q->color(Theme::ButtonTextColor).name();
501 elements["%buttonbackgroundcolor"] = q->color(Theme::ButtonBackgroundColor).name();
502 elements["%buttonhovercolor"] = q->color(Theme::ButtonHoverColor).name();
503 elements["%buttonfocuscolor"] = q->color(Theme::ButtonFocusColor).name();
504 elements["%viewtextcolor"] = q->color(Theme::ViewTextColor).name();
505 elements["%viewbackgroundcolor"] = q->color(Theme::ViewBackgroundColor).name();
506 elements["%viewhovercolor"] = q->color(Theme::ViewHoverColor).name();
507 elements["%viewfocuscolor"] = q->color(Theme::ViewFocusColor).name();
508
509 QFont font = q->font(Theme::DefaultFont);
510 elements["%fontsize"] = QString("%1pt").arg(font.pointSize());
511 elements["%fontfamily"] = font.family().split('[').first();
512 elements["%smallfontsize"] = QString("%1pt").arg(KGlobalSettings::smallestReadableFont().pointSize());
513
514 QHash<QString, QString>::const_iterator it = elements.constBegin();
515 QHash<QString, QString>::const_iterator itEnd = elements.constEnd();
516 for ( ; it != itEnd; ++it) {
517 stylesheet.replace(it.key(), it.value());
518 }
519 return stylesheet;
520}
521
522class ThemeSingleton
523{
524public:
525 ThemeSingleton()
526 {
527 self.d->isDefault = true;
528
529 //FIXME: if/when kconfig gets change notification, this will be unnecessary
530 KDirWatch::self()->addFile(KStandardDirs::locateLocal("config", ThemePrivate::themeRcFile));
531 QObject::connect(KDirWatch::self(), SIGNAL(created(QString)),
532 &self, SLOT(settingsFileChanged(QString)),
533 Qt::UniqueConnection);
534 QObject::connect(KDirWatch::self(), SIGNAL(dirty(QString)),
535 &self, SLOT(settingsFileChanged(QString)),
536 Qt::UniqueConnection);
537 }
538
539 Theme self;
540};
541
542K_GLOBAL_STATIC(ThemeSingleton, privateThemeSelf)
543
544Theme *Theme::defaultTheme()
545{
546 return &privateThemeSelf->self;
547}
548
549Theme::Theme(QObject *parent)
550 : QObject(parent),
551 d(new ThemePrivate(this))
552{
553 settingsChanged();
554 if (QCoreApplication::instance()) {
555 connect(QCoreApplication::instance(), SIGNAL(aboutToQuit()),
556 this, SLOT(onAppExitCleanup()));
557 }
558}
559
560Theme::Theme(const QString &themeName, QObject *parent)
561 : QObject(parent),
562 d(new ThemePrivate(this))
563{
564 // turn off caching so we don't accidently trigger unnecessary disk activity at this point
565 bool useCache = d->cacheTheme;
566 d->cacheTheme = false;
567 setThemeName(themeName);
568 d->cacheTheme = useCache;
569 if (QCoreApplication::instance()) {
570 connect(QCoreApplication::instance(), SIGNAL(aboutToQuit()),
571 this, SLOT(onAppExitCleanup()));
572 }
573}
574
575Theme::~Theme()
576{
577 if (d->svgElementsCache) {
578 QHashIterator<QString, QSet<QString> > it(d->invalidElements);
579 while (it.hasNext()) {
580 it.next();
581 KConfigGroup imageGroup(d->svgElementsCache, it.key());
582 imageGroup.writeEntry("invalidElements", it.value().toList()); //FIXME: add QSet support to KConfig
583 }
584 }
585
586 d->onAppExitCleanup();
587 delete d;
588}
589
590PackageStructure::Ptr Theme::packageStructure()
591{
592 if (!ThemePrivate::packageStructure) {
593 ThemePrivate::packageStructure = new ThemePackage();
594 }
595
596 return ThemePrivate::packageStructure;
597}
598
599KPluginInfo::List Theme::listThemeInfo()
600{
601 const QStringList themes = KGlobal::dirs()->findAllResources("data", "desktoptheme/*/metadata.desktop",
602 KStandardDirs::NoDuplicates);
603 return KPluginInfo::fromFiles(themes);
604}
605
606void ThemePrivate::settingsFileChanged(const QString &file)
607{
608 if (file == themeMetadataPath) {
609 const KPluginInfo pluginInfo(themeMetadataPath);
610 if (themeVersion != pluginInfo.version()) {
611 scheduleThemeChangeNotification(SvgElementsCache);
612 }
613 } else if (file.endsWith(themeRcFile)) {
614 config().config()->reparseConfiguration();
615 q->settingsChanged();
616 }
617}
618
619void Theme::settingsChanged()
620{
621 KConfigGroup cg = d->config();
622 d->setThemeName(cg.readEntry("name", ThemePrivate::defaultTheme), false);
623 cg = KConfigGroup(cg.config(), "PlasmaToolTips");
624 d->toolTipDelay = cg.readEntry("Delay", 700);
625}
626
627void Theme::setThemeName(const QString &themeName)
628{
629 d->setThemeName(themeName, true);
630}
631
632void ThemePrivate::processWallpaperSettings(KConfigBase *metadata)
633{
634 if (!defaultWallpaperTheme.isEmpty() && defaultWallpaperTheme != DEFAULT_WALLPAPER_THEME) {
635 return;
636 }
637
638 KConfigGroup cg;
639 if (metadata->hasGroup("Wallpaper")) {
640 // we have a theme color config, so let's also check to see if
641 // there is a wallpaper defined in there.
642 cg = KConfigGroup(metadata, "Wallpaper");
643 } else {
644 // since we didn't find an entry in the theme, let's look in the main
645 // theme config
646 cg = config();
647 }
648
649 defaultWallpaperTheme = cg.readEntry("defaultWallpaperTheme", DEFAULT_WALLPAPER_THEME);
650 defaultWallpaperSuffix = cg.readEntry("defaultFileSuffix", DEFAULT_WALLPAPER_SUFFIX);
651 defaultWallpaperWidth = cg.readEntry("defaultWidth", DEFAULT_WALLPAPER_WIDTH);
652 defaultWallpaperHeight = cg.readEntry("defaultHeight", DEFAULT_WALLPAPER_HEIGHT);
653}
654
655void ThemePrivate::processAnimationSettings(const QString &theme, KConfigBase *metadata)
656{
657 KConfigGroup cg(metadata, "Animations");
658 const QString animDir = QLatin1Literal("desktoptheme/") % theme % QLatin1Literal("/animations/");
659 foreach (const QString &path, cg.keyList()) {
660 const QStringList anims = cg.readEntry(path, QStringList());
661 foreach (const QString &anim, anims) {
662 if (!animationMapping.contains(anim)) {
663 kDebug() << "Registering animation. animDir: " << animDir
664 << "\tanim: " << anim
665 << "\tpath: " << path << "\t*******\n\n\n";
666 //key: desktoptheme/default/animations/+ all.js
667 //value: ZoomAnimation
668 animationMapping.insert(anim, animDir % path);
669 } else {
670 kDebug() << "************Animation already registered!\n\n\n";
671 }
672 }
673 }
674
675}
676
677void ThemePrivate::setThemeName(const QString &tempThemeName, bool writeSettings)
678{
679 //kDebug() << tempThemeName;
680 QString theme = tempThemeName;
681 if (theme.isEmpty() || theme == themeName) {
682 // let's try and get the default theme at least
683 if (themeName.isEmpty()) {
684 theme = ThemePrivate::defaultTheme;
685 } else {
686 return;
687 }
688 }
689
690 // we have one special theme: essentially a dummy theme used to cache things with
691 // the system colors.
692 bool realTheme = theme != systemColorsTheme;
693 if (realTheme) {
694 QString themePath = KStandardDirs::locate("data", QLatin1Literal("desktoptheme/") % theme % QLatin1Char('/'));
695 if (themePath.isEmpty() && themeName.isEmpty()) {
696 themePath = KStandardDirs::locate("data", "desktoptheme/default/");
697
698 if (themePath.isEmpty()) {
699 return;
700 }
701
702 theme = ThemePrivate::defaultTheme;
703 }
704 }
705
706 // check again as ThemePrivate::defaultTheme might be empty
707 if (themeName == theme) {
708 return;
709 }
710
711 themeName = theme;
712
713 // load the color scheme config
714 const QString colorsFile = realTheme ? KStandardDirs::locate("data", QLatin1Literal("desktoptheme/") % theme % QLatin1Literal("/colors"))
715 : QString();
716
717 //kDebug() << "we're going for..." << colorsFile << "*******************";
718
719 // load the wallpaper settings, if any
720 if (realTheme) {
721 const QString metadataPath(KStandardDirs::locate("data", QLatin1Literal("desktoptheme/") % theme % QLatin1Literal("/metadata.desktop")));
722 KConfig metadata(metadataPath);
723
724 processWallpaperSettings(&metadata);
725
726 AnimationScriptEngine::clearAnimations();
727 animationMapping.clear();
728 processAnimationSettings(themeName, &metadata);
729
730 KConfigGroup cg(&metadata, "Settings");
731 useNativeWidgetStyle = cg.readEntry("UseNativeWidgetStyle", false);
732 QString fallback = cg.readEntry("FallbackTheme", QString());
733
734 fallbackThemes.clear();
735 while (!fallback.isEmpty() && !fallbackThemes.contains(fallback)) {
736 fallbackThemes.append(fallback);
737
738 QString metadataPath(KStandardDirs::locate("data", QLatin1Literal("desktoptheme/") % theme % QLatin1Literal("/metadata.desktop")));
739 KConfig metadata(metadataPath);
740 KConfigGroup cg(&metadata, "Settings");
741 fallback = cg.readEntry("FallbackTheme", QString());
742 }
743
744 if (!fallbackThemes.contains("oxygen")) {
745 fallbackThemes.append("oxygen");
746 }
747
748 if (!fallbackThemes.contains(ThemePrivate::defaultTheme)) {
749 fallbackThemes.append(ThemePrivate::defaultTheme);
750 }
751
752 foreach (const QString &theme, fallbackThemes) {
753 QString metadataPath(KStandardDirs::locate("data", QLatin1Literal("desktoptheme/") % theme % QLatin1Literal("/metadata.desktop")));
754 KConfig metadata(metadataPath);
755 processAnimationSettings(theme, &metadata);
756 processWallpaperSettings(&metadata);
757 }
758 }
759
760 if (colorsFile.isEmpty()) {
761 colors = 0;
762 QObject::connect(KGlobalSettings::self(), SIGNAL(kdisplayPaletteChanged()),
763 q, SLOT(colorsChanged()), Qt::UniqueConnection);
764 } else {
765 QObject::disconnect(KGlobalSettings::self(), SIGNAL(kdisplayPaletteChanged()),
766 q, SLOT(colorsChanged()));
767 colors = KSharedConfig::openConfig(colorsFile);
768 }
769
770 colorScheme = KColorScheme(QPalette::Active, KColorScheme::Window, colors);
771 buttonColorScheme = KColorScheme(QPalette::Active, KColorScheme::Button, colors);
772 viewColorScheme = KColorScheme(QPalette::Active, KColorScheme::View, colors);
773 hasWallpapers = KStandardDirs::exists(KStandardDirs::locateLocal("data", QLatin1Literal("desktoptheme/") % theme % QLatin1Literal("/wallpapers/")));
774
775 if (realTheme && isDefault && writeSettings) {
776 // we're the default theme, let's save our state
777 KConfigGroup &cg = config();
778 if (ThemePrivate::defaultTheme == themeName) {
779 cg.deleteEntry("name");
780 } else {
781 cg.writeEntry("name", themeName);
782 }
783 cg.sync();
784 }
785
786 scheduleThemeChangeNotification(SvgElementsCache);
787}
788
789QString Theme::themeName() const
790{
791 return d->themeName;
792}
793
794QString Theme::imagePath(const QString &name) const
795{
796 // look for a compressed svg file in the theme
797 if (name.contains("../") || name.isEmpty()) {
798 // we don't support relative paths
799 //kDebug() << "Theme says: bad image path " << name;
800 return QString();
801 }
802
803 const QString svgzName = name % QLatin1Literal(".svgz");
804 QString path = d->findInTheme(svgzName, d->themeName);
805
806 if (path.isEmpty()) {
807 // try for an uncompressed svg file
808 const QString svgName = name % QLatin1Literal(".svg");
809 path = d->findInTheme(svgName, d->themeName);
810
811 // search in fallback themes if necessary
812 for (int i = 0; path.isEmpty() && i < d->fallbackThemes.count(); ++i) {
813 if (d->themeName == d->fallbackThemes[i]) {
814 continue;
815 }
816
817 // try a compressed svg file in the fallback theme
818 path = d->findInTheme(svgzName, d->fallbackThemes[i]);
819
820 if (path.isEmpty()) {
821 // try an uncompressed svg file in the fallback theme
822 path = d->findInTheme(svgName, d->fallbackThemes[i]);
823 }
824 }
825 }
826
827 /*
828 if (path.isEmpty()) {
829 kDebug() << "Theme says: bad image path " << name;
830 }
831 */
832
833 return path;
834}
835
836QString Theme::styleSheet(const QString &css) const
837{
838 return d->processStyleSheet(css);
839}
840
841QString Theme::animationPath(const QString &name) const
842{
843 const QString path = d->animationMapping.value(name);
844 if (path.isEmpty()) {
845 //kError() << "****** FAILED TO FIND IN MAPPING!";
846 return path;
847 }
848
849 return KStandardDirs::locate("data", path);
850}
851
852QString Theme::wallpaperPath(const QSize &size) const
853{
854 QString fullPath;
855 QString image = d->defaultWallpaperTheme;
856
857 image.append("/contents/images/%1x%2").append(d->defaultWallpaperSuffix);
858 QString defaultImage = image.arg(d->defaultWallpaperWidth).arg(d->defaultWallpaperHeight);
859
860 if (size.isValid()) {
861 // try to customize the paper to the size requested
862 //TODO: this should do better than just fallback to the default size.
863 // a "best fit" matching would be far better, so we don't end
864 // up returning a 1920x1200 wallpaper for a 640x480 request ;)
865 image = image.arg(size.width()).arg(size.height());
866 } else {
867 image = defaultImage;
868 }
869
870 //TODO: the theme's wallpaper overrides regularly installed wallpapers.
871 // should it be possible for user installed (e.g. locateLocal) wallpapers
872 // to override the theme?
873 if (d->hasWallpapers) {
874 // check in the theme first
875 fullPath = d->findInTheme(QLatin1Literal("wallpapers/") % image, d->themeName);
876
877 if (fullPath.isEmpty()) {
878 fullPath = d->findInTheme(QLatin1Literal("wallpapers/") % defaultImage, d->themeName);
879 }
880 }
881
882 if (fullPath.isEmpty()) {
883 // we failed to find it in the theme, so look in the standard directories
884 //kDebug() << "looking for" << image;
885 fullPath = KStandardDirs::locate("wallpaper", image);
886 }
887
888 if (fullPath.isEmpty()) {
889 // we still failed to find it in the theme, so look for the default in
890 // the standard directories
891 //kDebug() << "looking for" << defaultImage;
892 fullPath = KStandardDirs::locate("wallpaper", defaultImage);
893
894 if (fullPath.isEmpty()) {
895 kDebug() << "exhausted every effort to find a wallpaper.";
896 }
897 }
898
899 return fullPath;
900}
901
902bool Theme::currentThemeHasImage(const QString &name) const
903{
904 if (name.contains("../")) {
905 // we don't support relative paths
906 return false;
907 }
908
909 return !(d->findInTheme(name % QLatin1Literal(".svgz"), d->themeName, false).isEmpty()) ||
910 !(d->findInTheme(name % QLatin1Literal(".svg"), d->themeName, false).isEmpty());
911}
912
913KSharedConfigPtr Theme::colorScheme() const
914{
915 return d->colors;
916}
917
918QColor Theme::color(ColorRole role) const
919{
920 switch (role) {
921 case TextColor:
922 return d->colorScheme.foreground(KColorScheme::NormalText).color();
923
924 case HighlightColor:
925 return d->colorScheme.decoration(KColorScheme::HoverColor).color();
926
927 case BackgroundColor:
928 return d->colorScheme.background(KColorScheme::NormalBackground).color();
929
930 case ButtonTextColor:
931 return d->buttonColorScheme.foreground(KColorScheme::NormalText).color();
932
933 case ButtonBackgroundColor:
934 return d->buttonColorScheme.background(KColorScheme::NormalBackground).color();
935
936 case ButtonHoverColor:
937 return d->buttonColorScheme.decoration(KColorScheme::HoverColor).color();
938
939 case ButtonFocusColor:
940 return d->buttonColorScheme.decoration(KColorScheme::FocusColor).color();
941
942 case ViewTextColor:
943 return d->viewColorScheme.foreground(KColorScheme::NormalText).color();
944
945 case ViewBackgroundColor:
946 return d->viewColorScheme.background(KColorScheme::NormalBackground).color();
947
948 case ViewHoverColor:
949 return d->viewColorScheme.decoration(KColorScheme::HoverColor).color();
950
951 case ViewFocusColor:
952 return d->viewColorScheme.decoration(KColorScheme::FocusColor).color();
953
954 case LinkColor:
955 return d->viewColorScheme.foreground(KColorScheme::LinkText).color();
956
957 case VisitedLinkColor:
958 return d->viewColorScheme.foreground(KColorScheme::VisitedText).color();
959 }
960
961 return QColor();
962}
963
964void Theme::setFont(const QFont &font, FontRole role)
965{
966 Q_UNUSED(role)
967 d->generalFont = font;
968}
969
970QFont Theme::font(FontRole role) const
971{
972 switch (role) {
973 case DesktopFont: {
974 KConfigGroup cg(KGlobal::config(), "General");
975 return cg.readEntry("desktopFont", d->generalFont);
976 }
977 break;
978
979 case DefaultFont:
980 default:
981 return d->generalFont;
982 break;
983
984 case SmallestFont:
985 return KGlobalSettings::smallestReadableFont();
986 break;
987 }
988
989 return d->generalFont;
990}
991
992QFontMetrics Theme::fontMetrics() const
993{
994 //TODO: allow this to be overridden with a plasma specific font?
995 return QFontMetrics(d->generalFont);
996}
997
998bool Theme::windowTranslucencyEnabled() const
999{
1000 return d->compositingActive;
1001}
1002
1003void Theme::setUseGlobalSettings(bool useGlobal)
1004{
1005 if (d->useGlobal == useGlobal) {
1006 return;
1007 }
1008
1009 d->useGlobal = useGlobal;
1010 d->cfg = KConfigGroup();
1011 d->themeName.clear();
1012 settingsChanged();
1013}
1014
1015bool Theme::useGlobalSettings() const
1016{
1017 return d->useGlobal;
1018}
1019
1020bool Theme::useNativeWidgetStyle() const
1021{
1022 return d->useNativeWidgetStyle;
1023}
1024
1025bool Theme::findInCache(const QString &key, QPixmap &pix)
1026{
1027 if (d->useCache()) {
1028 const QString id = d->keysToCache.value(key);
1029 if (d->pixmapsToCache.contains(id)) {
1030 pix = d->pixmapsToCache.value(id);
1031 return !pix.isNull();
1032 }
1033
1034 QPixmap temp;
1035 if (d->pixmapCache->findPixmap(key, &temp) && !temp.isNull()) {
1036 pix = temp;
1037 return true;
1038 }
1039 }
1040
1041 return false;
1042}
1043
1044// BIC FIXME: Should be merged with the other findInCache method above when we break BC
1045bool Theme::findInCache(const QString &key, QPixmap &pix, unsigned int lastModified)
1046{
1047 if (d->useCache() && lastModified > uint(d->pixmapCache->lastModifiedTime())) {
1048 return false;
1049 }
1050
1051 return findInCache(key, pix);
1052}
1053
1054void Theme::insertIntoCache(const QString& key, const QPixmap& pix)
1055{
1056 if (d->useCache()) {
1057 d->pixmapCache->insertPixmap(key, pix);
1058 }
1059}
1060
1061void Theme::insertIntoCache(const QString& key, const QPixmap& pix, const QString& id)
1062{
1063 if (d->useCache()) {
1064 d->pixmapsToCache.insert(id, pix);
1065
1066 if (d->idsToCache.contains(id)) {
1067 d->keysToCache.remove(d->idsToCache[id]);
1068 }
1069
1070 d->keysToCache.insert(key, id);
1071 d->idsToCache.insert(id, key);
1072 d->saveTimer->start();
1073 }
1074}
1075
1076bool Theme::findInRectsCache(const QString &image, const QString &element, QRectF &rect) const
1077{
1078 if (!d->useCache()) {
1079 return false;
1080 }
1081
1082 KConfigGroup imageGroup(d->svgElementsCache, image);
1083 rect = imageGroup.readEntry(element % QLatin1Literal("Size"), QRectF());
1084
1085 if (rect.isValid()) {
1086 return true;
1087 }
1088
1089 //Name starting by _ means the element is empty and we're asked for the size of
1090 //the whole image, so the whole image is never invalid
1091 if (element.indexOf('_') <= 0) {
1092 return false;
1093 }
1094
1095 bool invalid = false;
1096
1097 QHash<QString, QSet<QString> >::iterator it = d->invalidElements.find(image);
1098 if (it == d->invalidElements.end()) {
1099 QSet<QString> elements = imageGroup.readEntry("invalidElements", QStringList()).toSet();
1100 d->invalidElements.insert(image, elements);
1101 invalid = elements.contains(element);
1102 } else {
1103 invalid = it.value().contains(element);
1104 }
1105
1106 return invalid;
1107}
1108
1109QStringList Theme::listCachedRectKeys(const QString &image) const
1110{
1111 if (!d->useCache()) {
1112 return QStringList();
1113 }
1114
1115 KConfigGroup imageGroup(d->svgElementsCache, image);
1116 QStringList keys = imageGroup.keyList();
1117
1118 QMutableListIterator<QString> i(keys);
1119 while (i.hasNext()) {
1120 QString key = i.next();
1121 if (key.endsWith("Size")) {
1122 // The actual cache id used from outside doesn't end on "Size".
1123 key.resize(key.size() - 4);
1124 i.setValue(key);
1125 } else {
1126 i.remove();
1127 }
1128 }
1129 return keys;
1130}
1131
1132void Theme::insertIntoRectsCache(const QString& image, const QString &element, const QRectF &rect)
1133{
1134 if (!d->useCache()) {
1135 return;
1136 }
1137
1138 if (rect.isValid()) {
1139 KConfigGroup imageGroup(d->svgElementsCache, image);
1140 imageGroup.writeEntry(element % QLatin1Literal("Size"), rect);
1141 } else {
1142 QHash<QString, QSet<QString> >::iterator it = d->invalidElements.find(image);
1143 if (it == d->invalidElements.end()) {
1144 d->invalidElements[image].insert(element);
1145 } else if (!it.value().contains(element)) {
1146 if (it.value().count() > 1000) {
1147 it.value().erase(it.value().begin());
1148 }
1149
1150 it.value().insert(element);
1151 }
1152 }
1153}
1154
1155void Theme::invalidateRectsCache(const QString& image)
1156{
1157 if (d->useCache()) {
1158 KConfigGroup imageGroup(d->svgElementsCache, image);
1159 imageGroup.deleteGroup();
1160 }
1161
1162 d->invalidElements.remove(image);
1163}
1164
1165void Theme::releaseRectsCache(const QString &image)
1166{
1167 QHash<QString, QSet<QString> >::iterator it = d->invalidElements.find(image);
1168 if (it != d->invalidElements.end()) {
1169 if (d->useCache()) {
1170 KConfigGroup imageGroup(d->svgElementsCache, it.key());
1171 imageGroup.writeEntry("invalidElements", it.value().toList());
1172 }
1173
1174 d->invalidElements.erase(it);
1175 }
1176}
1177
1178void Theme::setCacheLimit(int kbytes)
1179{
1180 Q_UNUSED(kbytes)
1181 if (d->useCache()) {
1182 ;
1183 // Too late for you bub.
1184 // d->pixmapCache->setCacheLimit(kbytes);
1185 }
1186}
1187
1188KUrl Theme::homepage() const
1189{
1190 const QString metadataPath(KStandardDirs::locate("data", QLatin1Literal("desktoptheme/") % d->themeName % QLatin1Literal("/metadata.desktop")));
1191 KConfig metadata(metadataPath);
1192 KConfigGroup brandConfig(&metadata, "Branding");
1193 return brandConfig.readEntry("homepage", KUrl("http://www.kde.org"));
1194}
1195
1196int Theme::toolTipDelay() const
1197{
1198 return d->toolTipDelay;
1199}
1200
1201}
1202
1203#include <theme.moc>
1204