File indexing completed on 2024-05-19 05:38:21

0001 /*
0002     SPDX-FileCopyrightText: 2014 Marco Martin <mart@kde.org>
0003     SPDX-FileCopyrightText: 2014 Vishesh Handa <me@vhanda.in>
0004     SPDX-FileCopyrightText: 2019 Cyril Rossi <cyril.rossi@enioka.com>
0005     SPDX-FileCopyrightText: 2021 Benjamin Port <benjamin.port@enioka.com>
0006     SPDX-FileCopyrightText: 2022 Dominic Hayes <ferenosdev@outlook.com>
0007     SPDX-FileCopyrightText: 2023 Ismael Asensio <isma.af@gmail.com>
0008 
0009     SPDX-License-Identifier: LGPL-2.0-only
0010 */
0011 
0012 #include "kcm.h"
0013 #include "../kcms-common_p.h"
0014 #include "config-kcm.h"
0015 #include "config-workspace.h"
0016 #include "krdb.h"
0017 
0018 #include <KDialogJobUiDelegate>
0019 #include <KIO/ApplicationLauncherJob>
0020 #include <KIconLoader>
0021 #include <KMessageBox>
0022 #include <KService>
0023 
0024 #include <QCollator>
0025 #include <QDBusConnection>
0026 #include <QDBusMessage>
0027 #include <QDebug>
0028 #include <QProcess>
0029 #include <QQuickItem>
0030 #include <QQuickWindow>
0031 #include <QStandardItemModel>
0032 #include <QStandardPaths>
0033 #include <private/qtx11extras_p.h>
0034 
0035 #include <KLocalizedString>
0036 #include <KPackage/PackageLoader>
0037 
0038 #include <array>
0039 
0040 #include <X11/Xlib.h>
0041 
0042 #include <QFileInfo>
0043 
0044 #ifdef HAVE_XCURSOR
0045 #include "../cursortheme/xcursor/xcursortheme.h"
0046 #include <X11/Xcursor/Xcursor.h>
0047 #endif
0048 
0049 #ifdef HAVE_XFIXES
0050 #include <X11/extensions/Xfixes.h>
0051 #endif
0052 
0053 using namespace Qt::StringLiterals;
0054 
0055 KCMLookandFeel::KCMLookandFeel(QObject *parent, const KPluginMetaData &data)
0056     : KQuickManagedConfigModule(parent, data)
0057     , m_lnf(new LookAndFeelManager(this))
0058     , m_themeContents(LookAndFeelManager::Empty)
0059     , m_selectedContents(LookAndFeelManager::AppearanceSettings)
0060 {
0061     constexpr char uri[] = "org.kde.private.kcms.lookandfeel";
0062     qmlRegisterAnonymousType<LookAndFeelSettings>("", 1);
0063     qmlRegisterAnonymousType<QStandardItemModel>("", 1);
0064     qmlRegisterUncreatableType<KCMLookandFeel>(uri, 1, 0, "KCMLookandFeel", "Can't create KCMLookandFeel");
0065     qmlRegisterUncreatableType<LookAndFeelManager>(uri, 1, 0, "LookandFeelManager", "Can't create LookandFeelManager");
0066 
0067     setButtons(Default | Help);
0068 
0069     m_model = new QStandardItemModel(this);
0070     QHash<int, QByteArray> roles = m_model->roleNames();
0071     roles[PluginNameRole] = "pluginName";
0072     roles[DescriptionRole] = "description";
0073     roles[ScreenshotRole] = "screenshot";
0074     roles[FullScreenPreviewRole] = "fullScreenPreview";
0075     roles[ContentsRole] = "contents";
0076     roles[PackagePathRole] = "packagePath";
0077     roles[UninstallableRole] = "uninstallable";
0078 
0079     m_model->setItemRoleNames(roles);
0080     loadModel();
0081 
0082     connect(lookAndFeelSettings(), &LookAndFeelSettings::lookAndFeelPackageChanged, this, [this]() {
0083         // When the selected LNF package changes, update the available theme contents
0084         const int index = pluginIndex(lookAndFeelSettings()->lookAndFeelPackage());
0085         const LookAndFeelManager::Contents packageContents = m_model->index(index, 0).data(ContentsRole).value<LookAndFeelManager::Contents>();
0086         if (m_themeContents != packageContents) {
0087             m_themeContents = packageContents;
0088             Q_EMIT themeContentsChanged();
0089         }
0090         // And also reset the user selection to the new available contents
0091         resetSelectedContents();
0092     });
0093 
0094     connect(m_lnf, &LookAndFeelManager::refreshServices, this, [](const QStringList &toStop, const QList<KService::Ptr> &toStart) {
0095         for (const auto &serviceName : toStop) {
0096             // FIXME: quite ugly way to stop things, and what about non KDE things?
0097             QProcess::startDetached(QStringLiteral("kquitapp6"), {QStringLiteral("--service"), serviceName});
0098         }
0099         for (const auto &service : toStart) {
0100             auto *job = new KIO::ApplicationLauncherJob(service);
0101             job->setUiDelegate(new KDialogJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, nullptr));
0102             job->start();
0103         }
0104     });
0105     connect(m_lnf, &LookAndFeelManager::styleChanged, this, [] {
0106         // FIXME: changing style on the fly breaks QQuickWidgets
0107         notifyKcmChange(GlobalChangeType::StyleChanged);
0108     });
0109     connect(m_lnf, &LookAndFeelManager::colorsChanged, this, [] {
0110         // FIXME: changing style on the fly breaks QQuickWidgets
0111         notifyKcmChange(GlobalChangeType::PaletteChanged);
0112     });
0113     connect(m_lnf, &LookAndFeelManager::iconsChanged, this, [] {
0114         for (int i = 0; i < KIconLoader::LastGroup; i++) {
0115             KIconLoader::emitChange(KIconLoader::Group(i));
0116         }
0117     });
0118     connect(m_lnf, &LookAndFeelManager::cursorsChanged, this, &KCMLookandFeel::cursorsChanged);
0119     connect(m_lnf, &LookAndFeelManager::fontsChanged, this, [] {
0120         QDBusMessage message = QDBusMessage::createSignal("/KDEPlatformTheme", "org.kde.KDEPlatformTheme", "refreshFonts");
0121         QDBusConnection::sessionBus().send(message);
0122     });
0123 }
0124 
0125 KCMLookandFeel::~KCMLookandFeel()
0126 {
0127 }
0128 
0129 void KCMLookandFeel::knsEntryChanged(const KNSCore::Entry &entry)
0130 {
0131     if (!entry.isValid()) {
0132         return;
0133     }
0134     auto removeItemFromModel = [&entry, this]() {
0135         if (entry.uninstalledFiles().isEmpty()) {
0136             return;
0137         }
0138         const QString guessedPluginId = QFileInfo(entry.uninstalledFiles().constFirst()).fileName();
0139         const int index = pluginIndex(guessedPluginId);
0140         if (index != -1) {
0141             m_model->removeRows(index, 1);
0142         }
0143     };
0144     if (entry.status() == KNSCore::Entry::Deleted) {
0145         removeItemFromModel();
0146     } else if (entry.status() == KNSCore::Entry::Installed && !entry.installedFiles().isEmpty()) {
0147         if (!entry.uninstalledFiles().isEmpty()) {
0148             removeItemFromModel(); // In case we updated it we don't want to have it in twice
0149         }
0150         KPackage::Package pkg = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/LookAndFeel"));
0151         pkg.setPath(entry.installedFiles().constFirst());
0152         addKPackageToModel(pkg);
0153     }
0154 }
0155 
0156 QStandardItemModel *KCMLookandFeel::lookAndFeelModel() const
0157 {
0158     return m_model;
0159 }
0160 
0161 bool KCMLookandFeel::removeRow(int row, bool removeDependencies)
0162 {
0163     const QModelIndex index = m_model->index(row, 0);
0164     if (!m_model->checkIndex(index) || !index.data(UninstallableRole).toBool()) {
0165         // Invalid request
0166         return false;
0167     }
0168 
0169     KPackage::Package package = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/LookAndFeel"));
0170     package.setPath(index.data(PackagePathRole).toString());
0171 
0172     if (!package.isValid()) {
0173         return false;
0174     }
0175 
0176     const auto contentsToRemove = removeDependencies ? index.data(ContentsRole).value<LookAndFeelManager::Contents>() : LookAndFeelManager::Empty;
0177     const bool isRemoved = m_lnf->remove(package, contentsToRemove);
0178 
0179     if (isRemoved) {
0180         // Remove the theme from the item model
0181         const bool ret = m_model->removeRow(row);
0182         Q_ASSERT_X(ret, "removeRow", QStringLiteral("Failed to remove item at row %1").arg(row).toLatin1().constData()); // Shouldn't happen
0183     }
0184 
0185     return isRemoved;
0186 }
0187 
0188 int KCMLookandFeel::pluginIndex(const QString &pluginName) const
0189 {
0190     const auto results = m_model->match(m_model->index(0, 0), PluginNameRole, pluginName, 1, Qt::MatchExactly);
0191     if (results.count() == 1) {
0192         return results.first().row();
0193     }
0194 
0195     return -1;
0196 }
0197 
0198 QList<KPackage::Package> KCMLookandFeel::availablePackages(const QStringList &components)
0199 {
0200     QList<KPackage::Package> packages;
0201 
0202     const QList<KPluginMetaData> packagesMetaData = KPackage::PackageLoader::self()->listPackages(QStringLiteral("Plasma/LookAndFeel"));
0203 
0204     for (const KPluginMetaData &metadata : packagesMetaData) {
0205         KPackage::Package pkg = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/LookAndFeel"), metadata.pluginId());
0206         if (components.isEmpty()) {
0207             packages << pkg;
0208         } else {
0209             for (const auto &component : components) {
0210                 if (!pkg.filePath(component.toUtf8()).isEmpty()) {
0211                     packages << pkg;
0212                     break;
0213                 }
0214             }
0215         }
0216     }
0217 
0218     return packages;
0219 }
0220 
0221 LookAndFeelSettings *KCMLookandFeel::lookAndFeelSettings() const
0222 {
0223     return m_lnf->settings();
0224 }
0225 
0226 void KCMLookandFeel::loadModel()
0227 {
0228     m_model->clear();
0229 
0230     QList<KPackage::Package> pkgs = availablePackages({"defaults", "layouts"});
0231 
0232     // Sort case-insensitively
0233     QCollator collator;
0234     collator.setCaseSensitivity(Qt::CaseInsensitive);
0235     std::sort(pkgs.begin(), pkgs.end(), [&collator](const KPackage::Package &a, const KPackage::Package &b) {
0236         return collator.compare(a.metadata().name(), b.metadata().name()) < 0;
0237     });
0238 
0239     for (const KPackage::Package &pkg : pkgs) {
0240         addKPackageToModel(pkg);
0241     }
0242 
0243     // Model has been cleared so pretend the selected look and fell changed to force view update
0244     Q_EMIT lookAndFeelSettings()->lookAndFeelPackageChanged();
0245 }
0246 
0247 void KCMLookandFeel::addKPackageToModel(const KPackage::Package &pkg)
0248 {
0249     if (!pkg.metadata().isValid()) {
0250         return;
0251     }
0252     QStandardItem *row = new QStandardItem(pkg.metadata().name());
0253     row->setData(pkg.metadata().pluginId(), PluginNameRole);
0254     row->setData(pkg.metadata().description(), DescriptionRole);
0255     row->setData(QUrl::fromLocalFile(pkg.filePath("preview")), ScreenshotRole);
0256     row->setData(pkg.filePath("fullscreenpreview"), FullScreenPreviewRole);
0257     row->setData(QVariant::fromValue(m_lnf->packageContents(pkg)), ContentsRole);
0258 
0259     row->setData(pkg.path(), PackagePathRole);
0260     const QString writableLocation = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation);
0261     row->setData(pkg.path().startsWith(writableLocation), UninstallableRole);
0262 
0263     m_model->appendRow(row);
0264 }
0265 
0266 bool KCMLookandFeel::isSaveNeeded() const
0267 {
0268     return lookAndFeelSettings()->isSaveNeeded();
0269 }
0270 
0271 void KCMLookandFeel::load()
0272 {
0273     KQuickManagedConfigModule::load();
0274 
0275     m_package = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/LookAndFeel"), lookAndFeelSettings()->lookAndFeelPackage());
0276 }
0277 
0278 void KCMLookandFeel::save()
0279 {
0280     QString newLnfPackage = lookAndFeelSettings()->lookAndFeelPackage();
0281     KPackage::Package package = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/LookAndFeel"));
0282     package.setPath(newLnfPackage);
0283 
0284     if (!package.isValid()) {
0285         return;
0286     }
0287 
0288     KQuickManagedConfigModule::save();
0289     m_lnf->save(package, m_package, m_selectedContents);
0290     m_package.setPath(newLnfPackage);
0291     runRdb(KRdbExportQtColors | KRdbExportGtkTheme | KRdbExportColors | KRdbExportQtSettings | KRdbExportXftSettings);
0292 }
0293 
0294 void KCMLookandFeel::defaults()
0295 {
0296     KQuickManagedConfigModule::defaults();
0297     Q_EMIT showConfirmation();
0298 }
0299 
0300 LookAndFeelManager::Contents KCMLookandFeel::themeContents() const
0301 {
0302     return m_themeContents;
0303 }
0304 
0305 LookAndFeelManager::Contents KCMLookandFeel::selectedContents() const
0306 {
0307     return m_selectedContents;
0308 }
0309 
0310 void KCMLookandFeel::setSelectedContents(LookAndFeelManager::Contents items)
0311 {
0312     if (selectedContents() == items) {
0313         return;
0314     }
0315 
0316     m_selectedContents = items;
0317     Q_EMIT selectedContentsChanged();
0318 }
0319 
0320 void KCMLookandFeel::resetSelectedContents()
0321 {
0322     // Reset the user selection to those contents provided by the theme.
0323     LookAndFeelManager::Contents resetContents = m_themeContents;
0324     // But do not select layout contents by default if there appaerance settings
0325     if (m_themeContents & LookAndFeelManager::AppearanceSettings) {
0326         resetContents &= ~LookAndFeelManager::LayoutSettings;
0327     }
0328     setSelectedContents(resetContents);
0329 }
0330 
0331 QDir KCMLookandFeel::cursorThemeDir(const QString &theme, const int depth)
0332 {
0333     // Prevent infinite recursion
0334     if (depth > 10) {
0335         return QDir();
0336     }
0337 
0338     // Search each icon theme directory for 'theme'
0339     foreach (const QString &baseDir, cursorSearchPaths()) {
0340         QDir dir(baseDir);
0341         if (!dir.exists() || !dir.cd(theme)) {
0342             continue;
0343         }
0344 
0345         // If there's a cursors subdir, we'll assume this is a cursor theme
0346         if (dir.exists(QStringLiteral("cursors"))) {
0347             return dir;
0348         }
0349 
0350         // If the theme doesn't have an index.theme file, it can't inherit any themes.
0351         if (!dir.exists(QStringLiteral("index.theme"))) {
0352             continue;
0353         }
0354 
0355         // Open the index.theme file, so we can get the list of inherited themes
0356         KConfig config(dir.path() + QStringLiteral("/index.theme"), KConfig::NoGlobals);
0357         KConfigGroup cg(&config, u"Icon Theme"_s);
0358 
0359         // Recurse through the list of inherited themes, to check if one of them
0360         // is a cursor theme.
0361         const QStringList inherits = cg.readEntry("Inherits", QStringList());
0362         for (const QString &inherit : inherits) {
0363             // Avoid possible DoS
0364             if (inherit == theme) {
0365                 continue;
0366             }
0367 
0368             if (cursorThemeDir(inherit, depth + 1).exists()) {
0369                 return dir;
0370             }
0371         }
0372     }
0373 
0374     return QDir();
0375 }
0376 
0377 QStringList KCMLookandFeel::cursorSearchPaths()
0378 {
0379 #ifdef HAVE_XCURSOR
0380 #if XCURSOR_LIB_MAJOR == 1 && XCURSOR_LIB_MINOR < 1
0381 
0382     if (!m_cursorSearchPaths.isEmpty())
0383         return m_cursorSearchPaths;
0384     // These are the default paths Xcursor will scan for cursor themes
0385     QString path("~/.icons:/usr/share/icons:/usr/share/pixmaps:/usr/X11R6/lib/X11/icons");
0386 
0387     // If XCURSOR_PATH is set, use that instead of the default path
0388     char *xcursorPath = std::getenv("XCURSOR_PATH");
0389     if (xcursorPath)
0390         path = xcursorPath;
0391 #else
0392     // Get the search path from Xcursor
0393     QString path = XcursorLibraryPath();
0394 #endif
0395 
0396     // Separate the paths
0397     m_cursorSearchPaths = path.split(QLatin1Char(':'), Qt::SkipEmptyParts);
0398 
0399     // Remove duplicates
0400     QMutableStringListIterator i(m_cursorSearchPaths);
0401     while (i.hasNext()) {
0402         const QString path = i.next();
0403         QMutableStringListIterator j(i);
0404         while (j.hasNext())
0405             if (j.next() == path)
0406                 j.remove();
0407     }
0408 
0409     // Expand all occurrences of ~/ to the home dir
0410     m_cursorSearchPaths.replaceInStrings(QRegularExpression(QStringLiteral("^~\\/")), QDir::home().path() + QLatin1Char('/'));
0411 #endif
0412     return m_cursorSearchPaths;
0413 }
0414 
0415 void KCMLookandFeel::cursorsChanged(const QString &themeName)
0416 {
0417 #ifdef HAVE_XCURSOR
0418     // Require the Xcursor version that shipped with X11R6.9 or greater, since
0419     // in previous versions the Xfixes code wasn't enabled due to a bug in the
0420     // build system (freedesktop bug #975).
0421 #if defined(HAVE_XFIXES) && XFIXES_MAJOR >= 2 && XCURSOR_LIB_VERSION >= 10105
0422     KSharedConfigPtr config = KSharedConfig::openConfig(QStringLiteral("kcminputrc"));
0423     KConfigGroup cg(config, QStringLiteral("Mouse"));
0424     const int cursorSize = cg.readEntry("cursorSize", 24);
0425 
0426     QDir themeDir = cursorThemeDir(themeName, 0);
0427     if (!themeDir.exists()) {
0428         return;
0429     }
0430 
0431     XCursorTheme theme(themeDir);
0432 
0433     if (!CursorTheme::haveXfixes()) {
0434         return;
0435     }
0436 
0437     // Update the Xcursor X resources
0438     runRdb(0);
0439 
0440     // Notify all applications that the cursor theme has changed
0441     notifyKcmChange(GlobalChangeType::CursorChanged);
0442 
0443     // Reload the standard cursors
0444     QStringList names;
0445 
0446     // Qt cursors
0447     names << QStringLiteral("left_ptr") << QStringLiteral("up_arrow") << QStringLiteral("cross") << QStringLiteral("wait") << QStringLiteral("left_ptr_watch")
0448           << QStringLiteral("ibeam") << QStringLiteral("size_ver") << QStringLiteral("size_hor") << QStringLiteral("size_bdiag") << QStringLiteral("size_fdiag")
0449           << QStringLiteral("size_all") << QStringLiteral("split_v") << QStringLiteral("split_h") << QStringLiteral("pointing_hand")
0450           << QStringLiteral("openhand") << QStringLiteral("closedhand") << QStringLiteral("forbidden") << QStringLiteral("whats_this") << QStringLiteral("copy")
0451           << QStringLiteral("move") << QStringLiteral("link");
0452 
0453     // X core cursors
0454     names << QStringLiteral("X_cursor") << QStringLiteral("right_ptr") << QStringLiteral("hand1") << QStringLiteral("hand2") << QStringLiteral("watch")
0455           << QStringLiteral("xterm") << QStringLiteral("crosshair") << QStringLiteral("left_ptr_watch") << QStringLiteral("center_ptr")
0456           << QStringLiteral("sb_h_double_arrow") << QStringLiteral("sb_v_double_arrow") << QStringLiteral("fleur") << QStringLiteral("top_left_corner")
0457           << QStringLiteral("top_side") << QStringLiteral("top_right_corner") << QStringLiteral("right_side") << QStringLiteral("bottom_right_corner")
0458           << QStringLiteral("bottom_side") << QStringLiteral("bottom_left_corner") << QStringLiteral("left_side") << QStringLiteral("question_arrow")
0459           << QStringLiteral("pirate");
0460 
0461     foreach (const QString &name, names) {
0462         XFixesChangeCursorByName(QX11Info::display(), theme.loadCursor(name, cursorSize), QFile::encodeName(name));
0463     }
0464 
0465 #else
0466     KMessageBox::information(this,
0467                              i18n("You have to restart the Plasma session for these changes to take effect."),
0468                              i18n("Cursor Settings Changed"),
0469                              "CursorSettingsChanged");
0470 #endif
0471 #endif
0472 }