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