File indexing completed on 2025-03-02 05:11:58

0001 /*
0002     SPDX-FileCopyrightText: 2006-2007 Fredrik Höglund <fredrik@kde.org>
0003 
0004     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only
0005 */
0006 
0007 #include <KConfig>
0008 #include <KConfigGroup>
0009 #include <KLocalizedString>
0010 
0011 #include <QCursor>
0012 #include <QDir>
0013 #include <QImage>
0014 #include <private/qtx11extras_p.h>
0015 
0016 #include <X11/Xcursor/Xcursor.h>
0017 #include <X11/Xlib.h>
0018 
0019 #include "xcursortheme.h"
0020 
0021 using namespace Qt::StringLiterals;
0022 
0023 // Static variable holding alternative names for some cursors
0024 QHash<QString, QString> XCursorTheme::alternatives;
0025 
0026 XCursorTheme::XCursorTheme(const QDir &themeDir)
0027     : CursorTheme(themeDir.dirName())
0028 {
0029     // Directory information
0030     setName(themeDir.dirName());
0031     setPath(themeDir.path());
0032     setIsWritable(QFileInfo(themeDir.path()).isWritable()); // ### perhaps this shouldn't be cached
0033 
0034     if (themeDir.exists(QStringLiteral("index.theme")))
0035         parseIndexFile();
0036 
0037     QString cursorFile = path() + "/cursors/left_ptr";
0038     QList<int> sizeList;
0039     XcursorImages *images = XcursorFilenameLoadAllImages(qPrintable(cursorFile));
0040     if (images) {
0041         for (int i = 0; i < images->nimage; ++i) {
0042             if (!sizeList.contains(images->images[i]->size))
0043                 sizeList.append(images->images[i]->size);
0044         };
0045         XcursorImagesDestroy(images);
0046         std::sort(sizeList.begin(), sizeList.end());
0047         m_availableSizes = sizeList;
0048     }
0049     if (!sizeList.isEmpty()) {
0050         QString sizeListString = QString::number(sizeList.takeFirst());
0051         while (!sizeList.isEmpty()) {
0052             sizeListString.append(", ");
0053             sizeListString.append(QString::number(sizeList.takeFirst()));
0054         };
0055         QString tempString = i18nc(
0056             "@info The argument is the list of available sizes (in pixel). Example: "
0057             "'Available sizes: 24' or 'Available sizes: 24, 36, 48'",
0058             "(Available sizes: %1)",
0059             sizeListString);
0060         if (m_description.isEmpty())
0061             m_description = tempString;
0062         else
0063             m_description = m_description + ' ' + tempString;
0064     };
0065 }
0066 
0067 void XCursorTheme::parseIndexFile()
0068 {
0069     KConfig config(path() + "/index.theme", KConfig::NoGlobals);
0070     KConfigGroup cg(&config, u"Icon Theme"_s);
0071 
0072     m_title = cg.readEntry("Name", m_title);
0073     m_description = cg.readEntry("Comment", m_description);
0074     m_sample = cg.readEntry("Example", m_sample);
0075     m_hidden = cg.readEntry("Hidden", false);
0076     m_inherits = cg.readEntry("Inherits", QStringList());
0077 }
0078 
0079 QString XCursorTheme::findAlternative(const QString &name) const
0080 {
0081     if (alternatives.isEmpty()) {
0082         alternatives.reserve(18);
0083 
0084         // Qt uses non-standard names for some core cursors.
0085         // If Xcursor fails to load the cursor, Qt creates it with the correct name using the
0086         // core protocol instead (which in turn calls Xcursor). We emulate that process here.
0087         // Note that there's a core cursor called cross, but it's not the one Qt expects.
0088         alternatives.insert(QStringLiteral("cross"), QStringLiteral("crosshair"));
0089         alternatives.insert(QStringLiteral("up_arrow"), QStringLiteral("center_ptr"));
0090         alternatives.insert(QStringLiteral("wait"), QStringLiteral("watch"));
0091         alternatives.insert(QStringLiteral("ibeam"), QStringLiteral("xterm"));
0092         alternatives.insert(QStringLiteral("size_all"), QStringLiteral("fleur"));
0093         alternatives.insert(QStringLiteral("pointing_hand"), QStringLiteral("hand2"));
0094 
0095         // Precomputed MD5 hashes for the hardcoded bitmap cursors in Qt and KDE.
0096         // Note that the MD5 hash for left_ptr_watch is for the KDE version of that cursor.
0097         alternatives.insert(QStringLiteral("size_ver"), QStringLiteral("00008160000006810000408080010102"));
0098         alternatives.insert(QStringLiteral("size_hor"), QStringLiteral("028006030e0e7ebffc7f7070c0600140"));
0099         alternatives.insert(QStringLiteral("size_bdiag"), QStringLiteral("fcf1c3c7cd4491d801f1e1c78f100000"));
0100         alternatives.insert(QStringLiteral("size_fdiag"), QStringLiteral("c7088f0f3e6c8088236ef8e1e3e70000"));
0101         alternatives.insert(QStringLiteral("whats_this"), QStringLiteral("d9ce0ab605698f320427677b458ad60b"));
0102         alternatives.insert(QStringLiteral("split_h"), QStringLiteral("14fef782d02440884392942c11205230"));
0103         alternatives.insert(QStringLiteral("split_v"), QStringLiteral("2870a09082c103050810ffdffffe0204"));
0104         alternatives.insert(QStringLiteral("forbidden"), QStringLiteral("03b6e0fcb3499374a867c041f52298f0"));
0105         alternatives.insert(QStringLiteral("left_ptr_watch"), QStringLiteral("3ecb610c1bf2410f44200f48c40d3599"));
0106         alternatives.insert(QStringLiteral("hand2"), QStringLiteral("e29285e634086352946a0e7090d73106"));
0107         alternatives.insert(QStringLiteral("openhand"), QStringLiteral("9141b49c8149039304290b508d208c40"));
0108         alternatives.insert(QStringLiteral("closedhand"), QStringLiteral("05e88622050804100c20044008402080"));
0109     }
0110 
0111     return alternatives.value(name, QString());
0112 }
0113 
0114 XcursorImage *XCursorTheme::xcLoadImage(const QString &image, int size) const
0115 {
0116     QByteArray cursorName = QFile::encodeName(image);
0117     QByteArray themeName = QFile::encodeName(name());
0118 
0119     return XcursorLibraryLoadImage(cursorName, themeName, size);
0120 }
0121 
0122 XcursorImages *XCursorTheme::xcLoadImages(const QString &image, int size) const
0123 {
0124     QByteArray cursorName = QFile::encodeName(image);
0125     QByteArray themeName = QFile::encodeName(name());
0126 
0127     return XcursorLibraryLoadImages(cursorName, themeName, size);
0128 }
0129 
0130 int XCursorTheme::defaultCursorSize() const
0131 {
0132     // TODO: manage Wayland
0133     if (!QX11Info::isPlatformX11()) {
0134         return 32;
0135     }
0136     /* This code is basically borrowed from display.c of the XCursor library
0137        We can't use "int XcursorGetDefaultSize(Display *dpy)" because if
0138        previously the cursor size was set to a custom value, it would return
0139        this custom value. */
0140     int size = 0;
0141     int dpi = 0;
0142     Display *dpy = QX11Info::display();
0143     // The string "v" is owned and will be destroyed by Xlib
0144     char *v = XGetDefault(dpy, "Xft", "dpi");
0145     if (v)
0146         dpi = atoi(v);
0147     if (dpi)
0148         size = dpi * 16 / 72;
0149     if (size == 0) {
0150         int dim;
0151         if (DisplayHeight(dpy, DefaultScreen(dpy)) < DisplayWidth(dpy, DefaultScreen(dpy))) {
0152             dim = DisplayHeight(dpy, DefaultScreen(dpy));
0153         } else {
0154             dim = DisplayWidth(dpy, DefaultScreen(dpy));
0155         }
0156         size = dim / 48;
0157     }
0158     return size;
0159 }
0160 
0161 qulonglong XCursorTheme::loadCursor(const QString &name, int size) const
0162 {
0163     // TODO: manage Wayland
0164     if (!QX11Info::isPlatformX11()) {
0165         return None;
0166     }
0167     if (size <= 0)
0168         size = defaultCursorSize();
0169 
0170     // Load the cursor images
0171     XcursorImages *images = xcLoadImages(name, size);
0172 
0173     if (!images)
0174         images = xcLoadImages(findAlternative(name), size);
0175 
0176     if (!images)
0177         return None;
0178 
0179     // Create the cursor
0180     Cursor handle = XcursorImagesLoadCursor(QX11Info::display(), images);
0181     XcursorImagesDestroy(images);
0182 
0183     setCursorName(handle, name);
0184     return handle;
0185 }
0186 
0187 QImage XCursorTheme::loadImage(const QString &name, int size) const
0188 {
0189     if (size <= 0)
0190         size = defaultCursorSize();
0191 
0192     // Load the image
0193     XcursorImage *xcimage = xcLoadImage(name, size);
0194 
0195     if (!xcimage)
0196         xcimage = xcLoadImage(findAlternative(name), size);
0197 
0198     if (!xcimage) {
0199         return QImage();
0200     }
0201 
0202     // Convert the XcursorImage to a QImage, and auto-crop it
0203     QImage image((uchar *)xcimage->pixels, xcimage->width, xcimage->height, QImage::Format_ARGB32_Premultiplied);
0204 
0205     image = autoCropImage(image);
0206     XcursorImageDestroy(xcimage);
0207 
0208     return image;
0209 }
0210 
0211 std::vector<CursorTheme::CursorImage> XCursorTheme::loadImages(const QString &name, int size) const
0212 {
0213     if (size <= 0)
0214         size = defaultCursorSize();
0215 
0216     // Load the images
0217     XcursorImages *xcimages = xcLoadImages(name, size);
0218 
0219     if (!xcimages)
0220         xcimages = xcLoadImages(findAlternative(name), size);
0221 
0222     if (!xcimages) {
0223         return {};
0224     }
0225 
0226     std::vector<CursorImage> images;
0227     images.reserve(xcimages->nimage);
0228     for (int i = 0; i < xcimages->nimage; ++i) {
0229         // Convert the XcursorImage to a QImage, and auto-crop it
0230         const XcursorImage *xcimage = xcimages->images[i];
0231         QImage image(reinterpret_cast<unsigned char *>(xcimage->pixels), xcimage->width, xcimage->height, QImage::Format_ARGB32_Premultiplied);
0232         images.push_back(CursorImage{autoCropImage(image), std::chrono::milliseconds{xcimage->delay}});
0233     }
0234 
0235     XcursorImagesDestroy(xcimages);
0236 
0237     return images;
0238 }