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