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"