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"