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"