File indexing completed on 2024-11-10 04:57:24

0001 /*
0002     SPDX-FileCopyrightText: 2020 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "xcursortheme.h"
0008 #include "3rdparty/xcursor.h"
0009 
0010 #include <KConfig>
0011 #include <KConfigGroup>
0012 
0013 #include <QDir>
0014 #include <QFile>
0015 #include <QSet>
0016 #include <QSharedData>
0017 #include <QStack>
0018 #include <QStandardPaths>
0019 
0020 namespace KWin
0021 {
0022 
0023 class KXcursorSpritePrivate : public QSharedData
0024 {
0025 public:
0026     QImage data;
0027     QPoint hotspot;
0028     std::chrono::milliseconds delay;
0029 };
0030 
0031 class KXcursorThemePrivate : public QSharedData
0032 {
0033 public:
0034     void load(const QString &themeName, int size, qreal devicePixelRatio);
0035     void loadCursors(const QString &packagePath, int size, qreal devicePixelRatio);
0036 
0037     QHash<QByteArray, QList<KXcursorSprite>> registry;
0038 };
0039 
0040 KXcursorSprite::KXcursorSprite()
0041     : d(new KXcursorSpritePrivate)
0042 {
0043 }
0044 
0045 KXcursorSprite::KXcursorSprite(const KXcursorSprite &other)
0046     : d(other.d)
0047 {
0048 }
0049 
0050 KXcursorSprite::~KXcursorSprite()
0051 {
0052 }
0053 
0054 KXcursorSprite &KXcursorSprite::operator=(const KXcursorSprite &other)
0055 {
0056     d = other.d;
0057     return *this;
0058 }
0059 
0060 KXcursorSprite::KXcursorSprite(const QImage &data, const QPoint &hotspot,
0061                                const std::chrono::milliseconds &delay)
0062     : d(new KXcursorSpritePrivate)
0063 {
0064     d->data = data;
0065     d->hotspot = hotspot;
0066     d->delay = delay;
0067 }
0068 
0069 QImage KXcursorSprite::data() const
0070 {
0071     return d->data;
0072 }
0073 
0074 QPoint KXcursorSprite::hotspot() const
0075 {
0076     return d->hotspot;
0077 }
0078 
0079 std::chrono::milliseconds KXcursorSprite::delay() const
0080 {
0081     return d->delay;
0082 }
0083 
0084 static QList<KXcursorSprite> loadCursor(const QString &filePath, int desiredSize, qreal devicePixelRatio)
0085 {
0086     XcursorImages *images = XcursorFileLoadImages(QFile::encodeName(filePath), desiredSize * devicePixelRatio);
0087     if (!images) {
0088         return {};
0089     }
0090 
0091     QList<KXcursorSprite> sprites;
0092     for (int i = 0; i < images->nimage; ++i) {
0093         const XcursorImage *nativeCursorImage = images->images[i];
0094         const qreal scale = std::max(qreal(1), qreal(nativeCursorImage->size) / desiredSize);
0095         const QPoint hotspot(nativeCursorImage->xhot, nativeCursorImage->yhot);
0096         const std::chrono::milliseconds delay(nativeCursorImage->delay);
0097 
0098         QImage data(nativeCursorImage->width, nativeCursorImage->height, QImage::Format_ARGB32_Premultiplied);
0099         data.setDevicePixelRatio(scale);
0100         memcpy(data.bits(), nativeCursorImage->pixels, data.sizeInBytes());
0101 
0102         sprites.append(KXcursorSprite(data, hotspot / scale, delay));
0103     }
0104 
0105     XcursorImagesDestroy(images);
0106     return sprites;
0107 }
0108 
0109 void KXcursorThemePrivate::loadCursors(const QString &packagePath, int size, qreal devicePixelRatio)
0110 {
0111     const QDir dir(packagePath);
0112     QFileInfoList entries = dir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot);
0113     std::partition(entries.begin(), entries.end(), [](const QFileInfo &fileInfo) {
0114         return !fileInfo.isSymLink();
0115     });
0116 
0117     for (const QFileInfo &entry : std::as_const(entries)) {
0118         const QByteArray shape = QFile::encodeName(entry.fileName());
0119         if (registry.contains(shape)) {
0120             continue;
0121         }
0122         if (entry.isSymLink()) {
0123             const QFileInfo symLinkInfo(entry.symLinkTarget());
0124             if (symLinkInfo.absolutePath() == entry.absolutePath()) {
0125                 const auto sprites = registry.value(QFile::encodeName(symLinkInfo.fileName()));
0126                 if (!sprites.isEmpty()) {
0127                     registry.insert(shape, sprites);
0128                     continue;
0129                 }
0130             }
0131         }
0132         const QList<KXcursorSprite> sprites = loadCursor(entry.absoluteFilePath(), size, devicePixelRatio);
0133         if (!sprites.isEmpty()) {
0134             registry.insert(shape, sprites);
0135         }
0136     }
0137 }
0138 
0139 static QStringList searchPaths()
0140 {
0141     static QStringList paths;
0142     if (paths.isEmpty()) {
0143         if (const QString env = qEnvironmentVariable("XCURSOR_PATH"); !env.isEmpty()) {
0144             paths.append(env.split(':', Qt::SkipEmptyParts));
0145         } else {
0146             const QString home = QDir::homePath();
0147             if (!home.isEmpty()) {
0148                 paths.append(home + QLatin1String("/.icons"));
0149             }
0150             const QStringList dataDirs = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation);
0151             for (const QString &dataDir : dataDirs) {
0152                 paths.append(dataDir + QLatin1String("/icons"));
0153             }
0154         }
0155     }
0156     return paths;
0157 }
0158 
0159 void KXcursorThemePrivate::load(const QString &themeName, int size, qreal devicePixelRatio)
0160 {
0161     const QStringList paths = searchPaths();
0162 
0163     QStack<QString> stack;
0164     QSet<QString> loaded;
0165 
0166     stack.push(themeName);
0167 
0168     while (!stack.isEmpty()) {
0169         const QString themeName = stack.pop();
0170         if (loaded.contains(themeName)) {
0171             continue;
0172         }
0173 
0174         QStringList inherits;
0175 
0176         for (const QString &path : paths) {
0177             const QDir dir(path + QLatin1Char('/') + themeName);
0178             if (!dir.exists()) {
0179                 continue;
0180             }
0181             loadCursors(dir.filePath(QStringLiteral("cursors")), size, devicePixelRatio);
0182             if (inherits.isEmpty()) {
0183                 const KConfig config(dir.filePath(QStringLiteral("index.theme")), KConfig::NoGlobals);
0184                 inherits << KConfigGroup(&config, QStringLiteral("Icon Theme")).readEntry("Inherits", QStringList());
0185             }
0186         }
0187 
0188         loaded.insert(themeName);
0189         for (auto it = inherits.crbegin(); it != inherits.crend(); ++it) {
0190             stack.push(*it);
0191         }
0192     }
0193 }
0194 
0195 KXcursorTheme::KXcursorTheme()
0196     : d(new KXcursorThemePrivate)
0197 {
0198 }
0199 
0200 KXcursorTheme::KXcursorTheme(const QString &themeName, int size, qreal devicePixelRatio)
0201     : d(new KXcursorThemePrivate)
0202 {
0203     d->load(themeName, size, devicePixelRatio);
0204 }
0205 
0206 KXcursorTheme::KXcursorTheme(const KXcursorTheme &other)
0207     : d(other.d)
0208 {
0209 }
0210 
0211 KXcursorTheme::~KXcursorTheme()
0212 {
0213 }
0214 
0215 KXcursorTheme &KXcursorTheme::operator=(const KXcursorTheme &other)
0216 {
0217     d = other.d;
0218     return *this;
0219 }
0220 
0221 bool KXcursorTheme::operator==(const KXcursorTheme &other)
0222 {
0223     return d == other.d;
0224 }
0225 
0226 bool KXcursorTheme::operator!=(const KXcursorTheme &other)
0227 {
0228     return !(*this == other);
0229 }
0230 
0231 bool KXcursorTheme::isEmpty() const
0232 {
0233     return d->registry.isEmpty();
0234 }
0235 
0236 QList<KXcursorSprite> KXcursorTheme::shape(const QByteArray &name) const
0237 {
0238     return d->registry.value(name);
0239 }
0240 
0241 } // namespace KWin