1 /*
2     SPDX-FileCopyrightText: 2018 Michail Vourlakos <mvourlakos@gmail.com>
3     SPDX-License-Identifier: GPL-2.0-or-later
4 */
5 
6 #include "theme.h"
7 
8 // local
9 #include "lattecorona.h"
10 #include "panelbackground.h"
11 #include "../../layouts/importer.h"
12 #include "../../view/panelshadows_p.h"
13 #include "../../wm/schemecolors.h"
14 #include "../../tools/commontools.h"
15 
16 // Qt
17 #include <QDebug>
18 #include <QDir>
19 #include <QPainter>
20 
21 // KDE
22 #include <KDirWatch>
23 #include <KConfigGroup>
24 #include <KSharedConfig>
25 
26 // X11
27 #include <KWindowSystem>
28 
29 #define DEFAULTCOLORSCHEME "default.colors"
30 #define REVERSEDCOLORSCHEME "reversed.colors"
31 
32 namespace Latte {
33 namespace PlasmaExtended {
34 
Theme(KSharedConfig::Ptr config,QObject * parent)35 Theme::Theme(KSharedConfig::Ptr config, QObject *parent) :
36     QObject(parent),
37     m_themeGroup(KConfigGroup(config, QStringLiteral("PlasmaThemeExtended"))),
38     m_backgroundTopEdge(new PanelBackground(Plasma::Types::TopEdge, this)),
39     m_backgroundLeftEdge(new PanelBackground(Plasma::Types::LeftEdge, this)),
40     m_backgroundBottomEdge(new PanelBackground(Plasma::Types::BottomEdge, this)),
41     m_backgroundRightEdge(new PanelBackground(Plasma::Types::RightEdge, this))
42 {
43     qmlRegisterTypes();
44 
45     m_corona = qobject_cast<Latte::Corona *>(parent);
46 
47     //! compositing tracking
48     if (KWindowSystem::isPlatformWayland()) {
49         //! TODO: Wayland compositing active
50         m_compositing = true;
51     } else {
52         connect(KWindowSystem::self(), &KWindowSystem::compositingChanged
53                 , this, [&](bool enabled) {
54             if (m_compositing == enabled)
55                 return;
56 
57             m_compositing = enabled;
58             emit compositingChanged();
59         });
60 
61         m_compositing = KWindowSystem::compositingActive();
62     }
63     //!
64 
65     loadConfig();
66 
67     connect(this, &Theme::compositingChanged, this, &Theme::updateBackgrounds);
68     connect(this, &Theme::outlineWidthChanged, this, &Theme::saveConfig);
69 
70     connect(&m_theme, &Plasma::Theme::themeChanged, this, &Theme::load);
71     connect(&m_theme, &Plasma::Theme::themeChanged, this, &Theme::themeChanged);
72 }
73 
load()74 void Theme::load()
75 {
76     loadThemePaths();
77     updateBackgrounds();
78     updateMarginsAreaValues();
79 }
80 
~Theme()81 Theme::~Theme()
82 {
83     saveConfig();
84 
85     m_defaultScheme->deleteLater();
86     m_reversedScheme->deleteLater();
87 }
88 
hasShadow() const89 bool Theme::hasShadow() const
90 {
91     return m_hasShadow;
92 }
93 
isLightTheme() const94 bool Theme::isLightTheme() const
95 {
96     return m_isLightTheme;
97 }
98 
isDarkTheme() const99 bool Theme::isDarkTheme() const
100 {
101     return !m_isLightTheme;
102 }
103 
outlineWidth() const104 int Theme::outlineWidth() const
105 {
106     return m_outlineWidth;
107 }
108 
setOutlineWidth(int width)109 void Theme::setOutlineWidth(int width)
110 {
111     if (m_outlineWidth == width) {
112         return;
113     }
114 
115     m_outlineWidth = width;
116     emit outlineWidthChanged();
117 }
118 
marginsAreaTop() const119 int Theme::marginsAreaTop() const
120 {
121     return m_marginsAreaTop;
122 }
123 
marginsAreaLeft() const124 int Theme::marginsAreaLeft() const
125 {
126     return m_marginsAreaLeft;
127 }
128 
marginsAreaBottom() const129 int Theme::marginsAreaBottom() const
130 {
131     return m_marginsAreaBottom;
132 }
133 
marginsAreaRight() const134 int Theme::marginsAreaRight() const
135 {
136     return m_marginsAreaRight;
137 }
138 
139 
backgroundTopEdge() const140 PanelBackground *Theme::backgroundTopEdge() const
141 {
142     return m_backgroundTopEdge;
143 }
144 
backgroundLeftEdge() const145 PanelBackground *Theme::backgroundLeftEdge() const
146 {
147     return m_backgroundLeftEdge;
148 }
149 
backgroundBottomEdge() const150 PanelBackground *Theme::backgroundBottomEdge() const
151 {
152     return m_backgroundBottomEdge;
153 }
154 
backgroundRightEdge() const155 PanelBackground *Theme::backgroundRightEdge() const
156 {
157     return m_backgroundRightEdge;
158 }
159 
defaultTheme() const160 WindowSystem::SchemeColors *Theme::defaultTheme() const
161 {
162     return m_defaultScheme;
163 }
164 
lightTheme() const165 WindowSystem::SchemeColors *Theme::lightTheme() const
166 {
167     return m_isLightTheme ? m_defaultScheme : m_reversedScheme;
168 }
169 
darkTheme() const170 WindowSystem::SchemeColors *Theme::darkTheme() const
171 {
172     return !m_isLightTheme ? m_defaultScheme : m_reversedScheme;
173 }
174 
175 
setOriginalSchemeFile(const QString & file)176 void Theme::setOriginalSchemeFile(const QString &file)
177 {
178     if (m_originalSchemePath == file) {
179         return;
180     }
181 
182     m_originalSchemePath = file;
183 
184     qDebug() << "plasma theme original colors ::: " << m_originalSchemePath;
185 
186     updateDefaultScheme();
187     updateReversedScheme();
188 
189     loadThemeLightness();
190 
191     emit themeChanged();
192 }
193 
194 //! WM records need to be updated based on the colors that
195 //! plasma will use in order to be consistent. Such an example
196 //! are the Breeze color schemes that have different values for
197 //! WM and the plasma theme records
updateDefaultScheme()198 void Theme::updateDefaultScheme()
199 {
200     QString defaultFilePath = m_extendedThemeDir.path() + "/" + DEFAULTCOLORSCHEME;
201     if (QFileInfo(defaultFilePath).exists()) {
202         QFile(defaultFilePath).remove();
203     }
204 
205     QFile(m_originalSchemePath).copy(defaultFilePath);
206     m_defaultSchemePath = defaultFilePath;
207 
208     updateDefaultSchemeValues();
209 
210     if (m_defaultScheme) {
211         disconnect(m_defaultScheme, &WindowSystem::SchemeColors::colorsChanged, this, &Theme::loadThemeLightness);
212         m_defaultScheme->deleteLater();
213     }
214 
215     m_defaultScheme = new WindowSystem::SchemeColors(this, m_defaultSchemePath, true);
216     connect(m_defaultScheme, &WindowSystem::SchemeColors::colorsChanged, this, &Theme::loadThemeLightness);
217 
218     qDebug() << "plasma theme default colors ::: " << m_defaultSchemePath;
219 }
220 
updateDefaultSchemeValues()221 void Theme::updateDefaultSchemeValues()
222 {
223     //! update WM values based on original scheme
224     KSharedConfigPtr originalPtr = KSharedConfig::openConfig(m_originalSchemePath);
225     KSharedConfigPtr defaultPtr = KSharedConfig::openConfig(m_defaultSchemePath);
226 
227     if (originalPtr && defaultPtr) {
228         KConfigGroup normalWindowGroup(originalPtr, "Colors:Window");
229         KConfigGroup defaultWMGroup(defaultPtr, "WM");
230 
231         defaultWMGroup.writeEntry("activeBackground", normalWindowGroup.readEntry("BackgroundNormal", QColor()));
232         defaultWMGroup.writeEntry("activeForeground", normalWindowGroup.readEntry("ForegroundNormal", QColor()));
233 
234         defaultWMGroup.sync();
235     }
236 }
237 
updateReversedScheme()238 void Theme::updateReversedScheme()
239 {
240     QString reversedFilePath = m_extendedThemeDir.path() + "/" + REVERSEDCOLORSCHEME;
241 
242     if (QFileInfo(reversedFilePath).exists()) {
243         QFile(reversedFilePath).remove();
244     }
245 
246     QFile(m_originalSchemePath).copy(reversedFilePath);
247     m_reversedSchemePath = reversedFilePath;
248 
249     updateReversedSchemeValues();
250 
251     if (m_reversedScheme) {
252         m_reversedScheme->deleteLater();
253     }
254 
255     m_reversedScheme = new WindowSystem::SchemeColors(this, m_reversedSchemePath, true);
256 
257     qDebug() << "plasma theme reversed colors ::: " << m_reversedSchemePath;
258 }
259 
updateReversedSchemeValues()260 void Theme::updateReversedSchemeValues()
261 {
262     //! reverse values based on original scheme
263     KSharedConfigPtr originalPtr = KSharedConfig::openConfig(m_originalSchemePath);
264     KSharedConfigPtr reversedPtr = KSharedConfig::openConfig(m_reversedSchemePath);
265 
266     if (originalPtr && reversedPtr) {
267         for (const auto &groupName : reversedPtr->groupList()) {
268             if (groupName != "Colors:Button" && groupName != "Colors:Selection") {
269                 KConfigGroup reversedGroup(reversedPtr, groupName);
270 
271                 if (reversedGroup.keyList().contains("BackgroundNormal")
272                         && reversedGroup.keyList().contains("ForegroundNormal")) {
273                     //! reverse usual text/background values
274                     KConfigGroup originalGroup(originalPtr, groupName);
275 
276                     reversedGroup.writeEntry("BackgroundNormal", originalGroup.readEntry("ForegroundNormal", QColor()));
277                     reversedGroup.writeEntry("ForegroundNormal", originalGroup.readEntry("BackgroundNormal", QColor()));
278 
279                     reversedGroup.sync();
280                 }
281             }
282         }
283 
284         //! update WM group
285         KConfigGroup reversedWMGroup(reversedPtr, "WM");
286         KConfigGroup normalWindowGroup(originalPtr, "Colors:Window");
287 
288         if (reversedWMGroup.keyList().contains("activeBackground")
289                 && reversedWMGroup.keyList().contains("activeForeground")
290                 && reversedWMGroup.keyList().contains("inactiveBackground")
291                 && reversedWMGroup.keyList().contains("inactiveForeground")) {
292             //! reverse usual wm titlebar values
293             KConfigGroup originalGroup(originalPtr, "WM");
294             reversedWMGroup.writeEntry("activeBackground", normalWindowGroup.readEntry("ForegroundNormal", QColor()));
295             reversedWMGroup.writeEntry("activeForeground", normalWindowGroup.readEntry("BackgroundNormal", QColor()));
296             reversedWMGroup.writeEntry("inactiveBackground", originalGroup.readEntry("inactiveForeground", QColor()));
297             reversedWMGroup.writeEntry("inactiveForeground", originalGroup.readEntry("inactiveBackground", QColor()));
298             reversedWMGroup.sync();
299         }
300 
301         if (reversedWMGroup.keyList().contains("activeBlend")
302                 && reversedWMGroup.keyList().contains("inactiveBlend")) {
303             KConfigGroup originalGroup(originalPtr, "WM");
304             reversedWMGroup.writeEntry("activeBlend", originalGroup.readEntry("inactiveBlend", QColor()));
305             reversedWMGroup.writeEntry("inactiveBlend", originalGroup.readEntry("activeBlend", QColor()));
306             reversedWMGroup.sync();
307         }
308 
309         //! update scheme name
310         QString originalSchemeName = WindowSystem::SchemeColors::schemeName(m_originalSchemePath);
311         KConfigGroup generalGroup(reversedPtr, "General");
312         generalGroup.writeEntry("Name", originalSchemeName + "_reversed");
313         generalGroup.sync();
314     }
315 }
316 
updateBackgrounds()317 void Theme::updateBackgrounds()
318 {
319     updateHasShadow();
320 
321     m_backgroundTopEdge->update();
322     m_backgroundLeftEdge->update();
323     m_backgroundBottomEdge->update();
324     m_backgroundRightEdge->update();
325 }
326 
updateHasShadow()327 void Theme::updateHasShadow()
328 {
329     Plasma::Svg *svg = new Plasma::Svg(this);
330     svg->setImagePath(QStringLiteral("widgets/panel-background"));
331     svg->resize();
332 
333     QString cornerId = "shadow-topleft";
334     QImage corner = svg->image(svg->elementSize(cornerId), cornerId);
335 
336     int fullTransparentPixels = 0;
337 
338     for(int c=0; c<corner.width(); ++c) {
339         for(int r=0; r<corner.height(); ++r) {
340             QRgb *line = (QRgb *)corner.scanLine(r);
341             QRgb point = line[c];
342 
343             if (qAlpha(point) == 0) {
344                 fullTransparentPixels++;
345             }
346         }
347     }
348 
349     int pixels = (corner.width() * corner.height());
350 
351     m_hasShadow = (fullTransparentPixels != pixels );
352     emit hasShadowChanged();
353 
354     qDebug() << "  PLASMA THEME TOPLEFT SHADOW :: pixels : " << pixels << "  transparent pixels" << fullTransparentPixels << " | HAS SHADOWS :" << m_hasShadow;
355 
356     svg->deleteLater();
357 }
358 
loadThemePaths()359 void Theme::loadThemePaths()
360 {
361     m_themePath = Layouts::Importer::standardPath("plasma/desktoptheme/" + m_theme.themeName());
362 
363     if (QDir(m_themePath+"/widgets").exists()) {
364         m_themeWidgetsPath = m_themePath + "/widgets";
365     } else {
366         m_themeWidgetsPath = Layouts::Importer::standardPath("plasma/desktoptheme/default/widgets");
367     }
368 
369     qDebug() << "current plasma theme ::: " << m_theme.themeName();
370     qDebug() << "theme path ::: " << m_themePath;
371     qDebug() << "theme widgets path ::: " << m_themeWidgetsPath;
372 
373     //! clear kde connections
374     for (auto &c : m_kdeConnections) {
375         disconnect(c);
376     }
377 
378     //! assign color schemes
379     QString themeColorScheme = m_themePath + "/colors";
380 
381     if (QFileInfo(themeColorScheme).exists()) {
382         setOriginalSchemeFile(themeColorScheme);
383     } else {
384         //! when plasma theme uses the kde colors
385         //! we track when kde color scheme is changing
386         QString kdeSettingsFile = Latte::configPath() + "/kdeglobals";
387 
388         KDirWatch::self()->addFile(kdeSettingsFile);
389 
390         m_kdeConnections[0] = connect(KDirWatch::self(), &KDirWatch::dirty, this, [ &, kdeSettingsFile](const QString & path) {
391             if (path == kdeSettingsFile) {
392                 this->setOriginalSchemeFile(WindowSystem::SchemeColors::possibleSchemeFile("kdeglobals"));
393             }
394         });
395 
396         m_kdeConnections[1] = connect(KDirWatch::self(), &KDirWatch::created, this, [ &, kdeSettingsFile](const QString & path) {
397             if (path == kdeSettingsFile) {
398                 this->setOriginalSchemeFile(WindowSystem::SchemeColors::possibleSchemeFile("kdeglobals"));
399             }
400         });
401 
402         setOriginalSchemeFile(WindowSystem::SchemeColors::possibleSchemeFile("kdeglobals"));
403     }
404 }
405 
loadThemeLightness()406 void Theme::loadThemeLightness()
407 {
408     float textColorLum = Latte::colorLumina(m_defaultScheme->textColor());
409     float backColorLum = Latte::colorLumina(m_defaultScheme->backgroundColor());
410 
411     if (backColorLum > textColorLum) {
412         m_isLightTheme = true;
413     } else {
414         m_isLightTheme = false;
415     }
416 
417     if (m_isLightTheme) {
418         qDebug() << "Plasma theme is light...";
419     } else {
420         qDebug() << "Plasma theme is dark...";
421     }
422 }
423 
cornersMask(const int & radius)424 const CornerRegions &Theme::cornersMask(const int &radius)
425 {
426     if (m_cornerRegions.contains(radius)) {
427         return m_cornerRegions[radius];
428     }
429 
430     qDebug() << radius;
431     CornerRegions corners;
432 
433     int axis = (2 * radius) + 2;
434     QImage cornerimage(axis, axis, QImage::Format_ARGB32);
435     QPainter painter(&cornerimage);
436     //!does not provide valid masks
437     //painter.setRenderHints(QPainter::Antialiasing);
438 
439     QPen pen(Qt::black);
440     pen.setStyle(Qt::SolidLine);
441     pen.setWidth(1);
442     painter.setPen(pen);
443 
444     QRect rectArea(0,0,axis,axis);
445     painter.fillRect(rectArea, Qt::white);
446     painter.drawRoundedRect(rectArea, axis, axis);
447 
448     QRegion topleft;
449     for(int y=0; y<radius; ++y) {
450         QRgb *line = (QRgb *)cornerimage.scanLine(y);
451 
452         QString bits;
453         int width{0};
454         for(int x=0; x<radius; ++x) {
455             QRgb point = line[x];
456 
457             if (QColor(point) == Qt::black) {
458                 bits = bits + "1 ";
459                 width = qMax(0, x);
460                 break;
461             } else {
462                 bits = bits + "0 ";
463             }
464         }
465 
466         if (width>0) {
467             topleft += QRect(0, y, width, 1);
468         }
469 
470         qDebug()<< "  " << bits;
471     }
472     corners.topLeft = topleft;
473 
474     QTransform transform;
475     transform.rotate(90);
476     corners.topRight = transform.map(corners.topLeft);
477     corners.topRight.translate(corners.topLeft.boundingRect().width(), 0);
478 
479     corners.bottomRight = transform.map(corners.topRight);
480     corners.bottomRight.translate(corners.topLeft.boundingRect().width(), 0);
481 
482     corners.bottomLeft = transform.map(corners.bottomRight);
483     corners.bottomLeft.translate(corners.topLeft.boundingRect().width(), 0);
484 
485     //qDebug() << " reg top;: " << corners.topLeft;
486     //qDebug() << " reg topr: " << corners.topRight;
487     //qDebug() << " reg bottomr: " << corners.bottomRight;
488     //qDebug() << " reg bottoml: " << corners.bottomLeft;
489 
490     m_cornerRegions[radius] = corners;
491     return m_cornerRegions[radius];
492 }
493 
updateMarginsAreaValues()494 void Theme::updateMarginsAreaValues()
495 {
496     m_marginsAreaTop = 0;
497     m_marginsAreaLeft = 0;
498     m_marginsAreaBottom = 0;
499     m_marginsAreaRight = 0;
500 
501     Plasma::Svg *svg = new Plasma::Svg(this);
502     svg->setImagePath(QStringLiteral("widgets/panel-background"));
503 
504     bool hasThickSeparatorMargins = svg->hasElement("thick-center");
505 
506     if (hasThickSeparatorMargins) {
507         int topMargin = svg->hasElement("hint-top-margin") ? svg->elementSize("hint-top-margin").height() : 0;
508         int leftMargin = svg->hasElement("hint-left-margin") ? svg->elementSize("hint-left-margin").width() : 0;
509         int bottomMargin = svg->hasElement("hint-bottom-margin") ? svg->elementSize("hint-bottom-margin").height() : 0;
510         int rightMargin = svg->hasElement("hint-right-margin") ? svg->elementSize("hint-right-margin").width() : 0;
511 
512         int thickTopMargin = svg->hasElement("thick-hint-top-margin") ? svg->elementSize("thick-hint-top-margin").height() : 0;
513         int thickLeftMargin = svg->hasElement("thick-hint-left-margin") ? svg->elementSize("thick-hint-left-margin").width() : 0;
514         int thickBottomMargin = svg->hasElement("thick-hint-bottom-margin") ? svg->elementSize("thick-hint-bottom-margin").height() : 0;
515         int thickRightMargin = svg->hasElement("thick-hint-right-margin") ? svg->elementSize("thick-hint-right-margin").width() : 0;
516 
517         m_marginsAreaTop = qMax(0, thickTopMargin - topMargin);
518         m_marginsAreaLeft = qMax(0, thickLeftMargin - leftMargin);
519         m_marginsAreaBottom = qMax(0, thickBottomMargin - bottomMargin);
520         m_marginsAreaRight = qMax(0, thickRightMargin - rightMargin);
521     }
522 
523     qDebug() << "PLASMA THEME MARGINS AREA ::" <<
524                 m_marginsAreaTop << m_marginsAreaLeft <<
525                 m_marginsAreaBottom << m_marginsAreaRight;
526 
527     svg->deleteLater();
528 
529     emit marginsAreaChanged();
530 }
531 
loadConfig()532 void Theme::loadConfig()
533 {
534     setOutlineWidth(m_themeGroup.readEntry("outlineWidth", 1));
535 }
536 
saveConfig()537 void Theme::saveConfig()
538 {
539     m_themeGroup.writeEntry("outlineWidth", m_outlineWidth);
540 }
541 
qmlRegisterTypes()542 void Theme::qmlRegisterTypes()
543 {
544 #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
545     qmlRegisterType<Latte::PlasmaExtended::Theme>();
546     qmlRegisterType<Latte::PlasmaExtended::PanelBackground>();
547 #else
548     qmlRegisterAnonymousType<Latte::PlasmaExtended::Theme>("latte-dock", 1);
549     qmlRegisterAnonymousType<Latte::PlasmaExtended::PanelBackground>("latte-dock", 1);
550 #endif
551 }
552 
553 }
554 }
555