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 | |
28 | namespace Kirigami |
29 | { |
30 | namespace Platform |
31 | { |
32 | template<> |
33 | KIRIGAMIPLATFORM_EXPORT QEvent::Type PlatformThemeEvents::DataChangedEvent::type = QEvent::None; |
34 | template<> |
35 | KIRIGAMIPLATFORM_EXPORT QEvent::Type PlatformThemeEvents::ColorSetChangedEvent::type = QEvent::None; |
36 | template<> |
37 | KIRIGAMIPLATFORM_EXPORT QEvent::Type PlatformThemeEvents::ColorGroupChangedEvent::type = QEvent::None; |
38 | template<> |
39 | KIRIGAMIPLATFORM_EXPORT QEvent::Type PlatformThemeEvents::ColorChangedEvent::type = QEvent::None; |
40 | template<> |
41 | KIRIGAMIPLATFORM_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. |
48 | struct 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 | }; |
58 | static 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. |
63 | class PlatformThemeData : public QObject |
64 | { |
65 | Q_OBJECT |
66 | |
67 | public: |
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 | |
274 | class PlatformThemePrivate |
275 | { |
276 | public: |
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 | |
402 | PlatformTheme::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 | |
425 | PlatformTheme::~PlatformTheme() |
426 | { |
427 | if (d->data) { |
428 | d->data->removeChangeWatcher(object: this); |
429 | } |
430 | |
431 | delete d; |
432 | } |
433 | |
434 | void 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 | |
444 | PlatformTheme::ColorSet PlatformTheme::colorSet() const |
445 | { |
446 | return d->data ? d->data->colorSet : Window; |
447 | } |
448 | |
449 | void 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 | |
459 | PlatformTheme::ColorGroup PlatformTheme::colorGroup() const |
460 | { |
461 | return d->data ? d->data->colorGroup : Active; |
462 | } |
463 | |
464 | bool PlatformTheme::inherit() const |
465 | { |
466 | return d->inherit; |
467 | } |
468 | |
469 | void 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 | |
481 | QColor PlatformTheme::textColor() const |
482 | { |
483 | return d->color(theme: this, color: PlatformThemeData::TextColor); |
484 | } |
485 | |
486 | QColor PlatformTheme::disabledTextColor() const |
487 | { |
488 | return d->color(theme: this, color: PlatformThemeData::DisabledTextColor); |
489 | } |
490 | |
491 | QColor PlatformTheme::highlightColor() const |
492 | { |
493 | return d->color(theme: this, color: PlatformThemeData::HighlightColor); |
494 | } |
495 | |
496 | QColor PlatformTheme::highlightedTextColor() const |
497 | { |
498 | return d->color(theme: this, color: PlatformThemeData::HighlightedTextColor); |
499 | } |
500 | |
501 | QColor PlatformTheme::backgroundColor() const |
502 | { |
503 | return d->color(theme: this, color: PlatformThemeData::BackgroundColor); |
504 | } |
505 | |
506 | QColor PlatformTheme::alternateBackgroundColor() const |
507 | { |
508 | return d->color(theme: this, color: PlatformThemeData::AlternateBackgroundColor); |
509 | } |
510 | |
511 | QColor PlatformTheme::activeTextColor() const |
512 | { |
513 | return d->color(theme: this, color: PlatformThemeData::ActiveTextColor); |
514 | } |
515 | |
516 | QColor PlatformTheme::activeBackgroundColor() const |
517 | { |
518 | return d->color(theme: this, color: PlatformThemeData::ActiveBackgroundColor); |
519 | } |
520 | |
521 | QColor PlatformTheme::linkColor() const |
522 | { |
523 | return d->color(theme: this, color: PlatformThemeData::LinkColor); |
524 | } |
525 | |
526 | QColor PlatformTheme::linkBackgroundColor() const |
527 | { |
528 | return d->color(theme: this, color: PlatformThemeData::LinkBackgroundColor); |
529 | } |
530 | |
531 | QColor PlatformTheme::visitedLinkColor() const |
532 | { |
533 | return d->color(theme: this, color: PlatformThemeData::VisitedLinkColor); |
534 | } |
535 | |
536 | QColor PlatformTheme::visitedLinkBackgroundColor() const |
537 | { |
538 | return d->color(theme: this, color: PlatformThemeData::VisitedLinkBackgroundColor); |
539 | } |
540 | |
541 | QColor PlatformTheme::negativeTextColor() const |
542 | { |
543 | return d->color(theme: this, color: PlatformThemeData::NegativeTextColor); |
544 | } |
545 | |
546 | QColor PlatformTheme::negativeBackgroundColor() const |
547 | { |
548 | return d->color(theme: this, color: PlatformThemeData::NegativeBackgroundColor); |
549 | } |
550 | |
551 | QColor PlatformTheme::neutralTextColor() const |
552 | { |
553 | return d->color(theme: this, color: PlatformThemeData::NeutralTextColor); |
554 | } |
555 | |
556 | QColor PlatformTheme::neutralBackgroundColor() const |
557 | { |
558 | return d->color(theme: this, color: PlatformThemeData::NeutralBackgroundColor); |
559 | } |
560 | |
561 | QColor PlatformTheme::positiveTextColor() const |
562 | { |
563 | return d->color(theme: this, color: PlatformThemeData::PositiveTextColor); |
564 | } |
565 | |
566 | QColor PlatformTheme::positiveBackgroundColor() const |
567 | { |
568 | return d->color(theme: this, color: PlatformThemeData::PositiveBackgroundColor); |
569 | } |
570 | |
571 | QColor PlatformTheme::focusColor() const |
572 | { |
573 | return d->color(theme: this, color: PlatformThemeData::FocusColor); |
574 | } |
575 | |
576 | QColor PlatformTheme::hoverColor() const |
577 | { |
578 | return d->color(theme: this, color: PlatformThemeData::HoverColor); |
579 | } |
580 | |
581 | // setters for theme implementations |
582 | void PlatformTheme::setTextColor(const QColor &color) |
583 | { |
584 | d->setDataColor(theme: this, color: PlatformThemeData::TextColor, value: color); |
585 | } |
586 | |
587 | void PlatformTheme::setDisabledTextColor(const QColor &color) |
588 | { |
589 | d->setDataColor(theme: this, color: PlatformThemeData::DisabledTextColor, value: color); |
590 | } |
591 | |
592 | void PlatformTheme::setBackgroundColor(const QColor &color) |
593 | { |
594 | d->setDataColor(theme: this, color: PlatformThemeData::BackgroundColor, value: color); |
595 | } |
596 | |
597 | void PlatformTheme::setAlternateBackgroundColor(const QColor &color) |
598 | { |
599 | d->setDataColor(theme: this, color: PlatformThemeData::AlternateBackgroundColor, value: color); |
600 | } |
601 | |
602 | void PlatformTheme::setHighlightColor(const QColor &color) |
603 | { |
604 | d->setDataColor(theme: this, color: PlatformThemeData::HighlightColor, value: color); |
605 | } |
606 | |
607 | void PlatformTheme::setHighlightedTextColor(const QColor &color) |
608 | { |
609 | d->setDataColor(theme: this, color: PlatformThemeData::HighlightedTextColor, value: color); |
610 | } |
611 | |
612 | void PlatformTheme::setActiveTextColor(const QColor &color) |
613 | { |
614 | d->setDataColor(theme: this, color: PlatformThemeData::ActiveTextColor, value: color); |
615 | } |
616 | |
617 | void PlatformTheme::setActiveBackgroundColor(const QColor &color) |
618 | { |
619 | d->setDataColor(theme: this, color: PlatformThemeData::ActiveBackgroundColor, value: color); |
620 | } |
621 | |
622 | void PlatformTheme::setLinkColor(const QColor &color) |
623 | { |
624 | d->setDataColor(theme: this, color: PlatformThemeData::LinkColor, value: color); |
625 | } |
626 | |
627 | void PlatformTheme::setLinkBackgroundColor(const QColor &color) |
628 | { |
629 | d->setDataColor(theme: this, color: PlatformThemeData::LinkBackgroundColor, value: color); |
630 | } |
631 | |
632 | void PlatformTheme::setVisitedLinkColor(const QColor &color) |
633 | { |
634 | d->setDataColor(theme: this, color: PlatformThemeData::VisitedLinkColor, value: color); |
635 | } |
636 | |
637 | void PlatformTheme::setVisitedLinkBackgroundColor(const QColor &color) |
638 | { |
639 | d->setDataColor(theme: this, color: PlatformThemeData::VisitedLinkBackgroundColor, value: color); |
640 | } |
641 | |
642 | void PlatformTheme::setNegativeTextColor(const QColor &color) |
643 | { |
644 | d->setDataColor(theme: this, color: PlatformThemeData::NegativeTextColor, value: color); |
645 | } |
646 | |
647 | void PlatformTheme::setNegativeBackgroundColor(const QColor &color) |
648 | { |
649 | d->setDataColor(theme: this, color: PlatformThemeData::NegativeBackgroundColor, value: color); |
650 | } |
651 | |
652 | void PlatformTheme::setNeutralTextColor(const QColor &color) |
653 | { |
654 | d->setDataColor(theme: this, color: PlatformThemeData::NeutralTextColor, value: color); |
655 | } |
656 | |
657 | void PlatformTheme::setNeutralBackgroundColor(const QColor &color) |
658 | { |
659 | d->setDataColor(theme: this, color: PlatformThemeData::NeutralBackgroundColor, value: color); |
660 | } |
661 | |
662 | void PlatformTheme::setPositiveTextColor(const QColor &color) |
663 | { |
664 | d->setDataColor(theme: this, color: PlatformThemeData::PositiveTextColor, value: color); |
665 | } |
666 | |
667 | void PlatformTheme::setPositiveBackgroundColor(const QColor &color) |
668 | { |
669 | d->setDataColor(theme: this, color: PlatformThemeData::PositiveBackgroundColor, value: color); |
670 | } |
671 | |
672 | void PlatformTheme::setHoverColor(const QColor &color) |
673 | { |
674 | d->setDataColor(theme: this, color: PlatformThemeData::HoverColor, value: color); |
675 | } |
676 | |
677 | void PlatformTheme::setFocusColor(const QColor &color) |
678 | { |
679 | d->setDataColor(theme: this, color: PlatformThemeData::FocusColor, value: color); |
680 | } |
681 | |
682 | QFont PlatformTheme::defaultFont() const |
683 | { |
684 | return d->data ? d->data->defaultFont : QFont{}; |
685 | } |
686 | |
687 | void 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 | |
695 | QFont PlatformTheme::smallFont() const |
696 | { |
697 | return d->data ? d->data->smallFont : QFont{}; |
698 | } |
699 | |
700 | void 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 | |
708 | QFont PlatformTheme::fixedWidthFont() const |
709 | { |
710 | return d->data ? d->data->fixedWidthFont : QFont{}; |
711 | } |
712 | |
713 | void 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 | |
721 | qreal 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 | |
729 | qreal 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 |
737 | void PlatformTheme::setCustomTextColor(const QColor &color) |
738 | { |
739 | d->setColor(theme: this, color: PlatformThemeData::TextColor, value: color); |
740 | } |
741 | |
742 | void PlatformTheme::setCustomDisabledTextColor(const QColor &color) |
743 | { |
744 | d->setColor(theme: this, color: PlatformThemeData::DisabledTextColor, value: color); |
745 | } |
746 | |
747 | void PlatformTheme::setCustomBackgroundColor(const QColor &color) |
748 | { |
749 | d->setColor(theme: this, color: PlatformThemeData::BackgroundColor, value: color); |
750 | } |
751 | |
752 | void PlatformTheme::setCustomAlternateBackgroundColor(const QColor &color) |
753 | { |
754 | d->setColor(theme: this, color: PlatformThemeData::AlternateBackgroundColor, value: color); |
755 | } |
756 | |
757 | void PlatformTheme::setCustomHighlightColor(const QColor &color) |
758 | { |
759 | d->setColor(theme: this, color: PlatformThemeData::HighlightColor, value: color); |
760 | } |
761 | |
762 | void PlatformTheme::setCustomHighlightedTextColor(const QColor &color) |
763 | { |
764 | d->setColor(theme: this, color: PlatformThemeData::HighlightedTextColor, value: color); |
765 | } |
766 | |
767 | void PlatformTheme::setCustomActiveTextColor(const QColor &color) |
768 | { |
769 | d->setColor(theme: this, color: PlatformThemeData::ActiveTextColor, value: color); |
770 | } |
771 | |
772 | void PlatformTheme::setCustomActiveBackgroundColor(const QColor &color) |
773 | { |
774 | d->setColor(theme: this, color: PlatformThemeData::ActiveBackgroundColor, value: color); |
775 | } |
776 | |
777 | void PlatformTheme::setCustomLinkColor(const QColor &color) |
778 | { |
779 | d->setColor(theme: this, color: PlatformThemeData::LinkColor, value: color); |
780 | } |
781 | |
782 | void PlatformTheme::setCustomLinkBackgroundColor(const QColor &color) |
783 | { |
784 | d->setColor(theme: this, color: PlatformThemeData::LinkBackgroundColor, value: color); |
785 | } |
786 | |
787 | void PlatformTheme::setCustomVisitedLinkColor(const QColor &color) |
788 | { |
789 | d->setColor(theme: this, color: PlatformThemeData::TextColor, value: color); |
790 | } |
791 | |
792 | void PlatformTheme::setCustomVisitedLinkBackgroundColor(const QColor &color) |
793 | { |
794 | d->setColor(theme: this, color: PlatformThemeData::VisitedLinkBackgroundColor, value: color); |
795 | } |
796 | |
797 | void PlatformTheme::setCustomNegativeTextColor(const QColor &color) |
798 | { |
799 | d->setColor(theme: this, color: PlatformThemeData::NegativeTextColor, value: color); |
800 | } |
801 | |
802 | void PlatformTheme::setCustomNegativeBackgroundColor(const QColor &color) |
803 | { |
804 | d->setColor(theme: this, color: PlatformThemeData::NegativeBackgroundColor, value: color); |
805 | } |
806 | |
807 | void PlatformTheme::setCustomNeutralTextColor(const QColor &color) |
808 | { |
809 | d->setColor(theme: this, color: PlatformThemeData::NeutralTextColor, value: color); |
810 | } |
811 | |
812 | void PlatformTheme::setCustomNeutralBackgroundColor(const QColor &color) |
813 | { |
814 | d->setColor(theme: this, color: PlatformThemeData::NeutralBackgroundColor, value: color); |
815 | } |
816 | |
817 | void PlatformTheme::setCustomPositiveTextColor(const QColor &color) |
818 | { |
819 | d->setColor(theme: this, color: PlatformThemeData::PositiveTextColor, value: color); |
820 | } |
821 | |
822 | void PlatformTheme::setCustomPositiveBackgroundColor(const QColor &color) |
823 | { |
824 | d->setColor(theme: this, color: PlatformThemeData::PositiveBackgroundColor, value: color); |
825 | } |
826 | |
827 | void PlatformTheme::setCustomHoverColor(const QColor &color) |
828 | { |
829 | d->setColor(theme: this, color: PlatformThemeData::HoverColor, value: color); |
830 | } |
831 | |
832 | void PlatformTheme::setCustomFocusColor(const QColor &color) |
833 | { |
834 | d->setColor(theme: this, color: PlatformThemeData::FocusColor, value: color); |
835 | } |
836 | |
837 | bool PlatformTheme::useAlternateBackgroundColor() const |
838 | { |
839 | return d->useAlternateBackgroundColor; |
840 | } |
841 | |
842 | void 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 | |
852 | QPalette 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 | |
867 | QIcon PlatformTheme::iconFromTheme(const QString &name, const QColor &customColor) |
868 | { |
869 | Q_UNUSED(customColor); |
870 | QIcon icon = QIcon::fromTheme(name); |
871 | return icon; |
872 | } |
873 | |
874 | bool PlatformTheme::supportsIconColoring() const |
875 | { |
876 | return d->supportsIconColoring; |
877 | } |
878 | |
879 | void PlatformTheme::setConstructing(bool isConstructing) |
880 | { |
881 | d->isConstructing = isConstructing; |
882 | } |
883 | |
884 | void PlatformTheme::setSupportsIconColoring(bool support) |
885 | { |
886 | d->supportsIconColoring = support; |
887 | } |
888 | |
889 | PlatformTheme *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 | |
912 | void 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 | |
947 | bool 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 | |
994 | void 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 | |
1060 | void 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. |
1080 | QObject *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 | |
1094 | PlatformThemeChangeTracker::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 | |
1108 | PlatformThemeChangeTracker::~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 | |
1123 | void PlatformThemeChangeTracker::markDirty(PropertyChanges changes) |
1124 | { |
1125 | m_data->changes |= changes; |
1126 | } |
1127 | } |
1128 | } |
1129 | |
1130 | #include "moc_platformtheme.cpp" |
1131 | #include "platformtheme.moc" |
1132 | |