Warning, file /plasma/plasma-workspace/kcms/colors/colorsapplicator.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).

0001 /*
0002     SPDX-FileCopyrightText: 2021 Dan Leinir Turthra Jensen <admin@leinir.dk>
0003     SPDX-FileCopyrightText: 2021 Benjamin Port <benjamin.port@enioka.com>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-only
0006 */
0007 
0008 #include "../kcms-common_p.h"
0009 #include "../krdb/krdb.h"
0010 
0011 #include <KColorScheme>
0012 #include <KConfigGroup>
0013 
0014 #include <QColorSpace>
0015 #include <QDBusConnection>
0016 #include <QDBusMessage>
0017 #include <QGenericMatrix>
0018 #include <QtMath>
0019 
0020 #include "colorsapplicator.h"
0021 
0022 float lerp(float a, float b, float f)
0023 {
0024     return (a * (1.0 - f)) + (b * f);
0025 }
0026 
0027 qreal cubeRootOf(qreal num)
0028 {
0029     return qPow(num, 1.0 / 3.0);
0030 }
0031 
0032 qreal cubed(qreal num)
0033 {
0034     return num * num * num;
0035 }
0036 
0037 // a structure representing a colour in the OKlab colour space.
0038 // for tinting, OKlab has some desirable properties:
0039 // - lightness is separated from hue (unlike RGB)
0040 //      so we don't make light themes darkish or dark themes lightish
0041 // - allows accurately adjusting hue without affecting perceptual lightness (unlike HSL/V)
0042 //      so we keep light themes and dark themes at the same perceptual lightness
0043 // - can be linearly blended
0044 //      once we get into oklab, we don't need fancy math to manipulate colours, we can just use a bog-standard
0045 //      linear interpolation function on the a and b values
0046 struct LAB {
0047     qreal L = 0;
0048     qreal a = 0;
0049     qreal b = 0;
0050 };
0051 
0052 // precomputed matrices from Björn Ottosson, public domain. or MIT if your country doesn't do that.
0053 /*
0054     SPDX-FileCopyrightText: 2020 Björn Ottosson
0055 
0056     SPDX-License-Identifier: MIT
0057     SPDX-License-Identifier: None
0058 */
0059 
0060 LAB linearSRGBToOKLab(const QColor &c)
0061 {
0062     // convert from srgb to linear lms
0063 
0064     const auto l = 0.4122214708 * c.redF() + 0.5363325363 * c.greenF() + 0.0514459929 * c.blueF();
0065     const auto m = 0.2119034982 * c.redF() + 0.6806995451 * c.greenF() + 0.1073969566 * c.blueF();
0066     const auto s = 0.0883024619 * c.redF() + 0.2817188376 * c.greenF() + 0.6299787005 * c.blueF();
0067 
0068     // convert from linear lms to non-linear lms
0069 
0070     const auto l_ = cubeRootOf(l);
0071     const auto m_ = cubeRootOf(m);
0072     const auto s_ = cubeRootOf(s);
0073 
0074     // convert from non-linear lms to lab
0075 
0076     return LAB{.L = 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_,
0077                .a = 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_,
0078                .b = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_};
0079 }
0080 
0081 QColor OKLabToLinearSRGB(LAB lab)
0082 {
0083     // convert from lab to non-linear lms
0084 
0085     const auto l_ = lab.L + 0.3963377774 * lab.a + 0.2158037573 * lab.b;
0086     const auto m_ = lab.L - 0.1055613458 * lab.a - 0.0638541728 * lab.b;
0087     const auto s_ = lab.L - 0.0894841775 * lab.a - 1.2914855480 * lab.b;
0088 
0089     // convert from non-linear lms to linear lms
0090 
0091     const auto l = cubed(l_);
0092     const auto m = cubed(m_);
0093     const auto s = cubed(s_);
0094 
0095     // convert from linear lms to linear srgb
0096 
0097     const auto r = +4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;
0098     const auto g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;
0099     const auto b = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s;
0100 
0101     return QColor::fromRgbF(r, g, b);
0102 }
0103 
0104 auto toLinearSRGB = QColorSpace(QColorSpace::SRgb).transformationToColorSpace(QColorSpace::SRgbLinear);
0105 auto fromLinearSRGB = QColorSpace(QColorSpace::SRgbLinear).transformationToColorSpace(QColorSpace::SRgb);
0106 
0107 QColor tintColor(const QColor &base, const QColor &with, qreal factor)
0108 {
0109     auto baseLAB = linearSRGBToOKLab(toLinearSRGB.map(base));
0110     const auto withLAB = linearSRGBToOKLab(toLinearSRGB.map(with));
0111     baseLAB.a = lerp(baseLAB.a, withLAB.a, factor);
0112     baseLAB.b = lerp(baseLAB.b, withLAB.b, factor);
0113 
0114     return fromLinearSRGB.map(OKLabToLinearSRGB(baseLAB));
0115 }
0116 
0117 static void copyEntry(KConfigGroup &from, KConfigGroup &to, const QString &entry, KConfig::WriteConfigFlags writeConfigFlag = KConfig::Normal)
0118 {
0119     if (from.hasKey(entry)) {
0120         to.writeEntry(entry, from.readEntry(entry), writeConfigFlag);
0121     }
0122 }
0123 
0124 void applyScheme(const QString &colorSchemePath, KConfig *configOutput, KConfig::WriteConfigFlags writeConfigFlag, std::optional<QColor> accentColor)
0125 {
0126     const auto accent = accentColor.value_or(configOutput->group("General").readEntry("AccentColor", QColor()));
0127 
0128     const auto hasAccent = [configOutput, &accent, accentColor]() {
0129         if (accent == QColor(Qt::transparent)) {
0130             return false;
0131         }
0132 
0133         return configOutput->group("General").hasKey("AccentColor")
0134             || accentColor.has_value(); // It's obvious that when accentColor.hasValue, it has (non-default/non-transparent) accent. reading configOutput for
0135                                         // any config is unreliable in this file.
0136     }();
0137 
0138     // Using KConfig::SimpleConfig because otherwise Header colors won't be
0139     // rewritten when a new color scheme is loaded.
0140     KSharedConfigPtr config = KSharedConfig::openConfig(colorSchemePath, KConfig::SimpleConfig);
0141 
0142     const auto applyAccentToTitlebar =
0143         config->group("General").readEntry("TitlebarIsAccentColored", config->group("General").readEntry("accentActiveTitlebar", false));
0144     const auto tintAccent = config->group("General").hasKey("TintFactor");
0145     const auto tintFactor = config->group("General").readEntry<qreal>("TintFactor", DefaultTintFactor);
0146 
0147     const QStringList colorSetGroupList{QStringLiteral("Colors:View"),
0148                                         QStringLiteral("Colors:Window"),
0149                                         QStringLiteral("Colors:Button"),
0150                                         QStringLiteral("Colors:Selection"),
0151                                         QStringLiteral("Colors:Tooltip"),
0152                                         QStringLiteral("Colors:Complementary"),
0153                                         QStringLiteral("Colors:Header")};
0154 
0155     const QStringList colorSetKeyList{QStringLiteral("BackgroundNormal"),
0156                                       QStringLiteral("BackgroundAlternate"),
0157                                       QStringLiteral("ForegroundNormal"),
0158                                       QStringLiteral("ForegroundInactive"),
0159                                       QStringLiteral("ForegroundActive"),
0160                                       QStringLiteral("ForegroundLink"),
0161                                       QStringLiteral("ForegroundVisited"),
0162                                       QStringLiteral("ForegroundNegative"),
0163                                       QStringLiteral("ForegroundNeutral"),
0164                                       QStringLiteral("ForegroundPositive"),
0165                                       QStringLiteral("DecorationFocus"),
0166                                       QStringLiteral("DecorationHover")};
0167 
0168     const QStringList accentList{QStringLiteral("ForegroundActive"),
0169                                  QStringLiteral("ForegroundLink"),
0170                                  QStringLiteral("DecorationFocus"),
0171                                  QStringLiteral("DecorationHover")};
0172 
0173     for (const auto &item : colorSetGroupList) {
0174         configOutput->deleteGroup(item);
0175 
0176         // Not all color schemes have header colors; in this case we don't want
0177         // to write out any header color data because then various things will think
0178         // the color scheme *does* have header colors, which it mostly doesn't, and
0179         // things will visually break in creative ways
0180         if (item == QStringLiteral("Colors:Header") && !config->hasGroup(QStringLiteral("Colors:Header"))) {
0181             continue;
0182         }
0183 
0184         KConfigGroup sourceGroup(config, item);
0185         KConfigGroup targetGroup(configOutput, item);
0186 
0187         for (const auto &entry : colorSetKeyList) {
0188             if (hasAccent) {
0189                 if (accentList.contains(entry)) {
0190                     targetGroup.writeEntry(entry, accent);
0191                 } else if (tintAccent) {
0192                     auto base = sourceGroup.readEntry<QColor>(entry, QColor());
0193                     targetGroup.writeEntry(entry, tintColor(base, accent, tintFactor));
0194                 } else {
0195                     copyEntry(sourceGroup, targetGroup, entry);
0196                 }
0197             } else {
0198                 copyEntry(sourceGroup, targetGroup, entry);
0199             }
0200         }
0201 
0202         if (item == QStringLiteral("Colors:Selection") && hasAccent) {
0203             QColor accentbg = accentBackground(accent, config->group("Colors:View").readEntry<QColor>("BackgroundNormal", QColor()));
0204             for (const auto &entry : {QStringLiteral("BackgroundNormal"), QStringLiteral("BackgroundAlternate")}) {
0205                 targetGroup.writeEntry(entry, accentbg);
0206             }
0207             for (const auto &entry : {QStringLiteral("ForegroundNormal"), QStringLiteral("ForegroundInactive")}) {
0208                 targetGroup.writeEntry(entry, accentForeground(accentbg, true));
0209             }
0210         }
0211 
0212         if (item == QStringLiteral("Colors:Button") && hasAccent) {
0213             QColor accentbg = accentBackground(accent, config->group("Colors:Button").readEntry<QColor>("BackgroundNormal", QColor()));
0214             for (const auto &entry : {QStringLiteral("BackgroundAlternate")}) {
0215                 targetGroup.writeEntry(entry, accentbg);
0216             }
0217         }
0218 
0219         if (sourceGroup.hasGroup("Inactive")) {
0220             sourceGroup = sourceGroup.group("Inactive");
0221             targetGroup = targetGroup.group("Inactive");
0222 
0223             for (const auto &entry : colorSetKeyList) {
0224                 if (tintAccent) {
0225                     auto base = sourceGroup.readEntry<QColor>(entry, QColor());
0226                     targetGroup.writeEntry(entry, tintColor(base, accent, tintFactor));
0227                 } else {
0228                     copyEntry(sourceGroup, targetGroup, entry, writeConfigFlag);
0229                 }
0230             }
0231         }
0232 
0233         // Header accent colouring
0234         if (item == QStringLiteral("Colors:Header") && hasAccent) {
0235             const auto windowBackground = config->group("Colors:Window").readEntry<QColor>("BackgroundNormal", QColor());
0236             const auto accentedWindowBackground = accentBackground(accent, windowBackground);
0237             const auto inactiveWindowBackground = tintColor(windowBackground, accent, tintFactor);
0238 
0239             if (applyAccentToTitlebar) {
0240                 targetGroup = KConfigGroup(configOutput, item);
0241                 targetGroup.writeEntry("BackgroundNormal", accentedWindowBackground);
0242                 targetGroup.writeEntry("ForegroundNormal", accentForeground(accentedWindowBackground, true));
0243 
0244                 targetGroup = targetGroup.group("Inactive");
0245                 targetGroup.writeEntry("BackgroundNormal", inactiveWindowBackground);
0246                 targetGroup.writeEntry("ForegroundNormal", accentForeground(inactiveWindowBackground, false));
0247             }
0248         }
0249     }
0250 
0251     KConfigGroup groupWMTheme(config, "WM");
0252     KConfigGroup groupWMOut(configOutput, "WM");
0253     KColorScheme inactiveHeaderColorScheme(QPalette::Inactive, KColorScheme::Header, config);
0254 
0255     const QStringList colorItemListWM{QStringLiteral("activeBackground"),
0256                                       QStringLiteral("activeForeground"),
0257                                       QStringLiteral("inactiveBackground"),
0258                                       QStringLiteral("inactiveForeground"),
0259                                       QStringLiteral("activeBlend"),
0260                                       QStringLiteral("inactiveBlend")};
0261 
0262     const QVector<QColor> defaultWMColors{KColorScheme(QPalette::Normal, KColorScheme::Header, config).background().color(),
0263                                           KColorScheme(QPalette::Normal, KColorScheme::Header, config).foreground().color(),
0264                                           inactiveHeaderColorScheme.background().color(),
0265                                           inactiveHeaderColorScheme.foreground().color(),
0266                                           KColorScheme(QPalette::Normal, KColorScheme::Header, config).background().color(),
0267                                           inactiveHeaderColorScheme.background().color()};
0268 
0269     int i = 0;
0270     for (const QString &coloritem : colorItemListWM) {
0271         groupWMOut.writeEntry(coloritem, groupWMTheme.readEntry(coloritem, defaultWMColors.value(i)), writeConfigFlag);
0272         ++i;
0273     }
0274 
0275     if (hasAccent && (tintAccent || applyAccentToTitlebar)) { // Titlebar accent colouring
0276         const auto windowBackground = config->group("Colors:Window").readEntry<QColor>("BackgroundNormal", QColor());
0277 
0278         if (tintAccent) {
0279             const auto tintedWindowBackground = tintColor(windowBackground, accent, tintFactor);
0280             if (!applyAccentToTitlebar) {
0281                 groupWMOut.writeEntry("activeBackground", tintedWindowBackground, writeConfigFlag);
0282                 groupWMOut.writeEntry("activeForeground", accentForeground(tintedWindowBackground, true), writeConfigFlag);
0283             }
0284             groupWMOut.writeEntry("inactiveBackground", tintedWindowBackground, writeConfigFlag);
0285             groupWMOut.writeEntry("inactiveForeground", accentForeground(tintedWindowBackground, false), writeConfigFlag);
0286         }
0287 
0288         if (applyAccentToTitlebar) {
0289             const auto accentedWindowBackground = accentBackground(accent, windowBackground);
0290             groupWMOut.writeEntry("activeBackground", accentedWindowBackground, writeConfigFlag);
0291             groupWMOut.writeEntry("activeForeground", accentForeground(accentedWindowBackground, true), writeConfigFlag);
0292         }
0293     }
0294 
0295     const QStringList groupNameList{QStringLiteral("ColorEffects:Inactive"), QStringLiteral("ColorEffects:Disabled")};
0296 
0297     const QStringList effectList{QStringLiteral("Enable"),
0298                                  QStringLiteral("ChangeSelectionColor"),
0299                                  QStringLiteral("IntensityEffect"),
0300                                  QStringLiteral("IntensityAmount"),
0301                                  QStringLiteral("ColorEffect"),
0302                                  QStringLiteral("ColorAmount"),
0303                                  QStringLiteral("Color"),
0304                                  QStringLiteral("ContrastEffect"),
0305                                  QStringLiteral("ContrastAmount")};
0306 
0307     for (const QString &groupName : groupNameList) {
0308         KConfigGroup groupEffectOut(configOutput, groupName);
0309         KConfigGroup groupEffectTheme(config, groupName);
0310 
0311         for (const QString &effect : effectList) {
0312             groupEffectOut.writeEntry(effect, groupEffectTheme.readEntry(effect), writeConfigFlag);
0313         }
0314     }
0315 
0316     bool applyToAlien{true};
0317     {
0318         KConfig cfg(QStringLiteral("kcmdisplayrc"), KConfig::NoGlobals);
0319         KConfigGroup group(configOutput, "General");
0320         group = KConfigGroup(&cfg, "X11");
0321         applyToAlien = group.readEntry("exportKDEColors", applyToAlien);
0322     }
0323     runRdb(KRdbExportQtColors | KRdbExportGtkTheme | (applyToAlien ? KRdbExportColors : 0));
0324 }