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"