/* SPDX-FileCopyrightText: 2014 Marco Martin SPDX-FileCopyrightText: 2014 Vishesh Handa SPDX-FileCopyrightText: 2019 Cyril Rossi SPDX-FileCopyrightText: 2021 Benjamin Port SPDX-License-Identifier: LGPL-2.0-only */ #include "kcm.h" #include "../kcms-common_p.h" #include "config-kcm.h" #include "config-workspace.h" #include "krdb.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "lookandfeelmanager.h" #include "lookandfeelsettings.h" #ifdef HAVE_XCURSOR #include "../cursortheme/xcursor/xcursortheme.h" #include #endif #ifdef HAVE_XFIXES #include #endif KCMLookandFeel::KCMLookandFeel(QObject *parent, const KPluginMetaData &data, const QVariantList &args) : KQuickAddons::ManagedConfigModule(parent, data, args) , m_lnf(new LookAndFeelManager(this)) { qmlRegisterAnonymousType("", 1); qmlRegisterAnonymousType("", 1); qmlRegisterAnonymousType("", 1); setButtons(Apply | Default); m_model = new QStandardItemModel(this); QHash roles = m_model->roleNames(); roles[PluginNameRole] = "pluginName"; roles[DescriptionRole] = "description"; roles[ScreenshotRole] = "screenshot"; roles[FullScreenPreviewRole] = "fullScreenPreview"; roles[HasSplashRole] = "hasSplash"; roles[HasLockScreenRole] = "hasLockScreen"; roles[HasRunCommandRole] = "hasRunCommand"; roles[HasLogoutRole] = "hasLogout"; roles[HasColorsRole] = "hasColors"; roles[HasWidgetStyleRole] = "hasWidgetStyle"; roles[HasIconsRole] = "hasIcons"; roles[HasPlasmaThemeRole] = "hasPlasmaTheme"; roles[HasCursorsRole] = "hasCursors"; roles[HasWindowSwitcherRole] = "hasWindowSwitcher"; roles[HasDesktopSwitcherRole] = "hasDesktopSwitcher"; m_model->setItemRoleNames(roles); loadModel(); connect(m_lnf, &LookAndFeelManager::resetDefaultLayoutChanged, this, [this] { Q_EMIT resetDefaultLayoutChanged(); settingsChanged(); }); connect(m_lnf, &LookAndFeelManager::refreshServices, this, [](const QStringList &toStop, const QList &toStart) { for (const auto &serviceName : toStop) { // FIXME: quite ugly way to stop things, and what about non KDE things? QProcess::startDetached(QStringLiteral("kquitapp5"), {QStringLiteral("--service"), serviceName}); } for (const auto &service : toStart) { auto *job = new KIO::ApplicationLauncherJob(service); job->setUiDelegate(new KDialogJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, nullptr)); job->start(); } }); connect(m_lnf, &LookAndFeelManager::styleChanged, this, [] { // FIXME: changing style on the fly breaks QQuickWidgets notifyKcmChange(GlobalChangeType::StyleChanged); }); connect(m_lnf, &LookAndFeelManager::colorsChanged, this, [] { // FIXME: changing style on the fly breaks QQuickWidgets notifyKcmChange(GlobalChangeType::PaletteChanged); }); connect(m_lnf, &LookAndFeelManager::iconsChanged, this, [] { for (int i = 0; i < KIconLoader::LastGroup; i++) { KIconLoader::emitChange(KIconLoader::Group(i)); } }); connect(m_lnf, &LookAndFeelManager::cursorsChanged, this, &KCMLookandFeel::cursorsChanged); connect(m_lnf, &LookAndFeelManager::fontsChanged, this, [] { QDBusMessage message = QDBusMessage::createSignal("/KDEPlatformTheme", "org.kde.KDEPlatformTheme", "refreshFonts"); QDBusConnection::sessionBus().send(message); }); } KCMLookandFeel::~KCMLookandFeel() { } void KCMLookandFeel::knsEntryChanged(KNSCore::EntryWrapper *wrapper) { if (!wrapper) { return; } const KNSCore::EntryInternal entry = wrapper->entry(); auto removeItemFromModel = [&entry, this]() { if (entry.uninstalledFiles().isEmpty()) { return; } const QString guessedPluginId = QFileInfo(entry.uninstalledFiles().constFirst()).fileName(); const int index = pluginIndex(guessedPluginId); if (index != -1) { m_model->removeRows(index, 1); } }; if (entry.status() == KNS3::Entry::Deleted) { removeItemFromModel(); } else if (entry.status() == KNS3::Entry::Installed && !entry.installedFiles().isEmpty()) { if (!entry.uninstalledFiles().isEmpty()) { removeItemFromModel(); // In case we updated it we don't want to have it in twice } KPackage::Package pkg = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/LookAndFeel")); pkg.setPath(entry.installedFiles().constFirst()); addKPackageToModel(pkg); } } QStandardItemModel *KCMLookandFeel::lookAndFeelModel() const { return m_model; } int KCMLookandFeel::pluginIndex(const QString &pluginName) const { const auto results = m_model->match(m_model->index(0, 0), PluginNameRole, pluginName, 1, Qt::MatchExactly); if (results.count() == 1) { return results.first().row(); } return -1; } QList KCMLookandFeel::availablePackages(const QStringList &components) { QList packages; QStringList paths; const QStringList dataPaths = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation); paths.reserve(dataPaths.count()); for (const QString &path : dataPaths) { QDir dir(path + QStringLiteral("/plasma/look-and-feel")); paths << dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot); } for (const QString &path : paths) { KPackage::Package pkg = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/LookAndFeel")); pkg.setPath(path); pkg.setFallbackPackage(KPackage::Package()); if (components.isEmpty()) { packages << pkg; } else { for (const auto &component : components) { if (!pkg.filePath(component.toUtf8()).isEmpty()) { packages << pkg; break; } } } } return packages; } LookAndFeelSettings *KCMLookandFeel::lookAndFeelSettings() const { return m_lnf->settings(); } void KCMLookandFeel::loadModel() { m_model->clear(); QList pkgs = availablePackages({"defaults", "layouts"}); // Sort case-insensitively QCollator collator; collator.setCaseSensitivity(Qt::CaseInsensitive); std::sort(pkgs.begin(), pkgs.end(), [&collator](const KPackage::Package &a, const KPackage::Package &b) { return collator.compare(a.metadata().name(), b.metadata().name()) < 0; }); for (const KPackage::Package &pkg : pkgs) { addKPackageToModel(pkg); } // Model has been cleared so pretend the selected look and fell changed to force view update Q_EMIT lookAndFeelSettings()->lookAndFeelPackageChanged(); } void KCMLookandFeel::addKPackageToModel(const KPackage::Package &pkg) { if (!pkg.metadata().isValid()) { return; } QStandardItem *row = new QStandardItem(pkg.metadata().name()); row->setData(pkg.metadata().pluginId(), PluginNameRole); row->setData(pkg.metadata().description(), DescriptionRole); row->setData(pkg.filePath("preview"), ScreenshotRole); row->setData(pkg.filePath("fullscreenpreview"), FullScreenPreviewRole); // What the package provides row->setData(!pkg.filePath("splashmainscript").isEmpty(), HasSplashRole); row->setData(!pkg.filePath("lockscreenmainscript").isEmpty(), HasLockScreenRole); row->setData(!pkg.filePath("runcommandmainscript").isEmpty(), HasRunCommandRole); row->setData(!pkg.filePath("logoutmainscript").isEmpty(), HasLogoutRole); if (!pkg.filePath("defaults").isEmpty()) { KSharedConfigPtr conf = KSharedConfig::openConfig(pkg.filePath("defaults")); KConfigGroup cg(conf, "kdeglobals"); cg = KConfigGroup(&cg, "General"); bool hasColors = !cg.readEntry("ColorScheme", QString()).isEmpty(); if (!hasColors) { hasColors = !pkg.filePath("colors").isEmpty(); } row->setData(hasColors, HasColorsRole); cg = KConfigGroup(&cg, "KDE"); row->setData(!cg.readEntry("widgetStyle", QString()).isEmpty(), HasWidgetStyleRole); cg = KConfigGroup(conf, "kdeglobals"); cg = KConfigGroup(&cg, "Icons"); row->setData(!cg.readEntry("Theme", QString()).isEmpty(), HasIconsRole); cg = KConfigGroup(conf, "kdeglobals"); cg = KConfigGroup(&cg, "Theme"); row->setData(!cg.readEntry("name", QString()).isEmpty(), HasPlasmaThemeRole); cg = KConfigGroup(conf, "kcminputrc"); cg = KConfigGroup(&cg, "Mouse"); row->setData(!cg.readEntry("cursorTheme", QString()).isEmpty(), HasCursorsRole); cg = KConfigGroup(conf, "kwinrc"); cg = KConfigGroup(&cg, "WindowSwitcher"); row->setData(!cg.readEntry("LayoutName", QString()).isEmpty(), HasWindowSwitcherRole); cg = KConfigGroup(conf, "kwinrc"); cg = KConfigGroup(&cg, "DesktopSwitcher"); row->setData(!cg.readEntry("LayoutName", QString()).isEmpty(), HasDesktopSwitcherRole); } m_model->appendRow(row); } bool KCMLookandFeel::isSaveNeeded() const { return m_lnf->resetDefaultLayout() || lookAndFeelSettings()->isSaveNeeded(); } void KCMLookandFeel::load() { ManagedConfigModule::load(); m_package = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/LookAndFeel"), lookAndFeelSettings()->lookAndFeelPackage()); m_lnf->setResetDefaultLayout(false); } void KCMLookandFeel::save() { QString newLnfPackage = lookAndFeelSettings()->lookAndFeelPackage(); KPackage::Package package = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/LookAndFeel")); package.setPath(newLnfPackage); if (!package.isValid()) { return; } { // Some global themes use styles that may not be installed. // Test if style can be installed before updating the config. KSharedConfigPtr conf = KSharedConfig::openConfig(package.filePath("defaults")); KConfigGroup cg(conf, "kdeglobals"); QScopedPointer newStyle(QStyleFactory::create(cg.readEntry("widgetStyle", QString()))); m_lnf->setApplyWidgetStyle(newStyle); } ManagedConfigModule::save(); m_lnf->save(package, m_package); m_package.setPath(newLnfPackage); runRdb(KRdbExportQtColors | KRdbExportGtkTheme | KRdbExportColors | KRdbExportQtSettings | KRdbExportXftSettings); setResetDefaultLayout(false); } void KCMLookandFeel::defaults() { m_lnf->setResetDefaultLayout(false); ManagedConfigModule::defaults(); } void KCMLookandFeel::setResetDefaultLayout(bool reset) { m_lnf->setResetDefaultLayout(reset); } bool KCMLookandFeel::resetDefaultLayout() const { return m_lnf->resetDefaultLayout(); } QDir KCMLookandFeel::cursorThemeDir(const QString &theme, const int depth) { // Prevent infinite recursion if (depth > 10) { return QDir(); } // Search each icon theme directory for 'theme' foreach (const QString &baseDir, cursorSearchPaths()) { QDir dir(baseDir); if (!dir.exists() || !dir.cd(theme)) { continue; } // If there's a cursors subdir, we'll assume this is a cursor theme if (dir.exists(QStringLiteral("cursors"))) { return dir; } // If the theme doesn't have an index.theme file, it can't inherit any themes. if (!dir.exists(QStringLiteral("index.theme"))) { continue; } // Open the index.theme file, so we can get the list of inherited themes KConfig config(dir.path() + QStringLiteral("/index.theme"), KConfig::NoGlobals); KConfigGroup cg(&config, "Icon Theme"); // Recurse through the list of inherited themes, to check if one of them // is a cursor theme. const QStringList inherits = cg.readEntry("Inherits", QStringList()); for (const QString &inherit : inherits) { // Avoid possible DoS if (inherit == theme) { continue; } if (cursorThemeDir(inherit, depth + 1).exists()) { return dir; } } } return QDir(); } QStringList KCMLookandFeel::cursorSearchPaths() { #ifdef HAVE_XCURSOR #if XCURSOR_LIB_MAJOR == 1 && XCURSOR_LIB_MINOR < 1 if (!m_cursorSearchPaths.isEmpty()) return m_cursorSearchPaths; // These are the default paths Xcursor will scan for cursor themes QString path("~/.icons:/usr/share/icons:/usr/share/pixmaps:/usr/X11R6/lib/X11/icons"); // If XCURSOR_PATH is set, use that instead of the default path char *xcursorPath = std::getenv("XCURSOR_PATH"); if (xcursorPath) path = xcursorPath; #else // Get the search path from Xcursor QString path = XcursorLibraryPath(); #endif // Separate the paths m_cursorSearchPaths = path.split(QLatin1Char(':'), Qt::SkipEmptyParts); // Remove duplicates QMutableStringListIterator i(m_cursorSearchPaths); while (i.hasNext()) { const QString path = i.next(); QMutableStringListIterator j(i); while (j.hasNext()) if (j.next() == path) j.remove(); } // Expand all occurrences of ~/ to the home dir m_cursorSearchPaths.replaceInStrings(QRegExp(QStringLiteral("^~\\/")), QDir::home().path() + QLatin1Char('/')); #endif return m_cursorSearchPaths; } void KCMLookandFeel::cursorsChanged(const QString &themeName) { #ifdef HAVE_XCURSOR // Require the Xcursor version that shipped with X11R6.9 or greater, since // in previous versions the Xfixes code wasn't enabled due to a bug in the // build system (freedesktop bug #975). #if defined(HAVE_XFIXES) && XFIXES_MAJOR >= 2 && XCURSOR_LIB_VERSION >= 10105 KSharedConfigPtr config = KSharedConfig::openConfig(QStringLiteral("kcminputrc")); KConfigGroup cg(config, QStringLiteral("Mouse")); const int cursorSize = cg.readEntry("cursorSize", 24); QDir themeDir = cursorThemeDir(themeName, 0); if (!themeDir.exists()) { return; } XCursorTheme theme(themeDir); if (!CursorTheme::haveXfixes()) { return; } UpdateLaunchEnvJob launchEnvJob(QStringLiteral("XCURSOR_THEME"), themeName); // Update the Xcursor X resources runRdb(0); // Notify all applications that the cursor theme has changed notifyKcmChange(GlobalChangeType::CursorChanged); // Reload the standard cursors QStringList names; // Qt cursors names << QStringLiteral("left_ptr") << QStringLiteral("up_arrow") << QStringLiteral("cross") << QStringLiteral("wait") << QStringLiteral("left_ptr_watch") << QStringLiteral("ibeam") << QStringLiteral("size_ver") << QStringLiteral("size_hor") << QStringLiteral("size_bdiag") << QStringLiteral("size_fdiag") << QStringLiteral("size_all") << QStringLiteral("split_v") << QStringLiteral("split_h") << QStringLiteral("pointing_hand") << QStringLiteral("openhand") << QStringLiteral("closedhand") << QStringLiteral("forbidden") << QStringLiteral("whats_this") << QStringLiteral("copy") << QStringLiteral("move") << QStringLiteral("link"); // X core cursors names << QStringLiteral("X_cursor") << QStringLiteral("right_ptr") << QStringLiteral("hand1") << QStringLiteral("hand2") << QStringLiteral("watch") << QStringLiteral("xterm") << QStringLiteral("crosshair") << QStringLiteral("left_ptr_watch") << QStringLiteral("center_ptr") << QStringLiteral("sb_h_double_arrow") << QStringLiteral("sb_v_double_arrow") << QStringLiteral("fleur") << QStringLiteral("top_left_corner") << QStringLiteral("top_side") << QStringLiteral("top_right_corner") << QStringLiteral("right_side") << QStringLiteral("bottom_right_corner") << QStringLiteral("bottom_side") << QStringLiteral("bottom_left_corner") << QStringLiteral("left_side") << QStringLiteral("question_arrow") << QStringLiteral("pirate"); foreach (const QString &name, names) { XFixesChangeCursorByName(QX11Info::display(), theme.loadCursor(name, cursorSize), QFile::encodeName(name)); } #else KMessageBox::information(this, i18n("You have to restart the Plasma session for these changes to take effect."), i18n("Cursor Settings Changed"), "CursorSettingsChanged"); #endif #endif }