1/*
2 This file is part of the KDE project
3 SPDX-FileCopyrightText: 2013 Martin Gräßlin <mgraesslin@kde.org>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6*/
7
8#include "kcolorschememanager.h"
9#include "kcolorschememanager_p.h"
10
11#include "kcolorscheme.h"
12#include "kcolorschememodel.h"
13
14#include <KColorSchemeWatcher>
15#include <KConfigGroup>
16#include <KConfigGui>
17#include <KLocalizedString>
18#include <KSharedConfig>
19
20#include <QDir>
21#include <QFileInfo>
22#include <QGuiApplication>
23#include <QIcon>
24#include <QPainter>
25#include <QPointer>
26#include <QStandardPaths>
27
28#include <private/qguiapplication_p.h>
29#include <qpa/qplatformtheme.h>
30
31// ensure we are linking KConfigGui, so QColor I/O from KConfig works
32KCONFIGGUI_EXPORT int initKConfigGroupGui();
33static int s_init = initKConfigGroupGui();
34
35constexpr int defaultSchemeRow = 0;
36
37static bool isKdePlatformTheme()
38{
39 if (!QGuiApplicationPrivate::platformTheme()) {
40 return false;
41 }
42
43 if (QGuiApplicationPrivate::platformTheme()->name() == QLatin1String("kde")) {
44 return true;
45 }
46
47 if (qgetenv(varName: "XDG_CURRENT_DESKTOP") == "KDE" && QGuiApplicationPrivate::platformTheme()->name() == QLatin1String("xdgdesktopportal")) {
48 return true;
49 }
50
51 return false;
52}
53
54void KColorSchemeManagerPrivate::activateSchemeInternal(const QString &colorSchemePath)
55{
56 // hint for plasma-integration to synchronize the color scheme with the window manager/compositor
57 // The property needs to be set before the palette change because is is checked upon the
58 // ApplicationPaletteChange event.
59 qApp->setProperty(name: "KDE_COLOR_SCHEME_PATH", value: colorSchemePath);
60 if (colorSchemePath.isEmpty()) {
61 qApp->setPalette(QPalette());
62 } else {
63 qApp->setPalette(KColorScheme::createApplicationPalette(config: KSharedConfig::openConfig(fileName: colorSchemePath)));
64 }
65}
66
67QString KColorSchemeManagerPrivate::automaticColorSchemeId() const
68{
69 if (!m_colorSchemeWatcher) {
70 return QString();
71 }
72
73 switch (m_colorSchemeWatcher->systemPreference()) {
74 case KColorSchemeWatcher::PreferHighContrast:
75 return QString();
76 case KColorSchemeWatcher::PreferDark:
77 return getDarkColorScheme();
78 case KColorSchemeWatcher::PreferLight:
79 case KColorSchemeWatcher::NoPreference:
80 return getLightColorScheme();
81 };
82 return QString();
83}
84
85// The meaning of the Default entry depends on the platform
86// On KDE we apply a default KColorScheme
87// On other platforms we automatically apply Breeze/Breeze Dark depending on the system preference
88QString KColorSchemeManagerPrivate::automaticColorSchemePath() const
89{
90 const QString colorSchemeId = automaticColorSchemeId();
91 if (colorSchemeId.isEmpty()) {
92 return QString();
93 } else {
94 return indexForSchemeId(id: colorSchemeId).data(arole: KColorSchemeModel::PathRole).toString();
95 }
96}
97
98QIcon KColorSchemeManagerPrivate::createPreview(const QString &path)
99{
100 KSharedConfigPtr schemeConfig = KSharedConfig::openConfig(fileName: path, mode: KConfig::SimpleConfig);
101 QIcon result;
102
103 KColorScheme activeWindow(QPalette::Active, KColorScheme::Window, schemeConfig);
104 KColorScheme activeButton(QPalette::Active, KColorScheme::Button, schemeConfig);
105 KColorScheme activeView(QPalette::Active, KColorScheme::View, schemeConfig);
106 KColorScheme activeSelection(QPalette::Active, KColorScheme::Selection, schemeConfig);
107
108 auto pixmap = [&](int size) {
109 QPixmap pix(size, size);
110 pix.fill(fillColor: Qt::black);
111 QPainter p;
112 p.begin(&pix);
113 const int itemSize = size / 2 - 1;
114 p.fillRect(x: 1, y: 1, w: itemSize, h: itemSize, b: activeWindow.background());
115 p.fillRect(x: 1 + itemSize, y: 1, w: itemSize, h: itemSize, b: activeButton.background());
116 p.fillRect(x: 1, y: 1 + itemSize, w: itemSize, h: itemSize, b: activeView.background());
117 p.fillRect(x: 1 + itemSize, y: 1 + itemSize, w: itemSize, h: itemSize, b: activeSelection.background());
118 p.end();
119 result.addPixmap(pixmap: pix);
120 };
121 // 16x16
122 pixmap(16);
123 // 24x24
124 pixmap(24);
125
126 return result;
127}
128
129KColorSchemeManagerPrivate::KColorSchemeManagerPrivate()
130 : model(new KColorSchemeModel())
131{
132}
133
134KColorSchemeManager::KColorSchemeManager(GuardApplicationConstructor, QGuiApplication *app)
135 : QObject(app)
136 , d(new KColorSchemeManagerPrivate)
137{
138 init();
139}
140
141#if KCOLORSCHEME_BUILD_DEPRECATED_SINCE(6, 6)
142KColorSchemeManager::KColorSchemeManager(QObject *parent)
143 : QObject(parent)
144 , d(new KColorSchemeManagerPrivate())
145{
146 init();
147}
148#endif
149
150KColorSchemeManager::~KColorSchemeManager()
151{
152}
153
154void KColorSchemeManager::init()
155{
156 QString platformThemeSchemePath = qApp->property(name: "KDE_COLOR_SCHEME_PATH").toString();
157 if (!isKdePlatformTheme() && platformThemeSchemePath.isEmpty()) {
158 d->m_colorSchemeWatcher.emplace();
159 QObject::connect(sender: &*d->m_colorSchemeWatcher, signal: &KColorSchemeWatcher::systemPreferenceChanged, context: this, slot: [this]() {
160 if (!d->m_activatedScheme.isEmpty()) {
161 // Don't override what has been manually set
162 return;
163 }
164
165 d->activateSchemeInternal(colorSchemePath: d->automaticColorSchemePath());
166 });
167 }
168
169 KSharedConfigPtr config = KSharedConfig::openConfig();
170 KConfigGroup cg(config, QStringLiteral("UiSettings"));
171 const QString scheme = cg.readEntry(key: "ColorScheme", aDefault: QString());
172
173 QString schemePath;
174
175 if (scheme.isEmpty() || scheme == QLatin1String("Default")) {
176 // Color scheme might be already set from a platform theme
177 // This is used for example by QGnomePlatform that can set color scheme
178 // matching GNOME settings. This avoids issues where QGnomePlatform sets
179 // QPalette for dark theme, but end up mixing it also with Breeze light
180 // that is going to be used as a fallback for apps using KColorScheme.
181 // BUG: 447029
182 if (platformThemeSchemePath.isEmpty()) {
183 schemePath = d->automaticColorSchemePath();
184 }
185 } else {
186 const auto index = indexForScheme(name: scheme);
187 schemePath = index.data(arole: KColorSchemeModel::PathRole).toString();
188 d->m_activatedScheme = index.data(arole: KColorSchemeModel::IdRole).toString();
189 }
190
191 if (!schemePath.isEmpty()) {
192 d->activateSchemeInternal(colorSchemePath: schemePath);
193 }
194}
195
196QAbstractItemModel *KColorSchemeManager::model() const
197{
198 return d->model.get();
199}
200
201QModelIndex KColorSchemeManagerPrivate::indexForSchemeId(const QString &id) const
202{
203 // Empty string is mapped to "reset to the system scheme"
204 if (id.isEmpty()) {
205 return model->index(row: defaultSchemeRow);
206 }
207 for (int i = 1; i < model->rowCount(); ++i) {
208 QModelIndex index = model->index(row: i);
209 if (index.data(arole: KColorSchemeModel::IdRole).toString() == id) {
210 return index;
211 }
212 }
213 return QModelIndex();
214}
215
216void KColorSchemeManager::setAutosaveChanges(bool autosaveChanges)
217{
218 d->m_autosaveChanges = autosaveChanges;
219}
220
221QModelIndex KColorSchemeManager::indexForSchemeId(const QString &id) const
222{
223 return d->indexForSchemeId(id);
224}
225
226QModelIndex KColorSchemeManager::indexForScheme(const QString &name) const
227{
228 // Empty string is mapped to "reset to the system scheme"
229 if (name.isEmpty()) {
230 return d->model->index(row: defaultSchemeRow);
231 }
232 for (int i = 1; i < d->model->rowCount(); ++i) {
233 QModelIndex index = d->model->index(row: i);
234 if (index.data(arole: KColorSchemeModel::NameRole).toString() == name) {
235 return index;
236 }
237 }
238 return QModelIndex();
239}
240
241void KColorSchemeManager::activateScheme(const QModelIndex &index)
242{
243 const bool isDefaultEntry = index.data(arole: KColorSchemeModel::PathRole).toString().isEmpty();
244
245 if (index.isValid() && index.model() == d->model.get() && !isDefaultEntry) {
246 d->activateSchemeInternal(colorSchemePath: index.data(arole: KColorSchemeModel::PathRole).toString());
247 d->m_activatedScheme = index.data(arole: KColorSchemeModel::IdRole).toString();
248 if (d->m_autosaveChanges) {
249 saveSchemeToConfigFile(schemeName: index.data(arole: KColorSchemeModel::NameRole).toString());
250 }
251 } else {
252 d->activateSchemeInternal(colorSchemePath: d->automaticColorSchemePath());
253 d->m_activatedScheme = QString();
254 if (d->m_autosaveChanges) {
255 saveSchemeToConfigFile(schemeName: QString());
256 }
257 }
258}
259
260void KColorSchemeManager::saveSchemeToConfigFile(const QString &schemeName) const
261{
262 KSharedConfigPtr config = KSharedConfig::openConfig();
263 KConfigGroup cg(config, QStringLiteral("UiSettings"));
264
265 if (schemeName.isEmpty() && !cg.hasDefault(key: "ColorScheme")) {
266 cg.revertToDefault(key: "ColorScheme");
267 } else {
268 cg.writeEntry(key: "ColorScheme", value: KLocalizedString::removeAcceleratorMarker(label: schemeName));
269 }
270
271 cg.sync();
272}
273
274QString KColorSchemeManager::activeSchemeId() const
275{
276 return d->m_activatedScheme;
277}
278
279QString KColorSchemeManager::activeSchemeName() const
280{
281 return d->indexForSchemeId(id: d->m_activatedScheme).data(arole: KColorSchemeModel::NameRole).toString();
282}
283
284KColorSchemeManager *KColorSchemeManager::instance()
285{
286 Q_ASSERT(qApp);
287 static QPointer<KColorSchemeManager> manager;
288 if (!manager) {
289 manager = new KColorSchemeManager(GuardApplicationConstructor{}, qApp);
290 }
291 return manager;
292}
293
294#include "moc_kcolorschememanager.cpp"
295

source code of kcolorscheme/src/kcolorschememanager.cpp