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"