1/*
2 * SPDX-FileCopyrightText: 2017 Marco Martin <mart@kde.org>
3 * SPDX-FileCopyrightText: 2021 Arjen Hiemstra <ahiemstra@heimr.nl>
4 *
5 * SPDX-License-Identifier: LGPL-2.0-or-later
6 */
7
8#include "platformtheme.h"
9#include "basictheme_p.h"
10#include "platformpluginfactory.h"
11#include <QDebug>
12#include <QDir>
13#include <QFontDatabase>
14#include <QGuiApplication>
15#include <QPluginLoader>
16#include <QPointer>
17#include <QQmlContext>
18#include <QQmlEngine>
19#include <QQuickStyle>
20#include <QQuickWindow>
21
22#include <array>
23#include <cinttypes>
24#include <functional>
25#include <memory>
26#include <unordered_map>
27
28namespace Kirigami
29{
30namespace Platform
31{
32template<>
33KIRIGAMIPLATFORM_EXPORT QEvent::Type PlatformThemeEvents::DataChangedEvent::type = QEvent::None;
34template<>
35KIRIGAMIPLATFORM_EXPORT QEvent::Type PlatformThemeEvents::ColorSetChangedEvent::type = QEvent::None;
36template<>
37KIRIGAMIPLATFORM_EXPORT QEvent::Type PlatformThemeEvents::ColorGroupChangedEvent::type = QEvent::None;
38template<>
39KIRIGAMIPLATFORM_EXPORT QEvent::Type PlatformThemeEvents::ColorChangedEvent::type = QEvent::None;
40template<>
41KIRIGAMIPLATFORM_EXPORT QEvent::Type PlatformThemeEvents::FontChangedEvent::type = QEvent::None;
42
43// Initialize event types.
44// We want to avoid collisions with application event types so we should use
45// registerEventType for generating the event types. Unfortunately, that method
46// is not constexpr so we need to call it somewhere during application startup.
47// This struct handles that.
48struct TypeInitializer {
49 TypeInitializer()
50 {
51 PlatformThemeEvents::DataChangedEvent::type = QEvent::Type(QEvent::registerEventType());
52 PlatformThemeEvents::ColorSetChangedEvent::type = QEvent::Type(QEvent::registerEventType());
53 PlatformThemeEvents::ColorGroupChangedEvent::type = QEvent::Type(QEvent::registerEventType());
54 PlatformThemeEvents::ColorChangedEvent::type = QEvent::Type(QEvent::registerEventType());
55 PlatformThemeEvents::FontChangedEvent::type = QEvent::Type(QEvent::registerEventType());
56 }
57};
58static TypeInitializer initializer;
59
60// This class encapsulates the actual data of the Theme object. It may be shared
61// among several instances of PlatformTheme, to ensure that the memory usage of
62// PlatformTheme stays low.
63class PlatformThemeData : public QObject
64{
65 Q_OBJECT
66
67public:
68 // An enum for all colors in PlatformTheme.
69 // This is used so we can have a QHash of local overrides in the
70 // PlatformTheme, which avoids needing to store all these colors in
71 // PlatformTheme even when they're not used.
72 enum ColorRole {
73 TextColor,
74 DisabledTextColor,
75 HighlightedTextColor,
76 ActiveTextColor,
77 LinkColor,
78 VisitedLinkColor,
79 NegativeTextColor,
80 NeutralTextColor,
81 PositiveTextColor,
82 BackgroundColor,
83 AlternateBackgroundColor,
84 HighlightColor,
85 ActiveBackgroundColor,
86 LinkBackgroundColor,
87 VisitedLinkBackgroundColor,
88 NegativeBackgroundColor,
89 NeutralBackgroundColor,
90 PositiveBackgroundColor,
91 FocusColor,
92 HoverColor,
93
94 // This should always be the last item. It indicates how many items
95 // there are and is used for the storage array below.
96 ColorRoleCount,
97 };
98
99 using ColorMap = std::unordered_map<std::underlying_type<ColorRole>::type, QColor>;
100
101 // Which PlatformTheme instance "owns" this data object. Only the owner is
102 // allowed to make changes to data.
103 QPointer<PlatformTheme> owner;
104
105 PlatformTheme::ColorSet colorSet = PlatformTheme::Window;
106 PlatformTheme::ColorGroup colorGroup = PlatformTheme::Active;
107
108 std::array<QColor, ColorRoleCount> colors;
109
110 QFont defaultFont;
111 QFont smallFont = QFontDatabase::systemFont(type: QFontDatabase::SmallestReadableFont);
112 QFont fixedWidthFont = QFontDatabase::systemFont(type: QFontDatabase::FixedFont);
113
114 QPalette palette;
115
116 // A list of PlatformTheme instances that want to be notified when the data
117 // changes. This is used instead of signal/slots as this way we only store
118 // a little bit of data and that data is shared among instances, whereas
119 // signal/slots turn out to have a pretty large memory overhead per instance.
120 using Watcher = PlatformTheme *;
121 QList<Watcher> watchers;
122
123 inline void setColorSet(PlatformTheme *sender, PlatformTheme::ColorSet set)
124 {
125 if (sender != owner || colorSet == set) {
126 return;
127 }
128
129 auto oldValue = colorSet;
130
131 colorSet = set;
132
133 notifyWatchers<PlatformTheme::ColorSet>(sender, oldValue, newValue: set);
134 }
135
136 inline void setColorGroup(PlatformTheme *sender, PlatformTheme::ColorGroup group)
137 {
138 if (sender != owner || colorGroup == group) {
139 return;
140 }
141
142 auto oldValue = colorGroup;
143
144 colorGroup = group;
145 palette.setCurrentColorGroup(QPalette::ColorGroup(group));
146
147 notifyWatchers<PlatformTheme::ColorGroup>(sender, oldValue, newValue: group);
148 }
149
150 inline void setColor(PlatformTheme *sender, ColorRole role, const QColor &color)
151 {
152 if (sender != owner || colors[role] == color) {
153 return;
154 }
155
156 auto oldValue = colors[role];
157
158 colors[role] = color;
159 updatePalette(palette, colors);
160
161 notifyWatchers<QColor>(sender, oldValue, newValue: colors[role]);
162 }
163
164 inline void setDefaultFont(PlatformTheme *sender, const QFont &font)
165 {
166 if (sender != owner || font == defaultFont) {
167 return;
168 }
169
170 auto oldValue = defaultFont;
171
172 defaultFont = font;
173
174 notifyWatchers<QFont>(sender, oldValue, newValue: font);
175 }
176
177 inline void setSmallFont(PlatformTheme *sender, const QFont &font)
178 {
179 if (sender != owner || font == smallFont) {
180 return;
181 }
182
183 auto oldValue = smallFont;
184
185 smallFont = font;
186
187 notifyWatchers<QFont>(sender, oldValue, newValue: smallFont);
188 }
189
190 inline void setFixedWidthFont(PlatformTheme *sender, const QFont &font)
191 {
192 if (sender != owner || font == fixedWidthFont) {
193 return;
194 }
195
196 auto oldValue = fixedWidthFont;
197
198 fixedWidthFont = font;
199
200 notifyWatchers<QFont>(sender, oldValue, newValue: fixedWidthFont);
201 }
202
203 inline void addChangeWatcher(PlatformTheme *object)
204 {
205 watchers.append(t: object);
206 }
207
208 inline void removeChangeWatcher(PlatformTheme *object)
209 {
210 watchers.removeOne(t: object);
211 }
212
213 template<typename T>
214 inline void notifyWatchers(PlatformTheme *sender, const T &oldValue, const T &newValue)
215 {
216 for (auto object : std::as_const(t&: watchers)) {
217 PlatformThemeEvents::PropertyChangedEvent<T> event(sender, oldValue, newValue);
218 QCoreApplication::sendEvent(receiver: object, event: &event);
219 }
220 }
221
222 // Update a palette from a list of colors.
223 inline static void updatePalette(QPalette &palette, const std::array<QColor, ColorRoleCount> &colors)
224 {
225 for (std::size_t i = 0; i < colors.size(); ++i) {
226 setPaletteColor(palette, role: ColorRole(i), color: colors.at(n: i));
227 }
228 }
229
230 // Update a palette from a hash of colors.
231 inline static void updatePalette(QPalette &palette, const ColorMap &colors)
232 {
233 for (auto entry : colors) {
234 setPaletteColor(palette, role: ColorRole(entry.first), color: entry.second);
235 }
236 }
237
238 inline static void setPaletteColor(QPalette &palette, ColorRole role, const QColor &color)
239 {
240 switch (role) {
241 case TextColor:
242 palette.setColor(acr: QPalette::Text, acolor: color);
243 palette.setColor(acr: QPalette::WindowText, acolor: color);
244 palette.setColor(acr: QPalette::ButtonText, acolor: color);
245 break;
246 case BackgroundColor:
247 palette.setColor(acr: QPalette::Window, acolor: color);
248 palette.setColor(acr: QPalette::Base, acolor: color);
249 palette.setColor(acr: QPalette::Button, acolor: color);
250 break;
251 case AlternateBackgroundColor:
252 palette.setColor(acr: QPalette::AlternateBase, acolor: color);
253 break;
254 case HighlightColor:
255 palette.setColor(acr: QPalette::Highlight, acolor: color);
256 palette.setColor(acr: QPalette::Accent, acolor: color);
257 break;
258 case HighlightedTextColor:
259 palette.setColor(acr: QPalette::HighlightedText, acolor: color);
260 break;
261 case LinkColor:
262 palette.setColor(acr: QPalette::Link, acolor: color);
263 break;
264 case VisitedLinkColor:
265 palette.setColor(acr: QPalette::LinkVisited, acolor: color);
266 break;
267
268 default:
269 break;
270 }
271 }
272};
273
274class PlatformThemePrivate
275{
276public:
277 PlatformThemePrivate()
278 : inherit(true)
279 , supportsIconColoring(false)
280 , pendingColorChange(false)
281 , pendingChildUpdate(false)
282 , useAlternateBackgroundColor(false)
283 , colorSet(PlatformTheme::Window)
284 , colorGroup(PlatformTheme::Active)
285 {
286 }
287
288 inline QColor color(const PlatformTheme *theme, PlatformThemeData::ColorRole color) const
289 {
290 if (!data) {
291 return QColor{};
292 }
293
294 QColor value = data->colors.at(n: color);
295
296 if (data->owner != theme && localOverrides) {
297 auto itr = localOverrides->find(x: color);
298 if (itr != localOverrides->end()) {
299 value = itr->second;
300 }
301 }
302
303 return value;
304 }
305
306 inline void setColor(PlatformTheme *theme, PlatformThemeData::ColorRole color, const QColor &value)
307 {
308 if (!localOverrides) {
309 localOverrides = std::make_unique<PlatformThemeData::ColorMap>();
310 }
311
312 if (!value.isValid()) {
313 // Invalid color, assume we are resetting the value.
314 auto itr = localOverrides->find(x: color);
315 if (itr != localOverrides->end()) {
316 PlatformThemeChangeTracker tracker(theme, PlatformThemeChangeTracker::PropertyChange::Color);
317 localOverrides->erase(position: itr);
318
319 if (data) {
320 // TODO: Find a better way to determine "default" color.
321 // Right now this sets the color to transparent to force a
322 // color change and relies on the style-specific subclass to
323 // handle resetting the actual color.
324 data->setColor(sender: theme, role: color, color: Qt::transparent);
325 }
326 }
327
328 return;
329 }
330
331 auto itr = localOverrides->find(x: color);
332 if (itr != localOverrides->end() && itr->second == value && (data && data->owner != theme)) {
333 return;
334 }
335
336 PlatformThemeChangeTracker tracker(theme, PlatformThemeChangeTracker::PropertyChange::Color);
337
338 (*localOverrides)[color] = value;
339
340 if (data) {
341 data->setColor(sender: theme, role: color, color: value);
342 }
343 }
344
345 inline void setDataColor(PlatformTheme *theme, PlatformThemeData::ColorRole color, const QColor &value)
346 {
347 // Only set color if we have no local override of the color.
348 // This is done because colorSet/colorGroup changes will trigger most
349 // subclasses to reevaluate and reset the colors, breaking any local
350 // overrides we have.
351 if (localOverrides) {
352 auto itr = localOverrides->find(x: color);
353 if (itr != localOverrides->end()) {
354 return;
355 }
356 }
357
358 PlatformThemeChangeTracker tracker(theme, PlatformThemeChangeTracker::PropertyChange::Color);
359
360 if (data) {
361 data->setColor(sender: theme, role: color, color: value);
362 }
363 }
364
365 /*
366 * Please note that there is no q pointer. This is intentional, as it avoids
367 * having to store that information for each instance of PlatformTheme,
368 * saving us 8 bytes per instance. Instead, we pass the theme object as
369 * first parameter of each method. This is a little uglier but essentially
370 * works the same without needing memory.
371 */
372
373 // An instance of the data object. This is potentially shared with many
374 // instances of PlatformTheme.
375 std::shared_ptr<PlatformThemeData> data;
376 // Used to store color overrides of inherited data. This is created on
377 // demand and will only exist if we actually have local overrides.
378 std::unique_ptr<PlatformThemeData::ColorMap> localOverrides;
379
380 bool inherit : 1;
381 bool supportsIconColoring : 1; // TODO KF6: Remove in favour of virtual method
382 bool pendingColorChange : 1;
383 bool pendingChildUpdate : 1;
384 bool useAlternateBackgroundColor : 1;
385 bool isConstructing : 1 = false;
386
387 // Note: We use these to store local values of PlatformTheme::ColorSet and
388 // PlatformTheme::ColorGroup. While these are standard enums and thus 32
389 // bits they only contain a few items so we store the value in only 4 bits
390 // to save space.
391 uint8_t colorSet : 4;
392 uint8_t colorGroup : 4;
393
394 // Ensure the above assumption holds. Should this static assert fail, the
395 // bit size above needs to be adjusted.
396 static_assert(PlatformTheme::ColorGroupCount <= 16, "PlatformTheme::ColorGroup contains more elements than can be stored in PlatformThemePrivate");
397 static_assert(PlatformTheme::ColorSetCount <= 16, "PlatformTheme::ColorSet contains more elements than can be stored in PlatformThemePrivate");
398
399 inline static PlatformPluginFactory *s_pluginFactory = nullptr;
400};
401
402PlatformTheme::PlatformTheme(QObject *parent)
403 : QObject(parent)
404 , d(new PlatformThemePrivate)
405{
406 setConstructing(true);
407 if (QQuickItem *item = qobject_cast<QQuickItem *>(o: parent)) {
408 connect(sender: item, signal: &QQuickItem::windowChanged, context: this, slot: [this](QQuickWindow *window) {
409 if (window) {
410 update();
411 }
412 });
413 connect(sender: item, signal: &QQuickItem::parentChanged, context: this, slot: &PlatformTheme::update);
414 // Needs to be connected to enabledChanged twice to correctly fully update when a
415 // Theme that does inherit becomes temporarly non-inherit and back due to
416 // the item being enabled or disabled
417 connect(sender: item, signal: &QQuickItem::enabledChanged, context: this, slot: &PlatformTheme::update);
418 connect(sender: item, signal: &QQuickItem::enabledChanged, context: this, slot: &PlatformTheme::update, type: Qt::QueuedConnection);
419 }
420
421 update();
422 setConstructing(false);
423}
424
425PlatformTheme::~PlatformTheme()
426{
427 if (d->data) {
428 d->data->removeChangeWatcher(object: this);
429 }
430
431 delete d;
432}
433
434void PlatformTheme::setColorSet(PlatformTheme::ColorSet colorSet)
435{
436 PlatformThemeChangeTracker tracker(this, PlatformThemeChangeTracker::PropertyChange::ColorSet);
437 d->colorSet = colorSet;
438
439 if (d->data) {
440 d->data->setColorSet(sender: this, set: colorSet);
441 }
442}
443
444PlatformTheme::ColorSet PlatformTheme::colorSet() const
445{
446 return d->data ? d->data->colorSet : Window;
447}
448
449void PlatformTheme::setColorGroup(PlatformTheme::ColorGroup colorGroup)
450{
451 PlatformThemeChangeTracker tracker(this, PlatformThemeChangeTracker::PropertyChange::ColorGroup);
452 d->colorGroup = colorGroup;
453
454 if (d->data) {
455 d->data->setColorGroup(sender: this, group: colorGroup);
456 }
457}
458
459PlatformTheme::ColorGroup PlatformTheme::colorGroup() const
460{
461 return d->data ? d->data->colorGroup : Active;
462}
463
464bool PlatformTheme::inherit() const
465{
466 return d->inherit;
467}
468
469void PlatformTheme::setInherit(bool inherit)
470{
471 if (inherit == d->inherit) {
472 return;
473 }
474
475 d->inherit = inherit;
476 update();
477
478 Q_EMIT inheritChanged(inherit);
479}
480
481QColor PlatformTheme::textColor() const
482{
483 return d->color(theme: this, color: PlatformThemeData::TextColor);
484}
485
486QColor PlatformTheme::disabledTextColor() const
487{
488 return d->color(theme: this, color: PlatformThemeData::DisabledTextColor);
489}
490
491QColor PlatformTheme::highlightColor() const
492{
493 return d->color(theme: this, color: PlatformThemeData::HighlightColor);
494}
495
496QColor PlatformTheme::highlightedTextColor() const
497{
498 return d->color(theme: this, color: PlatformThemeData::HighlightedTextColor);
499}
500
501QColor PlatformTheme::backgroundColor() const
502{
503 return d->color(theme: this, color: PlatformThemeData::BackgroundColor);
504}
505
506QColor PlatformTheme::alternateBackgroundColor() const
507{
508 return d->color(theme: this, color: PlatformThemeData::AlternateBackgroundColor);
509}
510
511QColor PlatformTheme::activeTextColor() const
512{
513 return d->color(theme: this, color: PlatformThemeData::ActiveTextColor);
514}
515
516QColor PlatformTheme::activeBackgroundColor() const
517{
518 return d->color(theme: this, color: PlatformThemeData::ActiveBackgroundColor);
519}
520
521QColor PlatformTheme::linkColor() const
522{
523 return d->color(theme: this, color: PlatformThemeData::LinkColor);
524}
525
526QColor PlatformTheme::linkBackgroundColor() const
527{
528 return d->color(theme: this, color: PlatformThemeData::LinkBackgroundColor);
529}
530
531QColor PlatformTheme::visitedLinkColor() const
532{
533 return d->color(theme: this, color: PlatformThemeData::VisitedLinkColor);
534}
535
536QColor PlatformTheme::visitedLinkBackgroundColor() const
537{
538 return d->color(theme: this, color: PlatformThemeData::VisitedLinkBackgroundColor);
539}
540
541QColor PlatformTheme::negativeTextColor() const
542{
543 return d->color(theme: this, color: PlatformThemeData::NegativeTextColor);
544}
545
546QColor PlatformTheme::negativeBackgroundColor() const
547{
548 return d->color(theme: this, color: PlatformThemeData::NegativeBackgroundColor);
549}
550
551QColor PlatformTheme::neutralTextColor() const
552{
553 return d->color(theme: this, color: PlatformThemeData::NeutralTextColor);
554}
555
556QColor PlatformTheme::neutralBackgroundColor() const
557{
558 return d->color(theme: this, color: PlatformThemeData::NeutralBackgroundColor);
559}
560
561QColor PlatformTheme::positiveTextColor() const
562{
563 return d->color(theme: this, color: PlatformThemeData::PositiveTextColor);
564}
565
566QColor PlatformTheme::positiveBackgroundColor() const
567{
568 return d->color(theme: this, color: PlatformThemeData::PositiveBackgroundColor);
569}
570
571QColor PlatformTheme::focusColor() const
572{
573 return d->color(theme: this, color: PlatformThemeData::FocusColor);
574}
575
576QColor PlatformTheme::hoverColor() const
577{
578 return d->color(theme: this, color: PlatformThemeData::HoverColor);
579}
580
581// setters for theme implementations
582void PlatformTheme::setTextColor(const QColor &color)
583{
584 d->setDataColor(theme: this, color: PlatformThemeData::TextColor, value: color);
585}
586
587void PlatformTheme::setDisabledTextColor(const QColor &color)
588{
589 d->setDataColor(theme: this, color: PlatformThemeData::DisabledTextColor, value: color);
590}
591
592void PlatformTheme::setBackgroundColor(const QColor &color)
593{
594 d->setDataColor(theme: this, color: PlatformThemeData::BackgroundColor, value: color);
595}
596
597void PlatformTheme::setAlternateBackgroundColor(const QColor &color)
598{
599 d->setDataColor(theme: this, color: PlatformThemeData::AlternateBackgroundColor, value: color);
600}
601
602void PlatformTheme::setHighlightColor(const QColor &color)
603{
604 d->setDataColor(theme: this, color: PlatformThemeData::HighlightColor, value: color);
605}
606
607void PlatformTheme::setHighlightedTextColor(const QColor &color)
608{
609 d->setDataColor(theme: this, color: PlatformThemeData::HighlightedTextColor, value: color);
610}
611
612void PlatformTheme::setActiveTextColor(const QColor &color)
613{
614 d->setDataColor(theme: this, color: PlatformThemeData::ActiveTextColor, value: color);
615}
616
617void PlatformTheme::setActiveBackgroundColor(const QColor &color)
618{
619 d->setDataColor(theme: this, color: PlatformThemeData::ActiveBackgroundColor, value: color);
620}
621
622void PlatformTheme::setLinkColor(const QColor &color)
623{
624 d->setDataColor(theme: this, color: PlatformThemeData::LinkColor, value: color);
625}
626
627void PlatformTheme::setLinkBackgroundColor(const QColor &color)
628{
629 d->setDataColor(theme: this, color: PlatformThemeData::LinkBackgroundColor, value: color);
630}
631
632void PlatformTheme::setVisitedLinkColor(const QColor &color)
633{
634 d->setDataColor(theme: this, color: PlatformThemeData::VisitedLinkColor, value: color);
635}
636
637void PlatformTheme::setVisitedLinkBackgroundColor(const QColor &color)
638{
639 d->setDataColor(theme: this, color: PlatformThemeData::VisitedLinkBackgroundColor, value: color);
640}
641
642void PlatformTheme::setNegativeTextColor(const QColor &color)
643{
644 d->setDataColor(theme: this, color: PlatformThemeData::NegativeTextColor, value: color);
645}
646
647void PlatformTheme::setNegativeBackgroundColor(const QColor &color)
648{
649 d->setDataColor(theme: this, color: PlatformThemeData::NegativeBackgroundColor, value: color);
650}
651
652void PlatformTheme::setNeutralTextColor(const QColor &color)
653{
654 d->setDataColor(theme: this, color: PlatformThemeData::NeutralTextColor, value: color);
655}
656
657void PlatformTheme::setNeutralBackgroundColor(const QColor &color)
658{
659 d->setDataColor(theme: this, color: PlatformThemeData::NeutralBackgroundColor, value: color);
660}
661
662void PlatformTheme::setPositiveTextColor(const QColor &color)
663{
664 d->setDataColor(theme: this, color: PlatformThemeData::PositiveTextColor, value: color);
665}
666
667void PlatformTheme::setPositiveBackgroundColor(const QColor &color)
668{
669 d->setDataColor(theme: this, color: PlatformThemeData::PositiveBackgroundColor, value: color);
670}
671
672void PlatformTheme::setHoverColor(const QColor &color)
673{
674 d->setDataColor(theme: this, color: PlatformThemeData::HoverColor, value: color);
675}
676
677void PlatformTheme::setFocusColor(const QColor &color)
678{
679 d->setDataColor(theme: this, color: PlatformThemeData::FocusColor, value: color);
680}
681
682QFont PlatformTheme::defaultFont() const
683{
684 return d->data ? d->data->defaultFont : QFont{};
685}
686
687void PlatformTheme::setDefaultFont(const QFont &font)
688{
689 PlatformThemeChangeTracker tracker(this, PlatformThemeChangeTracker::PropertyChange::Font);
690 if (d->data) {
691 d->data->setDefaultFont(sender: this, font);
692 }
693}
694
695QFont PlatformTheme::smallFont() const
696{
697 return d->data ? d->data->smallFont : QFont{};
698}
699
700void PlatformTheme::setSmallFont(const QFont &font)
701{
702 PlatformThemeChangeTracker tracker(this, PlatformThemeChangeTracker::PropertyChange::Font);
703 if (d->data) {
704 d->data->setSmallFont(sender: this, font);
705 }
706}
707
708QFont PlatformTheme::fixedWidthFont() const
709{
710 return d->data ? d->data->fixedWidthFont : QFont{};
711}
712
713void PlatformTheme::setFixedWidthFont(const QFont &font)
714{
715 PlatformThemeChangeTracker tracker(this, PlatformThemeChangeTracker::PropertyChange::Font);
716 if (d->data) {
717 d->data->setFixedWidthFont(sender: this, font);
718 }
719}
720
721qreal PlatformTheme::frameContrast() const
722{
723 // This value must be kept in sync with
724 // the value from Breeze Qt Widget theme.
725 // See: https://invent.kde.org/plasma/breeze/-/blob/master/kstyle/breezemetrics.h?ref_type=heads#L162
726 return 0.20;
727}
728
729qreal PlatformTheme::lightFrameContrast() const
730{
731 // This can be utilized to return full contrast
732 // if high contrast accessibility setting is enabled
733 return frameContrast() / 2.0;
734}
735
736// setters for QML clients
737void PlatformTheme::setCustomTextColor(const QColor &color)
738{
739 d->setColor(theme: this, color: PlatformThemeData::TextColor, value: color);
740}
741
742void PlatformTheme::setCustomDisabledTextColor(const QColor &color)
743{
744 d->setColor(theme: this, color: PlatformThemeData::DisabledTextColor, value: color);
745}
746
747void PlatformTheme::setCustomBackgroundColor(const QColor &color)
748{
749 d->setColor(theme: this, color: PlatformThemeData::BackgroundColor, value: color);
750}
751
752void PlatformTheme::setCustomAlternateBackgroundColor(const QColor &color)
753{
754 d->setColor(theme: this, color: PlatformThemeData::AlternateBackgroundColor, value: color);
755}
756
757void PlatformTheme::setCustomHighlightColor(const QColor &color)
758{
759 d->setColor(theme: this, color: PlatformThemeData::HighlightColor, value: color);
760}
761
762void PlatformTheme::setCustomHighlightedTextColor(const QColor &color)
763{
764 d->setColor(theme: this, color: PlatformThemeData::HighlightedTextColor, value: color);
765}
766
767void PlatformTheme::setCustomActiveTextColor(const QColor &color)
768{
769 d->setColor(theme: this, color: PlatformThemeData::ActiveTextColor, value: color);
770}
771
772void PlatformTheme::setCustomActiveBackgroundColor(const QColor &color)
773{
774 d->setColor(theme: this, color: PlatformThemeData::ActiveBackgroundColor, value: color);
775}
776
777void PlatformTheme::setCustomLinkColor(const QColor &color)
778{
779 d->setColor(theme: this, color: PlatformThemeData::LinkColor, value: color);
780}
781
782void PlatformTheme::setCustomLinkBackgroundColor(const QColor &color)
783{
784 d->setColor(theme: this, color: PlatformThemeData::LinkBackgroundColor, value: color);
785}
786
787void PlatformTheme::setCustomVisitedLinkColor(const QColor &color)
788{
789 d->setColor(theme: this, color: PlatformThemeData::TextColor, value: color);
790}
791
792void PlatformTheme::setCustomVisitedLinkBackgroundColor(const QColor &color)
793{
794 d->setColor(theme: this, color: PlatformThemeData::VisitedLinkBackgroundColor, value: color);
795}
796
797void PlatformTheme::setCustomNegativeTextColor(const QColor &color)
798{
799 d->setColor(theme: this, color: PlatformThemeData::NegativeTextColor, value: color);
800}
801
802void PlatformTheme::setCustomNegativeBackgroundColor(const QColor &color)
803{
804 d->setColor(theme: this, color: PlatformThemeData::NegativeBackgroundColor, value: color);
805}
806
807void PlatformTheme::setCustomNeutralTextColor(const QColor &color)
808{
809 d->setColor(theme: this, color: PlatformThemeData::NeutralTextColor, value: color);
810}
811
812void PlatformTheme::setCustomNeutralBackgroundColor(const QColor &color)
813{
814 d->setColor(theme: this, color: PlatformThemeData::NeutralBackgroundColor, value: color);
815}
816
817void PlatformTheme::setCustomPositiveTextColor(const QColor &color)
818{
819 d->setColor(theme: this, color: PlatformThemeData::PositiveTextColor, value: color);
820}
821
822void PlatformTheme::setCustomPositiveBackgroundColor(const QColor &color)
823{
824 d->setColor(theme: this, color: PlatformThemeData::PositiveBackgroundColor, value: color);
825}
826
827void PlatformTheme::setCustomHoverColor(const QColor &color)
828{
829 d->setColor(theme: this, color: PlatformThemeData::HoverColor, value: color);
830}
831
832void PlatformTheme::setCustomFocusColor(const QColor &color)
833{
834 d->setColor(theme: this, color: PlatformThemeData::FocusColor, value: color);
835}
836
837bool PlatformTheme::useAlternateBackgroundColor() const
838{
839 return d->useAlternateBackgroundColor;
840}
841
842void PlatformTheme::setUseAlternateBackgroundColor(bool alternate)
843{
844 if (alternate == d->useAlternateBackgroundColor) {
845 return;
846 }
847
848 d->useAlternateBackgroundColor = alternate;
849 Q_EMIT useAlternateBackgroundColorChanged(alternate);
850}
851
852QPalette PlatformTheme::palette() const
853{
854 if (!d->data) {
855 return QPalette{};
856 }
857
858 auto palette = d->data->palette;
859
860 if (d->localOverrides) {
861 PlatformThemeData::updatePalette(palette, colors: *d->localOverrides);
862 }
863
864 return palette;
865}
866
867QIcon PlatformTheme::iconFromTheme(const QString &name, const QColor &customColor)
868{
869 Q_UNUSED(customColor);
870 QIcon icon = QIcon::fromTheme(name);
871 return icon;
872}
873
874bool PlatformTheme::supportsIconColoring() const
875{
876 return d->supportsIconColoring;
877}
878
879void PlatformTheme::setConstructing(bool isConstructing)
880{
881 d->isConstructing = isConstructing;
882}
883
884void PlatformTheme::setSupportsIconColoring(bool support)
885{
886 d->supportsIconColoring = support;
887}
888
889PlatformTheme *PlatformTheme::qmlAttachedProperties(QObject *object)
890{
891 QQmlEngine *engine = qmlEngine(object);
892 QString pluginName;
893
894 if (engine) {
895 pluginName = engine->property(name: "_kirigamiTheme").toString();
896 }
897
898 auto plugin = PlatformPluginFactory::findPlugin(pluginName);
899 if (!plugin && !pluginName.isEmpty()) {
900 plugin = PlatformPluginFactory::findPlugin();
901 }
902
903 if (plugin) {
904 if (auto theme = plugin->createPlatformTheme(parent: object)) {
905 return theme;
906 }
907 }
908
909 return new BasicTheme(object);
910}
911
912void PlatformTheme::emitSignalsForChanges(int changes)
913{
914 if (!d->data) {
915 return;
916 }
917
918 auto propertyChanges = PlatformThemeChangeTracker::PropertyChanges::fromInt(i: changes);
919
920 if (propertyChanges & PlatformThemeChangeTracker::PropertyChange::ColorSet) {
921 Q_EMIT colorSetChanged(colorSet: ColorSet(d->data->colorSet));
922 }
923
924 if (propertyChanges & PlatformThemeChangeTracker::PropertyChange::ColorGroup) {
925 Q_EMIT colorGroupChanged(colorGroup: ColorGroup(d->data->colorGroup));
926 }
927
928 if (propertyChanges & PlatformThemeChangeTracker::PropertyChange::Color) {
929 Q_EMIT colorsChanged();
930 }
931
932 if (propertyChanges & PlatformThemeChangeTracker::PropertyChange::Palette) {
933 Q_EMIT paletteChanged(pal: d->data->palette);
934 }
935
936 if (propertyChanges & PlatformThemeChangeTracker::PropertyChange::Font) {
937 Q_EMIT defaultFontChanged(font: d->data->defaultFont);
938 Q_EMIT smallFontChanged(font: d->data->smallFont);
939 Q_EMIT fixedWidthFontChanged(font: d->data->fixedWidthFont);
940 }
941
942 if (propertyChanges & PlatformThemeChangeTracker::PropertyChange::Data) {
943 updateChildren(item: parent());
944 }
945}
946
947bool PlatformTheme::event(QEvent *event)
948{
949 PlatformThemeChangeTracker tracker(this);
950
951 if (event->type() == PlatformThemeEvents::DataChangedEvent::type) {
952 auto changeEvent = static_cast<PlatformThemeEvents::DataChangedEvent *>(event);
953
954 if (changeEvent->sender != this) {
955 return false;
956 }
957
958 if (changeEvent->oldValue) {
959 changeEvent->oldValue->removeChangeWatcher(object: this);
960 }
961
962 if (changeEvent->newValue) {
963 auto data = changeEvent->newValue;
964 data->addChangeWatcher(object: this);
965 }
966
967 tracker.markDirty(changes: PlatformThemeChangeTracker::PropertyChange::All);
968 return true;
969 }
970
971 if (event->type() == PlatformThemeEvents::ColorSetChangedEvent::type) {
972 tracker.markDirty(changes: PlatformThemeChangeTracker::PropertyChange::ColorSet);
973 return true;
974 }
975
976 if (event->type() == PlatformThemeEvents::ColorGroupChangedEvent::type) {
977 tracker.markDirty(changes: PlatformThemeChangeTracker::PropertyChange::ColorGroup);
978 return true;
979 }
980
981 if (event->type() == PlatformThemeEvents::ColorChangedEvent::type) {
982 tracker.markDirty(changes: PlatformThemeChangeTracker::PropertyChange::Color | PlatformThemeChangeTracker::PropertyChange::Palette);
983 return true;
984 }
985
986 if (event->type() == PlatformThemeEvents::FontChangedEvent::type) {
987 tracker.markDirty(changes: PlatformThemeChangeTracker::PropertyChange::Font);
988 return true;
989 }
990
991 return QObject::event(event);
992}
993
994void PlatformTheme::update()
995{
996 auto oldData = d->data;
997
998 bool actualInherit = d->inherit;
999 if (QQuickItem *item = qobject_cast<QQuickItem *>(o: parent())) {
1000 // For inactive windows it should work already, as also the non inherit themes get it
1001 if (colorGroup() != Disabled && !item->isEnabled()) {
1002 actualInherit = false;
1003 }
1004 }
1005
1006 if (actualInherit) {
1007 QObject *candidate = parent();
1008 while (true) {
1009 candidate = determineParent(object: candidate);
1010 if (!candidate) {
1011 break;
1012 }
1013
1014 auto t = static_cast<PlatformTheme *>(qmlAttachedPropertiesObject<PlatformTheme>(obj: candidate, create: false));
1015 if (t && t->d->data && t->d->data->owner == t) {
1016 if (d->data == t->d->data) {
1017 // Inheritance is already correct, do nothing.
1018 return;
1019 }
1020
1021 d->data = t->d->data;
1022
1023 PlatformThemeEvents::DataChangedEvent event{this, oldData, t->d->data};
1024 QCoreApplication::sendEvent(receiver: this, event: &event);
1025
1026 return;
1027 }
1028 }
1029 } else if (d->data && d->data->owner != this) {
1030 // Inherit has changed and we no longer want to inherit, clear the data
1031 // so it is recreated below.
1032 d->data = nullptr;
1033 }
1034
1035 if (!d->data) {
1036 d->data = std::make_shared<PlatformThemeData>();
1037 d->data->owner = this;
1038
1039 d->data->setColorSet(sender: this, set: static_cast<ColorSet>(d->colorSet));
1040 d->data->setColorGroup(sender: this, group: static_cast<ColorGroup>(d->colorGroup));
1041
1042 // If we normally inherit but do not do so currently due to an override,
1043 // copy over the old colorSet to ensure we do not suddenly change to a
1044 // different colorSet.
1045 if (d->inherit && !actualInherit && oldData) {
1046 d->data->setColorSet(sender: this, set: oldData->colorSet);
1047 }
1048 }
1049
1050 if (d->localOverrides) {
1051 for (auto entry : *d->localOverrides) {
1052 d->data->setColor(sender: this, role: PlatformThemeData::ColorRole(entry.first), color: entry.second);
1053 }
1054 }
1055
1056 PlatformThemeEvents::DataChangedEvent event{this, oldData, d->data};
1057 QCoreApplication::sendEvent(receiver: this, event: &event);
1058}
1059
1060void PlatformTheme::updateChildren(QObject *object)
1061{
1062 if (!object) {
1063 return;
1064 }
1065
1066 const auto children = object->children();
1067 for (auto child : children) {
1068 auto t = static_cast<PlatformTheme *>(qmlAttachedPropertiesObject<PlatformTheme>(obj: child, create: false));
1069 if (t) {
1070 t->update();
1071 } else {
1072 updateChildren(object: child);
1073 }
1074 }
1075}
1076
1077// We sometimes set theme properties on non-visual objects. However, if an item
1078// has a visual and a non-visual parent that are different, we should prefer the
1079// visual parent, so we need to apply some extra logic.
1080QObject *PlatformTheme::determineParent(QObject *object)
1081{
1082 if (!object) {
1083 return nullptr;
1084 }
1085
1086 auto item = qobject_cast<QQuickItem *>(o: object);
1087 if (item) {
1088 return item->parentItem();
1089 } else {
1090 return object->parent();
1091 }
1092}
1093
1094PlatformThemeChangeTracker::PlatformThemeChangeTracker(PlatformTheme *theme, PropertyChanges changes)
1095 : m_theme(theme)
1096{
1097 auto itr = s_blockedChanges.constFind(key: theme);
1098 if (itr == s_blockedChanges.constEnd() || (*itr).expired()) {
1099 m_data = std::make_shared<Data>();
1100 s_blockedChanges.insert(key: theme, value: m_data);
1101 } else {
1102 m_data = (*itr).lock();
1103 }
1104
1105 m_data->changes |= changes;
1106}
1107
1108PlatformThemeChangeTracker::~PlatformThemeChangeTracker() noexcept
1109{
1110 std::weak_ptr<Data> dataWatcher = m_data;
1111
1112 auto changes = m_data->changes;
1113 m_data.reset();
1114
1115 if (dataWatcher.use_count() <= 0) {
1116 if (!m_theme->d->isConstructing) {
1117 m_theme->emitSignalsForChanges(changes);
1118 }
1119 s_blockedChanges.remove(key: m_theme);
1120 }
1121}
1122
1123void PlatformThemeChangeTracker::markDirty(PropertyChanges changes)
1124{
1125 m_data->changes |= changes;
1126}
1127}
1128}
1129
1130#include "moc_platformtheme.cpp"
1131#include "platformtheme.moc"
1132

source code of kirigami/src/platform/platformtheme.cpp