Warning, file /plasma/plasma-workspace/kcms/colors/colors.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).
0001 /* 0002 SPDX-FileCopyrightText: 2007 Matthew Woehlke <mw_triad@users.sourceforge.net> 0003 SPDX-FileCopyrightText: 2007 Jeremy Whiting <jpwhiting@kde.org> 0004 SPDX-FileCopyrightText: 2016 Olivier Churlaud <olivier@churlaud.com> 0005 SPDX-FileCopyrightText: 2019 Kai Uwe Broulik <kde@privat.broulik.de> 0006 SPDX-FileCopyrightText: 2019 Cyril Rossi <cyril.rossi@enioka.com> 0007 0008 SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0009 */ 0010 0011 #include "colors.h" 0012 0013 #include <QDBusConnection> 0014 #include <QDBusMessage> 0015 #include <QDBusPendingCall> 0016 #include <QDBusReply> 0017 #include <QDir> 0018 #include <QFileInfo> 0019 #include <QGuiApplication> 0020 #include <QProcess> 0021 #include <QQuickItem> 0022 #include <QQuickRenderControl> 0023 #include <QQuickWindow> 0024 #include <QStandardItemModel> 0025 #include <QStandardPaths> 0026 #include <QTemporaryFile> 0027 0028 #include <KColorScheme> 0029 #include <KColorUtils> 0030 #include <KConfigGroup> 0031 #include <KLocalizedString> 0032 #include <KPluginFactory> 0033 #include <KWindowSystem> 0034 0035 #include <KIO/DeleteJob> 0036 #include <KIO/FileCopyJob> 0037 #include <KIO/JobUiDelegate> 0038 0039 #include <algorithm> 0040 0041 #include "krdb.h" 0042 0043 #include "colorsapplicator.h" 0044 #include "colorsdata.h" 0045 #include "filterproxymodel.h" 0046 0047 #include "../kcms-common_p.h" 0048 0049 using namespace Qt::StringLiterals; 0050 0051 K_PLUGIN_FACTORY_WITH_JSON(KCMColorsFactory, "kcm_colors.json", registerPlugin<KCMColors>(); registerPlugin<ColorsData>();) 0052 0053 #define PROPERTY(name) \ 0054 Q_PROPERTY(QColor name READ name CONSTANT) \ 0055 QColor name() const \ 0056 { \ 0057 return p.name().color(); \ 0058 } 0059 0060 struct QPaletteExtension { 0061 Q_GADGET 0062 QPalette p; 0063 PROPERTY(windowText) 0064 PROPERTY(button) 0065 PROPERTY(light) 0066 PROPERTY(dark) 0067 PROPERTY(mid) 0068 PROPERTY(text) 0069 PROPERTY(base) 0070 PROPERTY(alternateBase) 0071 PROPERTY(toolTipBase) 0072 PROPERTY(toolTipText) 0073 PROPERTY(window) 0074 PROPERTY(midlight) 0075 PROPERTY(brightText) 0076 PROPERTY(buttonText) 0077 PROPERTY(shadow) 0078 PROPERTY(highlight) 0079 PROPERTY(highlightedText) 0080 PROPERTY(link) 0081 PROPERTY(linkVisited) 0082 PROPERTY(placeholderText) 0083 }; 0084 0085 KCMColors::KCMColors(QObject *parent, const KPluginMetaData &data) 0086 : KQuickManagedConfigModule(parent, data) 0087 , m_model(new ColorsModel(this)) 0088 , m_filteredModel(new FilterProxyModel(this)) 0089 , m_data(new ColorsData(this)) 0090 , m_config(KSharedConfig::openConfig(QStringLiteral("kdeglobals"))) 0091 , m_configWatcher(KConfigWatcher::create(m_config)) 0092 { 0093 auto uri = "org.kde.private.kcms.colors"; 0094 qmlRegisterUncreatableType<KCMColors>(uri, 1, 0, "KCM", QStringLiteral("Cannot create instances of KCM")); 0095 qmlRegisterAnonymousType<ColorsModel>(uri, 1); 0096 qmlRegisterAnonymousType<FilterProxyModel>(uri, 1); 0097 qmlRegisterAnonymousType<ColorsSettings>(uri, 1); 0098 qmlRegisterExtendedUncreatableType<QPalette, QPaletteExtension>(uri, 1, 0, "paletteextension", u"palettes are read only"_qs); 0099 0100 connect(m_model, &ColorsModel::pendingDeletionsChanged, this, &KCMColors::settingsChanged); 0101 0102 connect(m_model, &ColorsModel::selectedSchemeChanged, this, [this](const QString &scheme) { 0103 m_selectedSchemeDirty = true; 0104 colorsSettings()->setColorScheme(scheme); 0105 }); 0106 0107 connect(colorsSettings(), &ColorsSettings::colorSchemeChanged, this, [this] { 0108 m_model->setSelectedScheme(colorsSettings()->colorScheme()); 0109 }); 0110 0111 connect(colorsSettings(), &ColorsSettings::accentColorChanged, this, &KCMColors::accentColorChanged); 0112 connect(colorsSettings(), &ColorsSettings::accentColorFromWallpaperChanged, this, &KCMColors::accentColorFromWallpaperChanged); 0113 0114 connect(m_model, &ColorsModel::selectedSchemeChanged, m_filteredModel, &FilterProxyModel::setSelectedScheme); 0115 m_filteredModel->setSourceModel(m_model); 0116 0117 // Since the accent color can now change from somewhere else, we need to update the view accordingly. 0118 connect(m_configWatcher.data(), &KConfigWatcher::configChanged, this, [this](const KConfigGroup &group, const QByteArrayList &names) { 0119 if (group.name() == QLatin1String("General") && names.contains(QByteArrayLiteral("AccentColor"))) { 0120 colorsSettings()->save(); // We need to first save the local changes, if any. 0121 colorsSettings()->load(); 0122 } 0123 }); 0124 } 0125 0126 KCMColors::~KCMColors() 0127 { 0128 m_config->markAsClean(); 0129 } 0130 0131 ColorsModel *KCMColors::model() const 0132 { 0133 return m_model; 0134 } 0135 0136 FilterProxyModel *KCMColors::filteredModel() const 0137 { 0138 return m_filteredModel; 0139 } 0140 0141 ColorsSettings *KCMColors::colorsSettings() const 0142 { 0143 return m_data->settings(); 0144 } 0145 0146 QColor KCMColors::accentColor() const 0147 { 0148 const QColor color = colorsSettings()->accentColor(); 0149 if (!color.isValid()) { 0150 return QColor(Qt::transparent); 0151 } 0152 return color; 0153 } 0154 0155 QColor KCMColors::tinted(const QColor &color, const QColor &accent, bool tints, qreal tintFactor) 0156 { 0157 if (accent == QColor(Qt::transparent) || !tints) { 0158 return color; 0159 } 0160 return tintColor(color, accentColor(), tintFactor); 0161 } 0162 0163 void KCMColors::setAccentColor(const QColor &accentColor) 0164 { 0165 colorsSettings()->setAccentColor(accentColor); 0166 Q_EMIT settingsChanged(); 0167 } 0168 0169 bool KCMColors::accentColorFromWallpaper() const 0170 { 0171 return colorsSettings()->accentColorFromWallpaper(); 0172 } 0173 0174 void KCMColors::setAccentColorFromWallpaper(bool boolean) 0175 { 0176 if (boolean == colorsSettings()->accentColorFromWallpaper()) { 0177 return; 0178 } 0179 if (boolean) { 0180 applyWallpaperAccentColor(); 0181 } 0182 colorsSettings()->setAccentColorFromWallpaper(boolean); 0183 Q_EMIT accentColorFromWallpaperChanged(); 0184 Q_EMIT settingsChanged(); 0185 } 0186 0187 QColor KCMColors::lastUsedCustomAccentColor() const 0188 { 0189 return colorsSettings()->lastUsedCustomAccentColor(); 0190 } 0191 void KCMColors::setLastUsedCustomAccentColor(const QColor &accentColor) 0192 { 0193 // Don't allow transparent since it will conflict with its usage for indicating default accent color 0194 if (accentColor == QColor(Qt::transparent)) { 0195 return; 0196 } 0197 0198 colorsSettings()->setLastUsedCustomAccentColor(accentColor); 0199 Q_EMIT lastUsedCustomAccentColorChanged(); 0200 Q_EMIT settingsChanged(); 0201 } 0202 bool KCMColors::downloadingFile() const 0203 { 0204 return m_tempCopyJob; 0205 } 0206 0207 void KCMColors::knsEntryChanged(const KNSCore::Entry &entry) 0208 { 0209 if (!entry.isValid()) { 0210 return; 0211 } 0212 m_model->load(); 0213 0214 // If a new theme was installed, select the first color file in it 0215 QStringList installedThemes; 0216 const QString suffix = QStringLiteral(".colors"); 0217 if (entry.status() == KNSCore::Entry::Installed) { 0218 for (const QString &path : entry.installedFiles()) { 0219 const QString fileName = path.section(QLatin1Char('/'), -1, -1); 0220 0221 const int suffixPos = fileName.indexOf(suffix); 0222 if (suffixPos != fileName.length() - suffix.length()) { 0223 continue; 0224 } 0225 0226 installedThemes.append(fileName.left(suffixPos)); 0227 } 0228 0229 if (!installedThemes.isEmpty()) { 0230 // The list is sorted by (potentially translated) name 0231 // but that would require us parse every file, so this should be close enough 0232 std::sort(installedThemes.begin(), installedThemes.end()); 0233 0234 m_model->setSelectedScheme(installedThemes.constFirst()); 0235 } 0236 } 0237 } 0238 0239 void KCMColors::loadSelectedColorScheme() 0240 { 0241 colorsSettings()->config()->reparseConfiguration(); 0242 colorsSettings()->read(); 0243 const QString schemeName = colorsSettings()->colorScheme(); 0244 0245 // If the scheme named in kdeglobals doesn't exist, show a warning and use default scheme 0246 if (m_model->indexOfScheme(schemeName) == -1) { 0247 m_model->setSelectedScheme(colorsSettings()->defaultColorSchemeValue()); 0248 // These are normally synced but initially the model doesn't Q_EMIT a change to avoid the 0249 // Apply button from being enabled without any user interaction. Sync manually here. 0250 m_filteredModel->setSelectedScheme(colorsSettings()->defaultColorSchemeValue()); 0251 Q_EMIT showSchemeNotInstalledWarning(schemeName); 0252 } else { 0253 m_model->setSelectedScheme(schemeName); 0254 m_filteredModel->setSelectedScheme(schemeName); 0255 } 0256 setNeedsSave(false); 0257 } 0258 0259 void KCMColors::installSchemeFromFile(const QUrl &url) 0260 { 0261 if (url.isLocalFile()) { 0262 installSchemeFile(url.toLocalFile()); 0263 return; 0264 } 0265 0266 if (m_tempCopyJob) { 0267 return; 0268 } 0269 0270 m_tempInstallFile.reset(new QTemporaryFile()); 0271 if (!m_tempInstallFile->open()) { 0272 Q_EMIT showErrorMessage(i18n("Unable to create a temporary file.")); 0273 m_tempInstallFile.reset(); 0274 return; 0275 } 0276 0277 // Ideally we copied the file into the proper location right away but 0278 // (for some reason) we determine the file name from the "Name" inside the file 0279 m_tempCopyJob = KIO::file_copy(url, QUrl::fromLocalFile(m_tempInstallFile->fileName()), -1, KIO::Overwrite); 0280 m_tempCopyJob->uiDelegate()->setAutoErrorHandlingEnabled(true); 0281 Q_EMIT downloadingFileChanged(); 0282 0283 connect(m_tempCopyJob, &KIO::FileCopyJob::result, this, [this, url](KJob *job) { 0284 if (job->error() != KJob::NoError) { 0285 Q_EMIT showErrorMessage(i18n("Unable to download the color scheme: %1", job->errorText())); 0286 return; 0287 } 0288 0289 installSchemeFile(m_tempInstallFile->fileName()); 0290 m_tempInstallFile.reset(); 0291 }); 0292 connect(m_tempCopyJob, &QObject::destroyed, this, &KCMColors::downloadingFileChanged); 0293 } 0294 0295 void KCMColors::installSchemeFile(const QString &path) 0296 { 0297 KSharedConfigPtr config = KSharedConfig::openConfig(path, KConfig::SimpleConfig); 0298 0299 KConfigGroup group(config, u"General"_s); 0300 const QString name = group.readEntry("Name"); 0301 0302 if (name.isEmpty()) { 0303 Q_EMIT showErrorMessage(i18n("This file is not a color scheme file.")); 0304 return; 0305 } 0306 0307 // Do not overwrite another scheme 0308 int increment = 0; 0309 QString newName = name; 0310 QString testpath; 0311 do { 0312 if (increment) { 0313 newName = name + QString::number(increment); 0314 } 0315 testpath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("color-schemes/%1.colors").arg(newName)); 0316 increment++; 0317 } while (!testpath.isEmpty()); 0318 0319 QString newPath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/color-schemes/"); 0320 0321 if (!QDir().mkpath(newPath)) { 0322 Q_EMIT showErrorMessage(i18n("Failed to create 'color-scheme' data folder.")); 0323 return; 0324 } 0325 0326 newPath += newName + QLatin1String(".colors"); 0327 0328 if (!QFile::copy(path, newPath)) { 0329 Q_EMIT showErrorMessage(i18n("Failed to copy color scheme into 'color-scheme' data folder.")); 0330 return; 0331 } 0332 0333 // Update name 0334 KSharedConfigPtr config2 = KSharedConfig::openConfig(newPath, KConfig::SimpleConfig); 0335 KConfigGroup group2(config2, u"General"_s); 0336 group2.writeEntry("Name", newName); 0337 config2->sync(); 0338 0339 m_model->load(); 0340 0341 const auto results = m_model->match(m_model->index(0, 0), ColorsModel::SchemeNameRole, newName, 1, Qt::MatchExactly); 0342 if (!results.isEmpty()) { 0343 m_model->setSelectedScheme(newName); 0344 } 0345 0346 Q_EMIT showSuccessMessage(i18n("Color scheme installed successfully.")); 0347 } 0348 0349 void KCMColors::editScheme(const QString &schemeName, QQuickItem *ctx) 0350 { 0351 if (m_editDialogProcess) { 0352 return; 0353 } 0354 0355 QModelIndex idx = m_model->index(m_model->indexOfScheme(schemeName), 0); 0356 0357 m_editDialogProcess = new QProcess(this); 0358 connect(m_editDialogProcess, &QProcess::finished, this, [this](int exitCode, QProcess::ExitStatus exitStatus) { 0359 Q_UNUSED(exitCode); 0360 Q_UNUSED(exitStatus); 0361 0362 const auto savedThemes = QString::fromUtf8(m_editDialogProcess->readAllStandardOutput()).split(QLatin1Char('\n'), Qt::SkipEmptyParts); 0363 0364 if (!savedThemes.isEmpty()) { 0365 m_model->load(); // would be cool to just reload/add the changed/new ones 0366 0367 // If the currently active scheme was edited, consider settings dirty even if the scheme itself didn't change 0368 if (savedThemes.contains(colorsSettings()->colorScheme())) { 0369 m_activeSchemeEdited = true; 0370 settingsChanged(); 0371 } 0372 0373 m_model->setSelectedScheme(savedThemes.last()); 0374 } 0375 0376 m_editDialogProcess->deleteLater(); 0377 m_editDialogProcess = nullptr; 0378 }); 0379 0380 QStringList args; 0381 args << idx.data(ColorsModel::SchemeNameRole).toString(); 0382 if (idx.data(ColorsModel::RemovableRole).toBool()) { 0383 args << QStringLiteral("--overwrite"); 0384 } 0385 0386 if (ctx && ctx->window()) { 0387 // QQuickWidget, used for embedding QML KCMs, renders everything into an offscreen window 0388 // Qt is able to resolve this on its own when setting transient parents in-process. 0389 // However, since we pass the ID to an external process which has no idea of this 0390 // we need to resolve the actual window we end up showing in. 0391 if (QWindow *actualWindow = QQuickRenderControl::renderWindowFor(ctx->window())) { 0392 if (KWindowSystem::isPlatformX11()) { 0393 // TODO wayland: once we have foreign surface support 0394 args << QStringLiteral("--attach") << (QStringLiteral("x11:") + QString::number(actualWindow->winId())); 0395 } 0396 } 0397 } 0398 0399 m_editDialogProcess->start(QStringLiteral("kcolorschemeeditor"), args); 0400 } 0401 0402 bool KCMColors::isSaveNeeded() const 0403 { 0404 return m_activeSchemeEdited || !m_model->match(m_model->index(0, 0), ColorsModel::PendingDeletionRole, true).isEmpty() || colorsSettings()->isSaveNeeded(); 0405 } 0406 0407 void KCMColors::load() 0408 { 0409 KQuickManagedConfigModule::load(); 0410 m_model->load(); 0411 0412 m_config->markAsClean(); 0413 m_config->reparseConfiguration(); 0414 0415 loadSelectedColorScheme(); 0416 0417 Q_EMIT accentColorFromWallpaperChanged(); 0418 Q_EMIT accentColorChanged(); 0419 0420 // If need save is true at the end of load() function, it will stay disabled forever. 0421 // setSelectedScheme() call due to unexisting scheme name in kdeglobals will trigger a need to save. 0422 // this following call ensure the apply button will work properly. 0423 setNeedsSave(false); 0424 } 0425 0426 void KCMColors::save() 0427 { 0428 auto msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.KWin"), 0429 QStringLiteral("/org/kde/KWin/BlendChanges"), 0430 QStringLiteral("org.kde.KWin.BlendChanges"), 0431 QStringLiteral("start")); 0432 msg << 300; 0433 // This is deliberately blocking so that we ensure Kwin has processed the 0434 // animation start event before we potentially trigger client side changes 0435 QDBusConnection::sessionBus().call(msg); 0436 0437 // We need to save the colors change first, to avoid a situation, 0438 // when we announced that the color scheme has changed, but 0439 // the colors themselves in the color scheme have not yet 0440 if (m_selectedSchemeDirty || m_activeSchemeEdited || colorsSettings()->isSaveNeeded()) { 0441 saveColors(); 0442 } 0443 0444 KQuickManagedConfigModule::save(); 0445 notifyKcmChange(GlobalChangeType::PaletteChanged); 0446 m_activeSchemeEdited = false; 0447 0448 processPendingDeletions(); 0449 } 0450 0451 void KCMColors::saveColors() 0452 { 0453 const QString path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("color-schemes/%1.colors").arg(m_model->selectedScheme())); 0454 0455 // Can't use KConfig readEntry because the config is not saved yet 0456 applyScheme(path, colorsSettings()->config(), KConfig::Normal, accentColor()); 0457 m_selectedSchemeDirty = false; 0458 } 0459 0460 QColor KCMColors::accentBackground(const QColor &accent, const QColor &background) 0461 { 0462 return ::accentBackground(accent, background); 0463 } 0464 0465 QColor KCMColors::accentForeground(const QColor &accent, const bool &isActive) 0466 { 0467 return ::accentForeground(accent, isActive); 0468 } 0469 0470 void KCMColors::applyWallpaperAccentColor() 0471 { 0472 QDBusMessage accentColor = QDBusMessage::createMethodCall("org.kde.plasmashell", "/PlasmaShell", "org.kde.PlasmaShell", "color"); 0473 auto const connection = QDBusConnection::connectToBus(QDBusConnection::SessionBus, "accentColorBus"); 0474 QDBusPendingCall async = connection.asyncCall(accentColor); 0475 QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(async, this); 0476 0477 connect(watcher, &QDBusPendingCallWatcher::finished, this, &KCMColors::wallpaperAccentColorArrivedSlot); 0478 } 0479 0480 void KCMColors::wallpaperAccentColorArrivedSlot(QDBusPendingCallWatcher *call) 0481 { 0482 QDBusPendingReply<uint> reply = *call; 0483 if (!reply.isError()) { 0484 setAccentColor(QColor::fromRgba(reply.value())); 0485 } 0486 call->deleteLater(); 0487 } 0488 0489 void KCMColors::processPendingDeletions() 0490 { 0491 const QStringList pendingDeletions = m_model->pendingDeletions(); 0492 0493 for (const QString &schemeName : pendingDeletions) { 0494 Q_ASSERT(schemeName != m_model->selectedScheme()); 0495 0496 const QString path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("color-schemes/%1.colors").arg(schemeName)); 0497 0498 auto *job = KIO::del(QUrl::fromLocalFile(path), KIO::HideProgressInfo); 0499 // needs to block for it to work on "OK" where the dialog (kcmshell) closes 0500 job->exec(); 0501 } 0502 0503 m_model->removeItemsPendingDeletion(); 0504 } 0505 0506 #include "colors.moc" 0507 #include "moc_colors.cpp"