Warning, file /plasma/plasma-workspace/kcms/cursortheme/kcmcursortheme.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: 2003-2007 Fredrik Höglund <fredrik@kde.org> 0003 SPDX-FileCopyrightText: 2019 Benjamin Port <benjamin.port@enioka.com> 0004 0005 SPDX-License-Identifier: GPL-2.0-or-later 0006 */ 0007 0008 #include <config-X11.h> 0009 0010 #include "cursorthemedata.h" 0011 #include "kcmcursortheme.h" 0012 0013 #include "../kcms-common_p.h" 0014 #include "krdb.h" 0015 0016 #include "xcursor/cursortheme.h" 0017 #include "xcursor/previewwidget.h" 0018 #include "xcursor/sortproxymodel.h" 0019 #include "xcursor/themeapplicator.h" 0020 #include "xcursor/thememodel.h" 0021 0022 #include <KIO/CopyJob> 0023 #include <KIO/DeleteJob> 0024 #include <KIO/Job> 0025 #include <KIO/JobUiDelegate> 0026 #include <KLocalizedString> 0027 #include <KMessageBox> 0028 #include <KPluginFactory> 0029 #include <KTar> 0030 #include <KUrlRequesterDialog> 0031 0032 #include <QStandardItemModel> 0033 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) 0034 #include <private/qtx11extras_p.h> 0035 #else 0036 #include <QX11Info> 0037 #endif 0038 #include <X11/Xcursor/Xcursor.h> 0039 #include <X11/Xlib.h> 0040 0041 #include <updatelaunchenvjob.h> 0042 0043 #ifdef HAVE_XFIXES 0044 #include <X11/extensions/Xfixes.h> 0045 #endif 0046 0047 K_PLUGIN_FACTORY_WITH_JSON(CursorThemeConfigFactory, "kcm_cursortheme.json", registerPlugin<CursorThemeConfig>(); registerPlugin<CursorThemeData>();) 0048 0049 CursorThemeConfig::CursorThemeConfig(QObject *parent, const KPluginMetaData &data, const QVariantList &args) 0050 : KQuickAddons::ManagedConfigModule(parent, data, args) 0051 , m_data(new CursorThemeData(this)) 0052 , m_canInstall(true) 0053 , m_canResize(true) 0054 , m_canConfigure(true) 0055 { 0056 m_preferredSize = cursorThemeSettings()->cursorSize(); 0057 connect(cursorThemeSettings(), &CursorThemeSettings::cursorThemeChanged, this, &CursorThemeConfig::updateSizeComboBox); 0058 qmlRegisterType<PreviewWidget>("org.kde.private.kcm_cursortheme", 1, 0, "PreviewWidget"); 0059 qmlRegisterAnonymousType<SortProxyModel>("SortProxyModel", 1); 0060 qmlRegisterAnonymousType<CursorThemeSettings>("CursorThemeSettings", 1); 0061 qmlRegisterAnonymousType<LaunchFeedbackSettings>("LaunchFeedbackSettings", 1); 0062 0063 m_themeModel = new CursorThemeModel(this); 0064 0065 m_themeProxyModel = new SortProxyModel(this); 0066 m_themeProxyModel->setSourceModel(m_themeModel); 0067 // sort ordering is already case-insensitive; match that for filtering too 0068 m_themeProxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); 0069 m_themeProxyModel->sort(0, Qt::AscendingOrder); 0070 0071 m_sizesModel = new QStandardItemModel(this); 0072 0073 // Disable the install button if we can't install new themes to ~/.icons, 0074 // or Xcursor isn't set up to look for cursor themes there. 0075 if (!m_themeModel->searchPaths().contains(QDir::homePath() + "/.icons") || !iconsIsWritable()) { 0076 setCanInstall(false); 0077 } 0078 0079 connect(m_themeModel, &QAbstractItemModel::dataChanged, this, &CursorThemeConfig::settingsChanged); 0080 connect(m_themeModel, &QAbstractItemModel::dataChanged, this, [this](const QModelIndex &start, const QModelIndex &end, const QVector<int> &roles) { 0081 const QModelIndex currentThemeIndex = m_themeModel->findIndex(cursorThemeSettings()->cursorTheme()); 0082 if (roles.contains(CursorTheme::PendingDeletionRole) && currentThemeIndex.data(CursorTheme::PendingDeletionRole) == true 0083 && start.row() <= currentThemeIndex.row() && currentThemeIndex.row() <= end.row()) { 0084 cursorThemeSettings()->setCursorTheme(m_themeModel->theme(m_themeModel->defaultIndex())->name()); 0085 } 0086 }); 0087 } 0088 0089 CursorThemeConfig::~CursorThemeConfig() 0090 { 0091 } 0092 0093 CursorThemeSettings *CursorThemeConfig::cursorThemeSettings() const 0094 { 0095 return m_data->cursorThemeSettings(); 0096 } 0097 0098 LaunchFeedbackSettings *CursorThemeConfig::launchFeedbackSettings() const 0099 { 0100 return m_data->launchFeedbackSettings(); 0101 } 0102 0103 void CursorThemeConfig::setCanInstall(bool can) 0104 { 0105 if (m_canInstall == can) { 0106 return; 0107 } 0108 0109 m_canInstall = can; 0110 Q_EMIT canInstallChanged(); 0111 } 0112 0113 bool CursorThemeConfig::canInstall() const 0114 { 0115 return m_canInstall; 0116 } 0117 0118 void CursorThemeConfig::setCanResize(bool can) 0119 { 0120 if (m_canResize == can) { 0121 return; 0122 } 0123 0124 m_canResize = can; 0125 Q_EMIT canResizeChanged(); 0126 } 0127 0128 bool CursorThemeConfig::canResize() const 0129 { 0130 return m_canResize; 0131 } 0132 0133 void CursorThemeConfig::setCanConfigure(bool can) 0134 { 0135 if (m_canConfigure == can) { 0136 return; 0137 } 0138 0139 m_canConfigure = can; 0140 Q_EMIT canConfigureChanged(); 0141 } 0142 0143 int CursorThemeConfig::preferredSize() const 0144 { 0145 return m_preferredSize; 0146 } 0147 0148 void CursorThemeConfig::setPreferredSize(int size) 0149 { 0150 if (m_preferredSize == size) { 0151 return; 0152 } 0153 m_preferredSize = size; 0154 Q_EMIT preferredSizeChanged(); 0155 } 0156 0157 bool CursorThemeConfig::canConfigure() const 0158 { 0159 return m_canConfigure; 0160 } 0161 0162 bool CursorThemeConfig::downloadingFile() const 0163 { 0164 return m_tempCopyJob; 0165 } 0166 0167 QAbstractItemModel *CursorThemeConfig::cursorsModel() 0168 { 0169 return m_themeProxyModel; 0170 } 0171 0172 QAbstractItemModel *CursorThemeConfig::sizesModel() 0173 { 0174 return m_sizesModel; 0175 } 0176 0177 bool CursorThemeConfig::iconsIsWritable() const 0178 { 0179 const QFileInfo icons = QFileInfo(QDir::homePath() + "/.icons"); 0180 const QFileInfo home = QFileInfo(QDir::homePath()); 0181 0182 return ((icons.exists() && icons.isDir() && icons.isWritable()) || (!icons.exists() && home.isWritable())); 0183 } 0184 0185 void CursorThemeConfig::updateSizeComboBox() 0186 { 0187 // clear the combo box 0188 m_sizesModel->clear(); 0189 0190 // refill the combo box and adopt its icon size 0191 int row = cursorThemeIndex(cursorThemeSettings()->cursorTheme()); 0192 QModelIndex selected = m_themeProxyModel->index(row, 0); 0193 int maxIconWidth = 0; 0194 int maxIconHeight = 0; 0195 if (selected.isValid()) { 0196 const CursorTheme *theme = m_themeProxyModel->theme(selected); 0197 const QList<int> sizes = theme->availableSizes(); 0198 // only refill the combobox if there is more that 1 size 0199 if (sizes.size() > 1) { 0200 int i; 0201 QList<int> comboBoxList; 0202 QPixmap m_pixmap; 0203 0204 // insert the items 0205 m_pixmap = theme->createIcon(0); 0206 if (m_pixmap.width() > maxIconWidth) { 0207 maxIconWidth = m_pixmap.width(); 0208 } 0209 if (m_pixmap.height() > maxIconHeight) { 0210 maxIconHeight = m_pixmap.height(); 0211 } 0212 0213 foreach (i, sizes) { 0214 m_pixmap = theme->createIcon(i); 0215 if (m_pixmap.width() > maxIconWidth) { 0216 maxIconWidth = m_pixmap.width(); 0217 } 0218 if (m_pixmap.height() > maxIconHeight) { 0219 maxIconHeight = m_pixmap.height(); 0220 } 0221 QStandardItem *item = new QStandardItem(QIcon(m_pixmap), QString::number(i)); 0222 item->setData(i); 0223 m_sizesModel->appendRow(item); 0224 comboBoxList << i; 0225 } 0226 0227 // select an item 0228 int size = m_preferredSize; 0229 int selectItem = comboBoxList.indexOf(size); 0230 0231 // cursor size not available for this theme 0232 if (selectItem < 0) { 0233 /* Search the value next to cursor size. The first entry (0) 0234 is ignored. (If cursor size would have been 0, then we 0235 would had found it yet. As cursor size is not 0, we won't 0236 default to "automatic size".)*/ 0237 int j; 0238 int distance; 0239 int smallestDistance; 0240 selectItem = 1; 0241 j = comboBoxList.value(selectItem); 0242 size = j; 0243 smallestDistance = qAbs(m_preferredSize - j); 0244 for (int i = 2; i < comboBoxList.size(); ++i) { 0245 j = comboBoxList.value(i); 0246 distance = qAbs(m_preferredSize - j); 0247 if (distance < smallestDistance || (distance == smallestDistance && j > m_preferredSize)) { 0248 smallestDistance = distance; 0249 selectItem = i; 0250 size = j; 0251 } 0252 } 0253 } 0254 cursorThemeSettings()->setCursorSize(size); 0255 } 0256 } 0257 0258 // enable or disable the combobox 0259 if (cursorThemeSettings()->isImmutable("cursorSize")) { 0260 setCanResize(false); 0261 } else { 0262 setCanResize(m_sizesModel->rowCount() > 0); 0263 } 0264 // We need to Q_EMIT a cursorSizeChanged in all case to refresh UI 0265 Q_EMIT cursorThemeSettings()->cursorSizeChanged(); 0266 } 0267 0268 int CursorThemeConfig::cursorSizeIndex(int cursorSize) const 0269 { 0270 if (m_sizesModel->rowCount() > 0) { 0271 const auto items = m_sizesModel->findItems(QString::number(cursorSize)); 0272 if (items.count() == 1) { 0273 return items.first()->row(); 0274 } 0275 } 0276 return -1; 0277 } 0278 0279 int CursorThemeConfig::cursorSizeFromIndex(int index) 0280 { 0281 Q_ASSERT(index < m_sizesModel->rowCount() && index >= 0); 0282 0283 return m_sizesModel->item(index)->data().toInt(); 0284 } 0285 0286 int CursorThemeConfig::cursorThemeIndex(const QString &cursorTheme) const 0287 { 0288 auto results = m_themeProxyModel->findIndex(cursorTheme); 0289 return results.row(); 0290 } 0291 0292 QString CursorThemeConfig::cursorThemeFromIndex(int index) const 0293 { 0294 QModelIndex idx = m_themeProxyModel->index(index, 0); 0295 return idx.isValid() ? m_themeProxyModel->theme(idx)->name() : QString(); 0296 } 0297 0298 void CursorThemeConfig::save() 0299 { 0300 ManagedConfigModule::save(); 0301 setPreferredSize(cursorThemeSettings()->cursorSize()); 0302 0303 int row = cursorThemeIndex(cursorThemeSettings()->cursorTheme()); 0304 QModelIndex selected = m_themeProxyModel->index(row, 0); 0305 const CursorTheme *theme = selected.isValid() ? m_themeProxyModel->theme(selected) : nullptr; 0306 0307 if (!applyTheme(theme, cursorThemeSettings()->cursorSize())) { 0308 Q_EMIT showInfoMessage(i18n("You have to restart the Plasma session for these changes to take effect.")); 0309 } 0310 removeThemes(); 0311 0312 notifyKcmChange(GlobalChangeType::CursorChanged); 0313 } 0314 0315 void CursorThemeConfig::load() 0316 { 0317 ManagedConfigModule::load(); 0318 setPreferredSize(cursorThemeSettings()->cursorSize()); 0319 0320 // Disable the listview and the buttons if we're in kiosk mode 0321 if (cursorThemeSettings()->isImmutable(QStringLiteral("cursorTheme"))) { 0322 setCanConfigure(false); 0323 setCanInstall(false); 0324 } 0325 0326 updateSizeComboBox(); // This handles also the kiosk mode 0327 0328 setNeedsSave(false); 0329 } 0330 0331 void CursorThemeConfig::defaults() 0332 { 0333 ManagedConfigModule::defaults(); 0334 m_preferredSize = cursorThemeSettings()->cursorSize(); 0335 } 0336 0337 bool CursorThemeConfig::isSaveNeeded() const 0338 { 0339 return !m_themeModel->match(m_themeModel->index(0, 0), CursorTheme::PendingDeletionRole, true).isEmpty(); 0340 } 0341 0342 void CursorThemeConfig::ghnsEntryChanged(KNSCore::EntryWrapper *entry) 0343 { 0344 if (entry->entry().status() == KNS3::Entry::Deleted) { 0345 for (const QString &deleted : entry->entry().uninstalledFiles()) { 0346 auto list = QStringView(deleted).split(QLatin1Char('/')); 0347 if (list.last() == QLatin1Char('*')) { 0348 list.takeLast(); 0349 } 0350 QModelIndex idx = m_themeModel->findIndex(list.last().toString()); 0351 if (idx.isValid()) { 0352 m_themeModel->removeTheme(idx); 0353 } 0354 } 0355 } else if (entry->entry().status() == KNS3::Entry::Installed) { 0356 const QList<QString> installedFiles = entry->entry().installedFiles(); 0357 if (installedFiles.size() != 1) { 0358 return; 0359 } 0360 const QString installedDir = installedFiles.first(); 0361 if (!installedDir.endsWith(QLatin1Char('*'))) { 0362 return; 0363 } 0364 0365 m_themeModel->addTheme(installedDir.left(installedDir.size() - 1)); 0366 } 0367 } 0368 0369 void CursorThemeConfig::installThemeFromFile(const QUrl &url) 0370 { 0371 if (url.isLocalFile()) { 0372 installThemeFile(url.toLocalFile()); 0373 return; 0374 } 0375 0376 if (m_tempCopyJob) { 0377 return; 0378 } 0379 0380 m_tempInstallFile.reset(new QTemporaryFile()); 0381 if (!m_tempInstallFile->open()) { 0382 Q_EMIT showErrorMessage(i18n("Unable to create a temporary file.")); 0383 m_tempInstallFile.reset(); 0384 return; 0385 } 0386 0387 m_tempCopyJob = KIO::file_copy(url, QUrl::fromLocalFile(m_tempInstallFile->fileName()), -1, KIO::Overwrite); 0388 m_tempCopyJob->uiDelegate()->setAutoErrorHandlingEnabled(true); 0389 Q_EMIT downloadingFileChanged(); 0390 0391 connect(m_tempCopyJob, &KIO::FileCopyJob::result, this, [this, url](KJob *job) { 0392 if (job->error() != KJob::NoError) { 0393 Q_EMIT showErrorMessage(i18n("Unable to download the icon theme archive: %1", job->errorText())); 0394 return; 0395 } 0396 0397 installThemeFile(m_tempInstallFile->fileName()); 0398 m_tempInstallFile.reset(); 0399 }); 0400 connect(m_tempCopyJob, &QObject::destroyed, this, &CursorThemeConfig::downloadingFileChanged); 0401 } 0402 0403 void CursorThemeConfig::installThemeFile(const QString &path) 0404 { 0405 KTar archive(path); 0406 archive.open(QIODevice::ReadOnly); 0407 0408 const KArchiveDirectory *archiveDir = archive.directory(); 0409 QStringList themeDirs; 0410 0411 // Extract the dir names of the cursor themes in the archive, and 0412 // append them to themeDirs 0413 foreach (const QString &name, archiveDir->entries()) { 0414 const KArchiveEntry *entry = archiveDir->entry(name); 0415 if (entry->isDirectory() && entry->name().toLower() != "default") { 0416 const KArchiveDirectory *dir = static_cast<const KArchiveDirectory *>(entry); 0417 if (dir->entry("index.theme") && dir->entry("cursors")) { 0418 themeDirs << dir->name(); 0419 } 0420 } 0421 } 0422 0423 if (themeDirs.isEmpty()) { 0424 Q_EMIT showErrorMessage(i18n("The file is not a valid icon theme archive.")); 0425 return; 0426 } 0427 0428 // The directory we'll install the themes to 0429 QString destDir = QDir::homePath() + "/.icons/"; 0430 if (!QDir().mkpath(destDir)) { 0431 Q_EMIT showErrorMessage(i18n("Failed to create 'icons' folder.")); 0432 return; 0433 } 0434 0435 // Process each cursor theme in the archive 0436 foreach (const QString &dirName, themeDirs) { 0437 QDir dest(destDir + dirName); 0438 if (dest.exists()) { 0439 QString question = i18n( 0440 "A theme named %1 already exists in your icon " 0441 "theme folder. Do you want replace it with this one?", 0442 dirName); 0443 0444 int answer = KMessageBox::warningContinueCancel(nullptr, question, i18n("Overwrite Theme?"), KStandardGuiItem::overwrite()); 0445 0446 if (answer != KMessageBox::Continue) { 0447 continue; 0448 } 0449 0450 // ### If the theme that's being replaced is the current theme, it 0451 // will cause cursor inconsistencies in newly started apps. 0452 } 0453 0454 // ### Should we check if a theme with the same name exists in a global theme dir? 0455 // If that's the case it will effectively replace it, even though the global theme 0456 // won't be deleted. Checking for this situation is easy, since the global theme 0457 // will be in the listview. Maybe this should never be allowed since it might 0458 // result in strange side effects (from the average users point of view). OTOH 0459 // a user might want to do this 'upgrade' a global theme. 0460 0461 const KArchiveDirectory *dir = static_cast<const KArchiveDirectory *>(archiveDir->entry(dirName)); 0462 dir->copyTo(dest.path()); 0463 m_themeModel->addTheme(dest); 0464 } 0465 0466 archive.close(); 0467 0468 Q_EMIT showSuccessMessage(i18n("Theme installed successfully.")); 0469 0470 m_themeModel->refreshList(); 0471 } 0472 0473 void CursorThemeConfig::removeThemes() 0474 { 0475 const QModelIndexList indices = m_themeModel->match(m_themeModel->index(0, 0), CursorTheme::PendingDeletionRole, true, -1); 0476 QList<QPersistentModelIndex> persistentIndices; 0477 persistentIndices.reserve(indices.count()); 0478 std::transform(indices.constBegin(), indices.constEnd(), std::back_inserter(persistentIndices), [](const QModelIndex index) { 0479 return QPersistentModelIndex(index); 0480 }); 0481 for (const auto &idx : qAsConst(persistentIndices)) { 0482 const CursorTheme *theme = m_themeModel->theme(idx); 0483 0484 // Delete the theme from the harddrive 0485 KIO::del(QUrl::fromLocalFile(theme->path())); // async 0486 0487 // Remove the theme from the model 0488 m_themeModel->removeTheme(idx); 0489 } 0490 0491 // TODO: 0492 // Since it's possible to substitute cursors in a system theme by adding a local 0493 // theme with the same name, we shouldn't remove the theme from the list if it's 0494 // still available elsewhere. We could add a 0495 // bool CursorThemeModel::tryAddTheme(const QString &name), and call that, but 0496 // since KIO::del() is an asynchronos operation, the theme we're deleting will be 0497 // readded to the list again before KIO has removed it. 0498 } 0499 0500 #include "kcmcursortheme.moc"