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