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 }