File indexing completed on 2024-04-28 05:36:25

0001 /*
0002     SPDX-FileCopyrightText: 2017 Marco Martin <mart@kde.org>
0003     SPDX-FileCopyrightText: 2020 Noah Davis <noahadvs@gmail.com>
0004     SPDX-FileCopyrightText: 2021 Arjen Hiemstra <ahiemstra@heimr.nl>
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "plasmadesktoptheme.h"
0009 #include <KColorScheme>
0010 #include <KColorUtils>
0011 #include <KConfigGroup>
0012 #include <QDebug>
0013 #include <QGuiApplication>
0014 #include <QPalette>
0015 #include <QQmlContext>
0016 #include <QQmlEngine>
0017 #include <QQuickRenderControl>
0018 #include <QQuickWindow>
0019 #include <QScopeGuard>
0020 
0021 #ifndef Q_OS_ANDROID
0022 #include <QDBusConnection>
0023 
0024 #include <KIconColors>
0025 #include <KIconLoader>
0026 #endif
0027 
0028 class StyleSingleton : public QObject
0029 {
0030     Q_OBJECT
0031 
0032 public:
0033     struct Colors {
0034         QPalette palette;
0035         KColorScheme selectionScheme;
0036         KColorScheme scheme;
0037     };
0038 
0039     explicit StyleSingleton()
0040         : QObject()
0041         , buttonScheme(QPalette::Active, KColorScheme::ColorSet::Button)
0042     {
0043         connect(qGuiApp, &QGuiApplication::paletteChanged, this, &StyleSingleton::refresh);
0044 
0045 #ifndef Q_OS_ANDROID
0046         // Use DBus in order to listen for settings changes directly, as the
0047         // QApplication doesn't expose the font variants we're looking for,
0048         // namely smallFont.
0049         QDBusConnection::sessionBus().connect(QString(),
0050                                               QStringLiteral("/KDEPlatformTheme"),
0051                                               QStringLiteral("org.kde.KDEPlatformTheme"),
0052                                               QStringLiteral("refreshFonts"),
0053                                               this,
0054                                               SLOT(notifyWatchersConfigurationChange()));
0055 #endif
0056 
0057         connect(qGuiApp, &QGuiApplication::fontDatabaseChanged, this, &StyleSingleton::notifyWatchersConfigurationChange);
0058         connect(qGuiApp, &QGuiApplication::fontChanged, this, &StyleSingleton::notifyWatchersConfigurationChange);
0059 
0060         /* QtTextRendering uses less memory, so use it in low power mode.
0061          *
0062          * For scale factors greater than 2, native rendering doesn't actually do much.
0063          * Does native rendering even work when scaleFactor >= 2?
0064          *
0065          * NativeTextRendering is still distorted sometimes with fractional scale
0066          * factors, despite https://bugreports.qt.io/browse/QTBUG-67007 being closed.
0067          * 1.5x scaling looks generally OK, but there are occasional and difficult to
0068          * reproduce issues with all fractional scale factors.
0069          */
0070         qreal devicePixelRatio = qGuiApp->devicePixelRatio();
0071 #ifndef Q_OS_ANDROID
0072         QQuickWindow::TextRenderType defaultTextRenderType = (int(devicePixelRatio) == devicePixelRatio //
0073                                                                   ? QQuickWindow::NativeTextRendering
0074                                                                   : QQuickWindow::QtTextRendering);
0075 
0076         // Allow setting the text rendering type with an environment variable
0077         QByteArrayList validInputs = {"qttextrendering", "qtrendering", "nativetextrendering", "nativerendering"};
0078         QByteArray input = qgetenv("QT_QUICK_DEFAULT_TEXT_RENDER_TYPE").toLower();
0079         if (validInputs.contains(input)) {
0080             if (input == validInputs[0] || input == validInputs[1]) {
0081                 defaultTextRenderType = QQuickWindow::QtTextRendering;
0082             } else {
0083                 defaultTextRenderType = QQuickWindow::NativeTextRendering;
0084             }
0085         }
0086 
0087         QQuickWindow::setTextRenderType(defaultTextRenderType);
0088 #else
0089         // Native rendering on android is broken, so prefer Qt rendering in
0090         // this case.
0091         QQuickWindow::setTextRenderType(QQuickWindow::QtTextRendering);
0092 #endif
0093 
0094         smallFont = loadSmallFont();
0095     }
0096 
0097     QFont loadSmallFont() const
0098     {
0099         KSharedConfigPtr ptr = KSharedConfig::openConfig();
0100         KConfigGroup general(ptr->group("general"));
0101 
0102         return general.readEntry("smallestReadableFont", []() {
0103             auto smallFont = qApp->font();
0104 #ifndef Q_OS_WIN
0105             if (smallFont.pixelSize() != -1) {
0106                 smallFont.setPixelSize(smallFont.pixelSize() - 2);
0107             } else {
0108                 smallFont.setPointSize(smallFont.pointSize() - 2);
0109             }
0110 #endif
0111             return smallFont;
0112         }());
0113     }
0114 
0115     void refresh()
0116     {
0117         m_cache.clear();
0118         buttonScheme = KColorScheme(QPalette::Active, KColorScheme::ColorSet::Button);
0119 
0120         notifyWatchersPaletteChange();
0121     }
0122 
0123     Colors loadColors(Kirigami::Platform::PlatformTheme::ColorSet cs, QPalette::ColorGroup group)
0124     {
0125         const auto key = qMakePair(cs, group);
0126         auto it = m_cache.constFind(key);
0127         if (it != m_cache.constEnd())
0128             return *it;
0129 
0130         using Kirigami::Platform::PlatformTheme;
0131 
0132         KColorScheme::ColorSet set;
0133 
0134         switch (cs) {
0135         case PlatformTheme::Button:
0136             set = KColorScheme::ColorSet::Button;
0137             break;
0138         case PlatformTheme::Selection:
0139             set = KColorScheme::ColorSet::Selection;
0140             break;
0141         case PlatformTheme::Tooltip:
0142             set = KColorScheme::ColorSet::Tooltip;
0143             break;
0144         case PlatformTheme::View:
0145             set = KColorScheme::ColorSet::View;
0146             break;
0147         case PlatformTheme::Complementary:
0148             set = KColorScheme::ColorSet::Complementary;
0149             break;
0150         case PlatformTheme::Header:
0151             set = KColorScheme::ColorSet::Header;
0152             break;
0153         case PlatformTheme::Window:
0154         default:
0155             set = KColorScheme::ColorSet::Window;
0156         }
0157 
0158         // HACK/FIXME: Working around the fact that KColorScheme changes the selection background color when inactive by default.
0159         // Yes, this is horrible.
0160         QPalette::ColorGroup selectionGroup = group == QPalette::Inactive ? QPalette::Active : group;
0161         Colors ret = {{}, KColorScheme(selectionGroup, KColorScheme::ColorSet::Selection), KColorScheme(group, set)};
0162 
0163         QPalette pal;
0164         for (auto state : {QPalette::Active, QPalette::Inactive, QPalette::Disabled}) {
0165             pal.setBrush(state, QPalette::WindowText, ret.scheme.foreground());
0166             pal.setBrush(state, QPalette::Window, ret.scheme.background());
0167             pal.setBrush(state, QPalette::Base, ret.scheme.background());
0168             pal.setBrush(state, QPalette::Text, ret.scheme.foreground());
0169             pal.setBrush(state, QPalette::Button, ret.scheme.background());
0170             pal.setBrush(state, QPalette::ButtonText, ret.scheme.foreground());
0171             pal.setBrush(state, QPalette::Highlight, ret.selectionScheme.background());
0172             pal.setBrush(state, QPalette::HighlightedText, ret.selectionScheme.foreground());
0173             pal.setBrush(state, QPalette::ToolTipBase, ret.scheme.background());
0174             pal.setBrush(state, QPalette::ToolTipText, ret.scheme.foreground());
0175 
0176             pal.setColor(state, QPalette::Light, ret.scheme.shade(KColorScheme::LightShade));
0177             pal.setColor(state, QPalette::Midlight, ret.scheme.shade(KColorScheme::MidlightShade));
0178             pal.setColor(state, QPalette::Mid, ret.scheme.shade(KColorScheme::MidShade));
0179             pal.setColor(state, QPalette::Dark, ret.scheme.shade(KColorScheme::DarkShade));
0180             pal.setColor(state, QPalette::Shadow, QColor(0, 0, 0, 51 /* 20% */)); // ret.scheme.shade(KColorScheme::ShadowShade));
0181 
0182             pal.setBrush(state, QPalette::AlternateBase, ret.scheme.background(KColorScheme::AlternateBackground));
0183             pal.setBrush(state, QPalette::Link, ret.scheme.foreground(KColorScheme::LinkText));
0184             pal.setBrush(state, QPalette::LinkVisited, ret.scheme.foreground(KColorScheme::VisitedText));
0185 
0186             pal.setBrush(state, QPalette::PlaceholderText, ret.scheme.foreground(KColorScheme::InactiveText));
0187             pal.setBrush(state,
0188                          QPalette::BrightText,
0189                          KColorUtils::hcyColor(KColorUtils::hue(pal.buttonText().color()),
0190                                                KColorUtils::chroma(pal.buttonText().color()),
0191                                                1 - KColorUtils::luma(pal.buttonText().color())));
0192         }
0193         ret.palette = pal;
0194         m_cache.insert(key, ret);
0195         return ret;
0196     }
0197 
0198     void notifyWatchersPaletteChange()
0199     {
0200         for (auto watcher : std::as_const(watchers)) {
0201             watcher->syncColors();
0202         }
0203     }
0204 
0205     Q_SLOT void notifyWatchersConfigurationChange()
0206     {
0207         smallFont = loadSmallFont();
0208         for (auto watcher : std::as_const(watchers)) {
0209             watcher->setSmallFont(smallFont);
0210             watcher->setDefaultFont(qApp->font());
0211         }
0212     }
0213 
0214     KColorScheme buttonScheme;
0215     QFont smallFont;
0216 
0217     QList<PlasmaDesktopTheme *> watchers;
0218 
0219 private:
0220     QHash<QPair<Kirigami::Platform::PlatformTheme::ColorSet, QPalette::ColorGroup>, Colors> m_cache;
0221 };
0222 Q_GLOBAL_STATIC(StyleSingleton, s_style)
0223 
0224 PlasmaDesktopTheme::PlasmaDesktopTheme(QObject *parent)
0225     : PlatformTheme(parent)
0226 {
0227     // We don't use KIconLoader on Android so we don't support recoloring there
0228 #ifndef Q_OS_ANDROID
0229     setSupportsIconColoring(true);
0230 #endif
0231 
0232     auto parentItem = qobject_cast<QQuickItem *>(parent);
0233     if (parentItem) {
0234         connect(parentItem, &QQuickItem::enabledChanged, this, &PlasmaDesktopTheme::syncColors);
0235         connect(parentItem, &QQuickItem::visibleChanged, this, &PlasmaDesktopTheme::syncColors);
0236         connect(parentItem, &QQuickItem::windowChanged, this, &PlasmaDesktopTheme::syncWindow);
0237     }
0238 
0239     s_style->watchers.append(this);
0240 
0241     setDefaultFont(qGuiApp->font());
0242     setSmallFont(s_style->smallFont);
0243 
0244     syncWindow();
0245     syncColors();
0246 }
0247 
0248 PlasmaDesktopTheme::~PlasmaDesktopTheme()
0249 {
0250     s_style->watchers.removeOne(this);
0251 }
0252 
0253 void PlasmaDesktopTheme::syncWindow()
0254 {
0255     if (m_window) {
0256         disconnect(m_window.data(), &QWindow::activeChanged, this, &PlasmaDesktopTheme::syncColors);
0257     }
0258 
0259     QWindow *window = nullptr;
0260 
0261     auto parentItem = qobject_cast<QQuickItem *>(parent());
0262     if (parentItem) {
0263         QQuickWindow *qw = parentItem->window();
0264 
0265         window = QQuickRenderControl::renderWindowFor(qw);
0266         if (!window) {
0267             window = qw;
0268         }
0269         if (qw) {
0270             connect(qw, &QQuickWindow::sceneGraphInitialized, this, &PlasmaDesktopTheme::syncWindow);
0271         }
0272     }
0273     m_window = window;
0274 
0275     if (window) {
0276         connect(m_window.data(), &QWindow::activeChanged, this, &PlasmaDesktopTheme::syncColors);
0277         syncColors();
0278     }
0279 }
0280 
0281 QIcon PlasmaDesktopTheme::iconFromTheme(const QString &name, const QColor &customColor)
0282 {
0283 #ifndef Q_OS_ANDROID
0284     if (customColor != Qt::transparent) {
0285         KIconColors colors;
0286         colors.setText(customColor);
0287         return KDE::icon(name, colors);
0288     } else {
0289         return KDE::icon(name);
0290     }
0291 
0292 #else
0293     // On Android we don't want to use the KIconThemes-based loader since that appears to be broken
0294     return QIcon::fromTheme(name);
0295 #endif
0296 }
0297 
0298 void PlasmaDesktopTheme::syncColors()
0299 {
0300     QPalette::ColorGroup group = (QPalette::ColorGroup)colorGroup();
0301     auto parentItem = qobject_cast<QQuickItem *>(parent());
0302     if (parentItem) {
0303         if (!parentItem->isEnabled()) {
0304             group = QPalette::Disabled;
0305         } else if (m_window && !m_window->isActive() && m_window->isExposed()) {
0306             // Why also checking the window is exposed?
0307             // in the case of QQuickWidget the window() will never be active
0308             // and the widgets will always have the inactive palette.
0309             // better to always show it active than always show it inactive
0310             group = QPalette::Inactive;
0311         }
0312     }
0313 
0314     const auto colors = s_style->loadColors(colorSet(), group);
0315 
0316     // foreground
0317     setTextColor(colors.scheme.foreground(KColorScheme::NormalText).color());
0318     setDisabledTextColor(colors.scheme.foreground(KColorScheme::InactiveText).color());
0319     setHighlightedTextColor(colors.selectionScheme.foreground(KColorScheme::NormalText).color());
0320     setActiveTextColor(colors.scheme.foreground(KColorScheme::ActiveText).color());
0321     setLinkColor(colors.scheme.foreground(KColorScheme::LinkText).color());
0322     setVisitedLinkColor(colors.scheme.foreground(KColorScheme::VisitedText).color());
0323     setNegativeTextColor(colors.scheme.foreground(KColorScheme::NegativeText).color());
0324     setNeutralTextColor(colors.scheme.foreground(KColorScheme::NeutralText).color());
0325     setPositiveTextColor(colors.scheme.foreground(KColorScheme::PositiveText).color());
0326 
0327     // background
0328     setHighlightColor(colors.selectionScheme.background(KColorScheme::NormalBackground).color());
0329     setBackgroundColor(colors.scheme.background(KColorScheme::NormalBackground).color());
0330 
0331     // HACK: It's awful, but people sometimes complain about their color scheme not working well with the theme.
0332     // This is because I'm using colors that weren't used before and lots of themes have bad colors for previously unused colors.
0333     QColor alternateBackgroundOriginalColor = colors.scheme.background(KColorScheme::AlternateBackground).color();
0334     // #bdc3c7 is the old default for the Breeze color scheme.
0335     // #4d4d4d is the old default for the Breeze Dark color scheme.
0336     // Most color schemes use one of these 2 colors.
0337     if (colorSet() == ColorSet::Button && (alternateBackgroundOriginalColor == QColor("#bdc3c7") || alternateBackgroundOriginalColor == QColor("#4d4d4d"))) {
0338         setAlternateBackgroundColor(KColorUtils::tint(backgroundColor(), highlightColor(), 0.4));
0339     } else {
0340         setAlternateBackgroundColor(alternateBackgroundOriginalColor);
0341     }
0342     setActiveBackgroundColor(colors.scheme.background(KColorScheme::ActiveBackground).color());
0343     setLinkBackgroundColor(colors.scheme.background(KColorScheme::LinkBackground).color());
0344     setVisitedLinkBackgroundColor(colors.scheme.background(KColorScheme::VisitedBackground).color());
0345     setNegativeBackgroundColor(colors.scheme.background(KColorScheme::NegativeBackground).color());
0346     setNeutralBackgroundColor(colors.scheme.background(KColorScheme::NeutralBackground).color());
0347     setPositiveBackgroundColor(colors.scheme.background(KColorScheme::PositiveBackground).color());
0348 
0349     // decoration
0350     setHoverColor(colors.scheme.decoration(KColorScheme::HoverColor).color());
0351     setFocusColor(colors.scheme.decoration(KColorScheme::FocusColor).color());
0352 }
0353 
0354 bool PlasmaDesktopTheme::event(QEvent *event)
0355 {
0356     if (event->type() == Kirigami::Platform::PlatformThemeEvents::DataChangedEvent::type) {
0357         syncColors();
0358     }
0359 
0360     if (event->type() == Kirigami::Platform::PlatformThemeEvents::ColorSetChangedEvent::type) {
0361         syncColors();
0362     }
0363 
0364     if (event->type() == Kirigami::Platform::PlatformThemeEvents::ColorGroupChangedEvent::type) {
0365         syncColors();
0366     }
0367 
0368     return PlatformTheme::event(event);
0369 }
0370 
0371 #include "plasmadesktoptheme.moc"
0372 
0373 #include "moc_plasmadesktoptheme.cpp"