1// Copyright (C) 2022 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4//
5// W A R N I N G
6// -------------
7//
8// This file is not part of the Qt API. It exists purely as an
9// implementation detail. This header file may change from version to
10// version without notice, or even be removed.
11//
12// We mean it.
13//
14
15#include "qgtk3json_p.h"
16#include "qgtk3storage_p.h"
17#include <qpa/qwindowsysteminterface.h>
18
19QT_BEGIN_NAMESPACE
20
21QGtk3Storage::QGtk3Storage()
22{
23 m_interface.reset(p: new QGtk3Interface(this));
24 populateMap();
25}
26
27/*!
28 \internal
29 \enum QGtk3Storage::SourceType
30 \brief This enum represents the type of a color source.
31
32 \value Gtk Color is read from a GTK widget
33 \value Fixed A fixed brush is specified
34 \value Modified The color is a modification of another color (fixed or read from GTK)
35 \omitvalue Invalid
36 */
37
38/*!
39 \internal
40 \brief Find a brush from a source.
41
42 Returns a QBrush from a given \param source and a \param map of available brushes
43 to search from.
44
45 A null QBrush is returned, if no brush corresponding to the source has been found.
46 */
47QBrush QGtk3Storage::brush(const Source &source, const BrushMap &map) const
48{
49 switch (source.sourceType) {
50 case SourceType::Gtk:
51 return m_interface ? QBrush(m_interface->brush(wtype: source.gtk3.gtkWidgetType,
52 source: source.gtk3.source, state: source.gtk3.state))
53 : QBrush();
54
55 case SourceType::Modified: {
56 // don't loop through modified sources, break if modified source not found
57 Source recSource = brush(brush: TargetBrush(source.rec.colorGroup, source.rec.colorRole,
58 source.rec.colorScheme), map);
59
60 if (!recSource.isValid() || (recSource.sourceType == SourceType::Modified))
61 return QBrush();
62
63 // Set brush and alter color
64 QBrush b = brush(source: recSource, map);
65 if (source.rec.width > 0 && source.rec.height > 0)
66 b.setTexture(QPixmap(source.rec.width, source.rec.height));
67 QColor c = b.color().lighter(f: source.rec.lighter);
68 c = QColor((c.red() + source.rec.deltaRed),
69 (c.green() + source.rec.deltaGreen),
70 (c.blue() + source.rec.deltaBlue));
71 b.setColor(c);
72 return b;
73 }
74
75 case SourceType::Fixed:
76 return source.fix.fixedBrush;
77
78 case SourceType::Invalid:
79 return QBrush();
80 }
81
82 // needed because of the scope after recursive
83 Q_UNREACHABLE();
84}
85
86/*!
87 \internal
88 \brief Recurse to find a source brush for modification.
89
90 Returns the source specified by the target brush \param b in the \param map of brushes.
91 Takes dark/light/unknown into consideration.
92 Returns an empty brush if no suitable one can be found.
93 */
94QGtk3Storage::Source QGtk3Storage::brush(const TargetBrush &b, const BrushMap &map) const
95{
96#define FIND(brush) if (map.contains(brush))\
97 return map.value(brush)
98
99 // Return exact match
100 FIND(b);
101
102 // unknown color scheme can find anything
103 if (b.colorScheme == Qt::ColorScheme::Unknown) {
104 FIND(TargetBrush(b, Qt::ColorScheme::Dark));
105 FIND(TargetBrush(b, Qt::ColorScheme::Light));
106 }
107
108 // Color group All can always be found
109 if (b.colorGroup != QPalette::All)
110 return brush(b: TargetBrush(QPalette::All, b.colorRole, b.colorScheme), map);
111
112 // Brush not found
113 return Source();
114#undef FIND
115}
116
117/*!
118 \internal
119 \brief Returns a simple, hard coded base palette.
120
121 Create a hard coded palette with default colors as a fallback for any color that can't be
122 obtained from GTK.
123
124 \note This palette will be used as a default baseline for the system palette, which then
125 will be used as a default baseline for any other palette type.
126 */
127QPalette QGtk3Storage::standardPalette()
128{
129 QColor backgroundColor(0xd4, 0xd0, 0xc8);
130 QColor lightColor(backgroundColor.lighter());
131 QColor darkColor(backgroundColor.darker());
132 const QBrush darkBrush(darkColor);
133 QColor midColor(Qt::gray);
134 QPalette palette(Qt::black, backgroundColor, lightColor, darkColor,
135 midColor, Qt::black, Qt::white);
136 palette.setBrush(cg: QPalette::Disabled, cr: QPalette::WindowText, brush: darkBrush);
137 palette.setBrush(cg: QPalette::Disabled, cr: QPalette::Text, brush: darkBrush);
138 palette.setBrush(cg: QPalette::Disabled, cr: QPalette::ButtonText, brush: darkBrush);
139 palette.setBrush(cg: QPalette::Disabled, cr: QPalette::Base, brush: QBrush(backgroundColor));
140 return palette;
141}
142
143/*!
144 \internal
145 \brief Return a GTK styled QPalette.
146
147 Returns the pointer to a (cached) QPalette for \param type, with its brushes
148 populated according to the current GTK theme.
149 */
150const QPalette *QGtk3Storage::palette(QPlatformTheme::Palette type) const
151{
152 if (type >= QPlatformTheme::NPalettes)
153 return nullptr;
154
155 if (m_paletteCache[type].has_value()) {
156 qCDebug(lcQGtk3Interface) << "Returning palette from cache:"
157 << QGtk3Json::fromPalette(palette: type);
158
159 return &m_paletteCache[type].value();
160 }
161
162 // Read system palette as a baseline first
163 if (!m_paletteCache[QPlatformTheme::SystemPalette].has_value() && type != QPlatformTheme::SystemPalette)
164 palette();
165
166 // Fall back to system palette for unknown types
167 if (!m_palettes.contains(key: type) && type != QPlatformTheme::SystemPalette) {
168 qCDebug(lcQGtk3Interface) << "Returning system palette for unknown type"
169 << QGtk3Json::fromPalette(palette: type);
170 return palette();
171 }
172
173 BrushMap brushes = m_palettes.value(key: type);
174
175 // Standard palette is base for system palette. System palette is base for all others.
176 QPalette p = QPalette( type == QPlatformTheme::SystemPalette ? standardPalette()
177 : m_paletteCache[QPlatformTheme::SystemPalette].value());
178
179 qCDebug(lcQGtk3Interface) << "Creating palette:" << QGtk3Json::fromPalette(palette: type);
180 for (auto i = brushes.begin(); i != brushes.end(); ++i) {
181 Source source = i.value();
182
183 // Brush is set if
184 // - theme and source color scheme match
185 // - or either of them is unknown
186 const auto appSource = i.key().colorScheme;
187 const auto appTheme = colorScheme();
188 const bool setBrush = (appSource == appTheme) ||
189 (appSource == Qt::ColorScheme::Unknown) ||
190 (appTheme == Qt::ColorScheme::Unknown);
191
192 if (setBrush) {
193 p.setBrush(cg: i.key().colorGroup, cr: i.key().colorRole, brush: brush(source, map: brushes));
194 }
195 }
196
197 m_paletteCache[type].emplace(args&: p);
198 if (type == QPlatformTheme::SystemPalette)
199 qCDebug(lcQGtk3Interface) << "System Palette defined" << themeName() << colorScheme() << p;
200
201 return &m_paletteCache[type].value();
202}
203
204/*!
205 \internal
206 \brief Return a GTK styled font.
207
208 Returns a QFont of \param type, styled according to the current GTK theme.
209*/
210const QFont *QGtk3Storage::font(QPlatformTheme::Font type) const
211{
212 if (m_fontCache[type].has_value())
213 return &m_fontCache[type].value();
214
215 m_fontCache[type].emplace(args: m_interface->font(type));
216 return &m_fontCache[type].value();
217}
218
219/*!
220 \internal
221 \brief Return a GTK styled standard pixmap if available.
222
223 Returns a pixmap specified by \param standardPixmap and \param size.
224 Returns an empty pixmap if GTK doesn't support the requested one.
225 */
226QPixmap QGtk3Storage::standardPixmap(QPlatformTheme::StandardPixmap standardPixmap,
227 const QSizeF &size) const
228{
229 if (m_pixmapCache.contains(key: standardPixmap))
230 return QPixmap::fromImage(image: m_pixmapCache.object(key: standardPixmap)->scaled(s: size.toSize()));
231
232 if (!m_interface)
233 return QPixmap();
234
235 QImage image = m_interface->standardPixmap(standardPixmap);
236 if (image.isNull())
237 return QPixmap();
238
239 m_pixmapCache.insert(key: standardPixmap, object: new QImage(image));
240 return QPixmap::fromImage(image: image.scaled(s: size.toSize()));
241}
242
243/*!
244 \internal
245 \brief Returns a GTK styled file icon corresponding to \param fileInfo.
246 */
247QIcon QGtk3Storage::fileIcon(const QFileInfo &fileInfo) const
248{
249 return m_interface ? m_interface->fileIcon(fileInfo) : QIcon();
250}
251
252/*!
253 \internal
254 \brief Clears all caches.
255 */
256void QGtk3Storage::clear()
257{
258 m_colorScheme = Qt::ColorScheme::Unknown;
259 m_palettes.clear();
260 for (auto &cache : m_paletteCache)
261 cache.reset();
262
263 for (auto &cache : m_fontCache)
264 cache.reset();
265}
266
267/*!
268 \internal
269 \brief Handles a theme change at runtime.
270
271 Clear all caches, re-populate with current GTK theme and notify the window system interface.
272 This method is a callback for the theme change signal sent from GTK.
273 */
274void QGtk3Storage::handleThemeChange()
275{
276 clear();
277 populateMap();
278 QWindowSystemInterface::handleThemeChange();
279}
280
281/*!
282 \internal
283 \brief Populates a map with information about how to locate colors in GTK.
284
285 This method creates a data structure to locate color information for each brush of a QPalette
286 within GTK. The structure can hold mapping information for each QPlatformTheme::Palette
287 enum value. If no specific mapping is stored for an enum value, the system palette is returned
288 instead of a specific one. If no mapping is stored for the system palette, it will fall back to
289 QGtk3Storage::standardPalette.
290
291 The method will populate the data structure with a standard mapping, covering the following
292 palette types:
293 \list
294 \li QPlatformTheme::SystemPalette
295 \li QPlatformTheme::CheckBoxPalette
296 \li QPlatformTheme::RadioButtonPalette
297 \li QPlatformTheme::ComboBoxPalette
298 \li QPlatformTheme::GroupBoxPalette
299 \li QPlatformTheme::MenuPalette
300 \li QPlatformTheme::TextLineEditPalette
301 \endlist
302
303 The method will check the environment variable {{QT_GUI_GTK_JSON_SAVE}}. If it points to a
304 valid path with write access, it will write the standard mapping into a Json file.
305 That Json file can be modified and/or extended.
306 The Json syntax is
307 - "QGtk3Palettes" (top level value)
308 - QPlatformTheme::Palette
309 - QPalette::ColorRole
310 - Qt::ColorScheme
311 - Qt::ColorGroup
312 - Source data
313 - Source Type
314 - [source data]
315
316 If the environment variable {{QT_GUI_GTK_JSON_HARDCODED}} contains the keyword \c true,
317 all sources are converted to fixed sources. In that case, they contain the hard coded HexRGBA
318 values read from GTK.
319
320 The method will also check the environment variable {{QT_GUI_GTK_JSON}}. If it points to a valid
321 Json file with read access, it will be parsed instead of creating a standard mapping.
322 Parsing errors will be printed out with qCInfo if the logging category {{qt.qpa.gtk}} is activated.
323 In case of a parsing error, the method will fall back to creating a standard mapping.
324
325 \note
326 If a Json file contains only fixed brushes (e.g. exported with {{QT_GUI_GTK_JSON_HARDCODED=true}}),
327 no colors will be imported from GTK.
328 */
329void QGtk3Storage::populateMap()
330{
331 static QString m_themeName;
332
333 // Distiguish initialization, theme change or call without theme change
334 const QString newThemeName = themeName();
335 if (m_themeName == newThemeName)
336 return;
337
338 clear();
339
340 // Derive color scheme from theme name
341 m_colorScheme = newThemeName.contains(s: "dark"_L1, cs: Qt::CaseInsensitive)
342 ? Qt::ColorScheme::Dark : m_interface->colorSchemeByColors();
343
344 if (m_themeName.isEmpty()) {
345 qCDebug(lcQGtk3Interface) << "GTK theme initialized:" << newThemeName << m_colorScheme;
346 } else {
347 qCDebug(lcQGtk3Interface) << "GTK theme changed to:" << newThemeName << m_colorScheme;
348 }
349 m_themeName = newThemeName;
350
351 // create standard mapping or load from Json file?
352 const QString jsonInput = qEnvironmentVariable(varName: "QT_GUI_GTK_JSON");
353 if (!jsonInput.isEmpty()) {
354 if (load(filename: jsonInput)) {
355 return;
356 } else {
357 qWarning() << "Falling back to standard GTK mapping.";
358 }
359 }
360
361 createMapping();
362
363 const QString jsonOutput = qEnvironmentVariable(varName: "QT_GUI_GTK_JSON_SAVE");
364 if (!jsonOutput.isEmpty() && !save(filename: jsonOutput))
365 qWarning() << "File" << jsonOutput << "could not be saved.\n";
366}
367
368/*!
369 \internal
370 \brief Return a palette map for saving.
371
372 This method returns the existing palette map, if the environment variable
373 {{QT_GUI_GTK_JSON_HARDCODED}} is not set or does not contain the keyword \c true.
374 If it contains the keyword \c true, it returns a palette map with all brush
375 sources converted to fixed sources.
376 */
377const QGtk3Storage::PaletteMap QGtk3Storage::savePalettes() const
378{
379 const QString hard = qEnvironmentVariable(varName: "QT_GUI_GTK_JSON_HARDCODED");
380 if (!hard.contains(s: "true"_L1, cs: Qt::CaseInsensitive))
381 return m_palettes;
382
383 // Json output is supposed to be readable without GTK connection
384 // convert palette map into hard coded brushes
385 PaletteMap map = m_palettes;
386 for (auto paletteIterator = map.begin(); paletteIterator != map.end();
387 ++paletteIterator) {
388 QGtk3Storage::BrushMap &bm = paletteIterator.value();
389 for (auto brushIterator = bm.begin(); brushIterator != bm.end();
390 ++brushIterator) {
391 QGtk3Storage::Source &s = brushIterator.value();
392 switch (s.sourceType) {
393
394 // Read the brush and convert it into a fixed brush
395 case SourceType::Gtk: {
396 const QBrush fixedBrush = brush(source: s, map: bm);
397 s.fix.fixedBrush = fixedBrush;
398 s.sourceType = SourceType::Fixed;
399 }
400 break;
401 case SourceType::Fixed:
402 case SourceType::Modified:
403 case SourceType::Invalid:
404 break;
405 }
406 }
407 }
408 return map;
409}
410
411/*!
412 \internal
413 \brief Saves current palette mapping to a \param filename with Json format \param f.
414
415 Saves the current palette mapping into a QJson file,
416 taking {{QT_GUI_GTK_JSON_HARDCODED}} into consideration.
417 Returns \c true if saving was successful and \c false otherwise.
418 */
419bool QGtk3Storage::save(const QString &filename, QJsonDocument::JsonFormat f) const
420{
421 return QGtk3Json::save(map: savePalettes(), fileName: filename, format: f);
422}
423
424/*!
425 \internal
426 \brief Returns a QJsonDocument with current palette mapping.
427
428 Saves the current palette mapping into a QJsonDocument,
429 taking {{QT_GUI_GTK_JSON_HARDCODED}} into consideration.
430 Returns \c true if saving was successful and \c false otherwise.
431 */
432QJsonDocument QGtk3Storage::save() const
433{
434 return QGtk3Json::save(map: savePalettes());
435}
436
437/*!
438 \internal
439 \brief Loads palette mapping from Json file \param filename.
440
441 Returns \c true if the file was successfully parsed and \c false otherwise.
442 */
443bool QGtk3Storage::load(const QString &filename)
444{
445 return QGtk3Json::load(map&: m_palettes, fileName: filename);
446}
447
448/*!
449 \internal
450 \brief Creates a standard palette mapping.
451
452 The method creates a hard coded standard mapping, used if no external Json file
453 containing a valid mapping has been specified in the environment variable {{QT_GUI_GTK_JSON}}.
454 */
455void QGtk3Storage::createMapping()
456{
457 // Hard code standard mapping
458 BrushMap map;
459 Source source;
460
461 // Define a GTK source
462#define GTK(wtype, colorSource, state)\
463 source = Source(QGtk3Interface::QGtkWidget::gtk_ ##wtype,\
464 QGtk3Interface::QGtkColorSource::colorSource, GTK_STATE_FLAG_ ##state)
465
466 // Define a modified source
467#define LIGHTER(group, role, lighter)\
468 source = Source(QPalette::group, QPalette::role,\
469 Qt::ColorScheme::Unknown, lighter)
470#define MODIFY(group, role, red, green, blue)\
471 source = Source(QPalette::group, QPalette::role,\
472 Qt::ColorScheme::Unknown, red, green, blue)
473
474 // Define fixed source
475#define FIX(color) source = FixedSource(color);
476
477 // Add the source to a target brush
478 // Use default Qt::ColorScheme::Unknown, if no color scheme was specified
479#define ADD_2(group, role) map.insert(TargetBrush(QPalette::group, QPalette::role), source);
480#define ADD_3(group, role, app) map.insert(TargetBrush(QPalette::group, QPalette::role,\
481 Qt::ColorScheme::app), source);
482#define ADD_X(x, group, role, app, FUNC, ...) FUNC
483#define ADD(...) ADD_X(,##__VA_ARGS__, ADD_3(__VA_ARGS__), ADD_2(__VA_ARGS__))
484 // Save target brushes to a palette type
485#define SAVE(palette) m_palettes.insert(QPlatformTheme::palette, map)
486 // Clear brushes to start next palette
487#define CLEAR map.clear()
488
489 /*
490 Macro usage:
491
492 1. Define a source
493 GTK(QGtkWidget, QGtkColorSource, GTK_STATE_FLAG)
494 Fetch the color from a GtkWidget, related to a source and a state.
495
496 LIGHTER(ColorGroup, ColorROle, lighter)
497 Use a color of the same QPalette related to ColorGroup and ColorRole.
498 Make the color lighter (if lighter >100) or darker (if lighter < 100)
499
500 MODIFY(ColorGroup, ColorRole, red, green, blue)
501 Use a color of the same QPalette related to ColorGroup and ColorRole.
502 Modify it by adding red, green, blue.
503
504 FIX(const QBrush &)
505 Use a fixed brush without querying GTK
506
507 2. Define the target
508 Use ADD(ColorGroup, ColorRole) to use the defined source for the
509 color group / role in the current palette.
510
511 Use ADD(ColorGroup, ColorRole, ColorScheme) to use the defined source
512 only for a specific color scheme
513
514 3. Save mapping
515 Save the defined mappings for a specific palette.
516 If a mapping entry does not cover all color groups and roles of a palette,
517 the system palette will be used for the remaining values.
518 If the system palette does not have all combination of color groups and roles,
519 the remaining ones will be populated by a hard coded fusion-style like palette.
520
521 4. Clear mapping
522 Use CLEAR to clear the mapping and begin a new one.
523 */
524
525
526 // System palette
527 // background color and calculate derivates
528 GTK(Default, Background, INSENSITIVE);
529 ADD(Normal, Window);
530 ADD(Normal, Button);
531 ADD(Normal, Base);
532 ADD(Inactive, Base);
533 ADD(Inactive, Window);
534 LIGHTER(Normal, Window, 125);
535 ADD(Normal, Light);
536 LIGHTER(Normal, Window, 70);
537 ADD(Normal, Shadow);
538 LIGHTER(Normal, Window, 80);
539 ADD(Normal, Dark);
540 GTK(button, Foreground, ACTIVE);
541 ADD(Inactive, WindowText);
542 LIGHTER(Normal, WindowText, 50);
543 ADD(Disabled, Text);
544 ADD(Disabled, WindowText);
545 ADD(Inactive, ButtonText);
546 GTK(button, Text, NORMAL);
547 ADD(Disabled, ButtonText);
548 // special background colors
549 GTK(Default, Background, SELECTED);
550 ADD(Disabled, Highlight);
551 ADD(Normal, Highlight);
552 GTK(entry, Foreground, SELECTED);
553 ADD(Normal, HighlightedText);
554 GTK(entry, Background, ACTIVE);
555 ADD(Inactive, HighlightedText);
556 // text color and friends
557 GTK(entry, Text, NORMAL);
558 ADD(Normal, ButtonText);
559 ADD(Normal, WindowText);
560 ADD(Disabled, WindowText);
561 ADD(Disabled, HighlightedText);
562 GTK(Default, Text, NORMAL);
563 ADD(Normal, Text);
564 ADD(Normal, WindowText);
565 ADD(Inactive, Text);
566 ADD(Normal, HighlightedText);
567 LIGHTER(Normal, Base, 93);
568 ADD(All, AlternateBase);
569 GTK(Default, Foreground, NORMAL);
570 ADD(All, ToolTipText);
571 MODIFY(Normal, Text, 100, 100, 100);
572 ADD(All, PlaceholderText, Light);
573 MODIFY(Normal, Text, -100, -100, -100);
574 ADD(All, PlaceholderText, Dark);
575 SAVE(SystemPalette);
576 CLEAR;
577
578 // Checkbox and Radio Button
579 GTK(button, Text, ACTIVE);
580 ADD(Normal, Base, Dark);
581 GTK(Default, Background, NORMAL);
582 ADD(All, Base);
583 GTK(button, Text, NORMAL);
584 ADD(Normal, Base, Light);
585 SAVE(CheckBoxPalette);
586 SAVE(RadioButtonPalette);
587 CLEAR;
588
589 // ComboBox, GroupBox, Frame
590 GTK(combo_box, Text, NORMAL);
591 ADD(Normal, ButtonText, Dark);
592 ADD(Normal, Text, Dark);
593 GTK(combo_box, Text, ACTIVE);
594 ADD(Normal, ButtonText, Light);
595 ADD(Normal, Text, Light);
596 SAVE(ComboBoxPalette);
597 SAVE(GroupBoxPalette);
598 CLEAR;
599
600 // Menu bar
601 GTK(Default, Text, ACTIVE);
602 ADD(Normal, ButtonText);
603 SAVE(MenuPalette);
604 CLEAR;
605
606 // LineEdit
607 GTK(Default, Background, NORMAL);
608 ADD(All, Base);
609 SAVE(TextLineEditPalette);
610 CLEAR;
611
612#undef GTK
613#undef REC
614#undef FIX
615#undef ADD
616#undef ADD_2
617#undef ADD_3
618#undef ADD_X
619#undef SAVE
620#undef LOAD
621}
622
623QT_END_NAMESPACE
624

source code of qtbase/src/plugins/platformthemes/gtk3/qgtk3storage.cpp