utils: Add support for svg cursors
With this change, KXcursorTheme will be able to load svg cursors provided by breeze. If a cursor theme provides both xcursor cursors and svg cursors, the svg cursors will be preferred. At the moment, KXcursorTheme doesn't cache svg render results but it could do that if it becomes a noticeable issue.
This commit is contained in:
parent
e7c1144e2c
commit
266c6ee855
7 changed files with 316 additions and 60 deletions
|
@ -63,6 +63,7 @@ find_package(Qt6 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS
|
|||
WaylandClient
|
||||
Widgets
|
||||
Sensors
|
||||
Svg
|
||||
)
|
||||
|
||||
find_package(Qt6Test ${QT_MIN_VERSION} CONFIG QUIET)
|
||||
|
|
|
@ -206,7 +206,9 @@ target_sources(kwin PRIVATE
|
|||
tiles/tilemanager.cpp
|
||||
touch_input.cpp
|
||||
useractions.cpp
|
||||
utils/svgcursorreader.cpp
|
||||
utils/version.cpp
|
||||
utils/xcursorreader.cpp
|
||||
virtualdesktops.cpp
|
||||
virtualdesktopsdbustypes.cpp
|
||||
virtualkeyboard_dbus.cpp
|
||||
|
@ -234,8 +236,9 @@ target_link_libraries(kwin
|
|||
|
||||
PRIVATE
|
||||
Qt::Concurrent
|
||||
Qt::Sensors
|
||||
Qt::GuiPrivate
|
||||
Qt::Sensors
|
||||
Qt::Svg
|
||||
|
||||
KF6::ColorScheme
|
||||
KF6::ConfigGui
|
||||
|
|
143
src/utils/svgcursorreader.cpp
Normal file
143
src/utils/svgcursorreader.cpp
Normal file
|
@ -0,0 +1,143 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2024 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
|
||||
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#include "utils/svgcursorreader.h"
|
||||
#include "utils/common.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QPainter>
|
||||
#include <QSvgRenderer>
|
||||
|
||||
namespace KWin
|
||||
{
|
||||
|
||||
struct SvgCursorMetaDataEntry
|
||||
{
|
||||
static std::optional<SvgCursorMetaDataEntry> parse(const QJsonObject &object);
|
||||
|
||||
QString fileName;
|
||||
QPointF hotspot;
|
||||
std::chrono::milliseconds delay;
|
||||
};
|
||||
|
||||
std::optional<SvgCursorMetaDataEntry> SvgCursorMetaDataEntry::parse(const QJsonObject &object)
|
||||
{
|
||||
const QJsonValue fileName = object.value(QLatin1String("filename"));
|
||||
if (!fileName.isString()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const QJsonValue hotspotX = object.value(QLatin1String("hotspot_x"));
|
||||
if (!hotspotX.isDouble()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const QJsonValue hotspotY = object.value(QLatin1String("hotspot_y"));
|
||||
if (!hotspotY.isDouble()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const QJsonValue frametime = object.value(QLatin1String("frametime"));
|
||||
|
||||
return SvgCursorMetaDataEntry{
|
||||
.fileName = fileName.toString(),
|
||||
.hotspot = QPointF(hotspotX.toDouble(), hotspotY.toDouble()),
|
||||
.delay = std::chrono::milliseconds(frametime.toInt()),
|
||||
};
|
||||
}
|
||||
|
||||
struct SvgCursorMetaData
|
||||
{
|
||||
static std::optional<SvgCursorMetaData> parse(const QString &filePath);
|
||||
|
||||
QList<SvgCursorMetaDataEntry> entries;
|
||||
};
|
||||
|
||||
std::optional<SvgCursorMetaData> SvgCursorMetaData::parse(const QString &filePath)
|
||||
{
|
||||
QFile metaDataFile(filePath);
|
||||
if (!metaDataFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
QJsonParseError jsonParseError;
|
||||
const QJsonDocument document = QJsonDocument::fromJson(metaDataFile.readAll(), &jsonParseError);
|
||||
if (jsonParseError.error) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
QList<SvgCursorMetaDataEntry> entries;
|
||||
if (document.isObject()) {
|
||||
if (const auto entry = SvgCursorMetaDataEntry::parse(document.object())) {
|
||||
entries.append(entry.value());
|
||||
} else {
|
||||
return std::nullopt;
|
||||
}
|
||||
} else if (document.isArray()) {
|
||||
const QJsonArray array = document.array();
|
||||
for (int i = 0; i < array.size(); ++i) {
|
||||
const QJsonValue element = array.at(i);
|
||||
if (!element.isObject()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
if (const auto entry = SvgCursorMetaDataEntry::parse(element.toObject())) {
|
||||
entries.append(entry.value());
|
||||
} else {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return SvgCursorMetaData{
|
||||
.entries = entries,
|
||||
};
|
||||
}
|
||||
|
||||
QList<KXcursorSprite> SvgCursorReader::load(const QString &containerPath, int desiredSize, qreal devicePixelRatio)
|
||||
{
|
||||
const QDir containerDir(containerPath);
|
||||
|
||||
const QString metadataFilePath = containerDir.filePath(QStringLiteral("metadata.json"));
|
||||
const auto metadata = SvgCursorMetaData::parse(metadataFilePath);
|
||||
if (!metadata.has_value()) {
|
||||
qCWarning(KWIN_CORE) << "Failed to parse" << metadataFilePath;
|
||||
return {};
|
||||
}
|
||||
|
||||
const qreal scale = desiredSize / 24.0;
|
||||
|
||||
QList<KXcursorSprite> sprites;
|
||||
for (const SvgCursorMetaDataEntry &entry : metadata->entries) {
|
||||
const QString filePath = containerDir.filePath(entry.fileName);
|
||||
|
||||
QSvgRenderer renderer(filePath);
|
||||
if (!renderer.isValid()) {
|
||||
qCWarning(KWIN_CORE) << "Failed to render" << filePath;
|
||||
return {};
|
||||
}
|
||||
|
||||
const QRect bounds(QPoint(0, 0), renderer.defaultSize() * scale);
|
||||
QImage image(bounds.size() * devicePixelRatio, QImage::Format_ARGB32_Premultiplied);
|
||||
image.fill(Qt::transparent);
|
||||
image.setDevicePixelRatio(devicePixelRatio);
|
||||
|
||||
QPainter painter(&image);
|
||||
renderer.render(&painter, bounds);
|
||||
painter.end();
|
||||
|
||||
sprites.append(KXcursorSprite(image, (entry.hotspot * scale).toPoint(), entry.delay));
|
||||
}
|
||||
|
||||
return sprites;
|
||||
}
|
||||
|
||||
} // namespace KWin
|
20
src/utils/svgcursorreader.h
Normal file
20
src/utils/svgcursorreader.h
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2024 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
|
||||
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "utils/xcursortheme.h"
|
||||
|
||||
namespace KWin
|
||||
{
|
||||
|
||||
class SvgCursorReader
|
||||
{
|
||||
public:
|
||||
static QList<KXcursorSprite> load(const QString &filePath, int desiredSize, qreal devicePixelRatio);
|
||||
};
|
||||
|
||||
} // namespace KWin
|
61
src/utils/xcursorreader.cpp
Normal file
61
src/utils/xcursorreader.cpp
Normal file
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2024 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
|
||||
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#include "utils/xcursorreader.h"
|
||||
#include "3rdparty/xcursor.h"
|
||||
|
||||
#include <QFile>
|
||||
|
||||
namespace KWin
|
||||
{
|
||||
|
||||
QList<KXcursorSprite> XCursorReader::load(const QString &filePath, int desiredSize, qreal devicePixelRatio)
|
||||
{
|
||||
QFile file(filePath);
|
||||
if (!file.open(QFile::ReadOnly)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
XcursorFile reader {
|
||||
.closure = &file,
|
||||
.read = [](XcursorFile *file, uint8_t *buffer, int len) -> int {
|
||||
QFile *device = static_cast<QFile *>(file->closure);
|
||||
return device->read(reinterpret_cast<char *>(buffer), len);
|
||||
},
|
||||
.skip = [](XcursorFile *file, long offset) -> XcursorBool {
|
||||
QFile *device = static_cast<QFile *>(file->closure);
|
||||
return device->skip(offset) != -1;
|
||||
},
|
||||
.seek = [](XcursorFile *file, long offset) -> XcursorBool {
|
||||
QFile *device = static_cast<QFile *>(file->closure);
|
||||
return device->seek(offset);
|
||||
},
|
||||
};
|
||||
|
||||
XcursorImages *images = XcursorXcFileLoadImages(&reader, desiredSize * devicePixelRatio);
|
||||
if (!images) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QList<KXcursorSprite> sprites;
|
||||
for (int i = 0; i < images->nimage; ++i) {
|
||||
const XcursorImage *nativeCursorImage = images->images[i];
|
||||
const qreal scale = std::max(qreal(1), qreal(nativeCursorImage->size) / desiredSize);
|
||||
const QPoint hotspot(nativeCursorImage->xhot, nativeCursorImage->yhot);
|
||||
const std::chrono::milliseconds delay(nativeCursorImage->delay);
|
||||
|
||||
QImage data(nativeCursorImage->width, nativeCursorImage->height, QImage::Format_ARGB32_Premultiplied);
|
||||
data.setDevicePixelRatio(scale);
|
||||
memcpy(data.bits(), nativeCursorImage->pixels, data.sizeInBytes());
|
||||
|
||||
sprites.append(KXcursorSprite(data, hotspot / scale, delay));
|
||||
}
|
||||
|
||||
XcursorImagesDestroy(images);
|
||||
return sprites;
|
||||
}
|
||||
|
||||
} // namespace KWin
|
20
src/utils/xcursorreader.h
Normal file
20
src/utils/xcursorreader.h
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2024 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
|
||||
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "utils/xcursortheme.h"
|
||||
|
||||
namespace KWin
|
||||
{
|
||||
|
||||
class XCursorReader
|
||||
{
|
||||
public:
|
||||
static QList<KXcursorSprite> load(const QString &filePath, int desiredSize, qreal devicePixelRatio);
|
||||
};
|
||||
|
||||
} // namespace KWin
|
|
@ -4,15 +4,15 @@
|
|||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#include "xcursortheme.h"
|
||||
#include "3rdparty/xcursor.h"
|
||||
#include "utils/xcursortheme.h"
|
||||
#include "utils/svgcursorreader.h"
|
||||
#include "utils/xcursorreader.h"
|
||||
|
||||
#include <KConfig>
|
||||
#include <KConfigGroup>
|
||||
#include <KShell>
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QSet>
|
||||
#include <QSharedData>
|
||||
#include <QStack>
|
||||
|
@ -29,14 +29,27 @@ public:
|
|||
std::chrono::milliseconds delay;
|
||||
};
|
||||
|
||||
struct KXcursorThemeXEntryInfo
|
||||
{
|
||||
QString path;
|
||||
};
|
||||
|
||||
struct KXcursorThemeSvgEntryInfo
|
||||
{
|
||||
QString path;
|
||||
};
|
||||
|
||||
using KXcursorThemeEntryInfo = std::variant<KXcursorThemeXEntryInfo,
|
||||
KXcursorThemeSvgEntryInfo>;
|
||||
|
||||
class KXcursorThemeEntry
|
||||
{
|
||||
public:
|
||||
explicit KXcursorThemeEntry(const QString &filePath);
|
||||
explicit KXcursorThemeEntry(const KXcursorThemeEntryInfo &info);
|
||||
|
||||
void load(int size, qreal devicePixelRatio);
|
||||
|
||||
QString filePath;
|
||||
KXcursorThemeEntryInfo info;
|
||||
QList<KXcursorSprite> sprites;
|
||||
};
|
||||
|
||||
|
@ -47,7 +60,8 @@ public:
|
|||
KXcursorThemePrivate(const QString &themeName, int size, qreal devicePixelRatio);
|
||||
|
||||
void discover(const QStringList &searchPaths);
|
||||
void discoverCursors(const QString &packagePath);
|
||||
void discoverXCursors(const QString &packagePath);
|
||||
void discoverSvgCursors(const QString &packagePath);
|
||||
|
||||
QString name;
|
||||
int size = 0;
|
||||
|
@ -111,65 +125,25 @@ KXcursorThemePrivate::KXcursorThemePrivate(const QString &themeName, int size, q
|
|||
{
|
||||
}
|
||||
|
||||
static QList<KXcursorSprite> loadCursor(const QString &filePath, int desiredSize, qreal devicePixelRatio)
|
||||
{
|
||||
QFile file(filePath);
|
||||
if (!file.open(QFile::ReadOnly)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
XcursorFile reader {
|
||||
.closure = &file,
|
||||
.read = [](XcursorFile *file, uint8_t *buffer, int len) -> int {
|
||||
QFile *device = static_cast<QFile *>(file->closure);
|
||||
return device->read(reinterpret_cast<char *>(buffer), len);
|
||||
},
|
||||
.skip = [](XcursorFile *file, long offset) -> XcursorBool {
|
||||
QFile *device = static_cast<QFile *>(file->closure);
|
||||
return device->skip(offset) != -1;
|
||||
},
|
||||
.seek = [](XcursorFile *file, long offset) -> XcursorBool {
|
||||
QFile *device = static_cast<QFile *>(file->closure);
|
||||
return device->seek(offset);
|
||||
},
|
||||
};
|
||||
|
||||
XcursorImages *images = XcursorXcFileLoadImages(&reader, desiredSize * devicePixelRatio);
|
||||
if (!images) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QList<KXcursorSprite> sprites;
|
||||
for (int i = 0; i < images->nimage; ++i) {
|
||||
const XcursorImage *nativeCursorImage = images->images[i];
|
||||
const qreal scale = std::max(qreal(1), qreal(nativeCursorImage->size) / desiredSize);
|
||||
const QPoint hotspot(nativeCursorImage->xhot, nativeCursorImage->yhot);
|
||||
const std::chrono::milliseconds delay(nativeCursorImage->delay);
|
||||
|
||||
QImage data(nativeCursorImage->width, nativeCursorImage->height, QImage::Format_ARGB32_Premultiplied);
|
||||
data.setDevicePixelRatio(scale);
|
||||
memcpy(data.bits(), nativeCursorImage->pixels, data.sizeInBytes());
|
||||
|
||||
sprites.append(KXcursorSprite(data, hotspot / scale, delay));
|
||||
}
|
||||
|
||||
XcursorImagesDestroy(images);
|
||||
return sprites;
|
||||
}
|
||||
|
||||
KXcursorThemeEntry::KXcursorThemeEntry(const QString &filePath)
|
||||
: filePath(filePath)
|
||||
KXcursorThemeEntry::KXcursorThemeEntry(const KXcursorThemeEntryInfo &info)
|
||||
: info(info)
|
||||
{
|
||||
}
|
||||
|
||||
void KXcursorThemeEntry::load(int size, qreal devicePixelRatio)
|
||||
{
|
||||
if (sprites.isEmpty()) {
|
||||
sprites = loadCursor(filePath, size, devicePixelRatio);
|
||||
if (!sprites.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (const auto raster = std::get_if<KXcursorThemeXEntryInfo>(&info)) {
|
||||
sprites = XCursorReader::load(raster->path, size, devicePixelRatio);
|
||||
} else if (const auto svg = std::get_if<KXcursorThemeSvgEntryInfo>(&info)) {
|
||||
sprites = SvgCursorReader::load(svg->path, size, devicePixelRatio);
|
||||
}
|
||||
}
|
||||
|
||||
void KXcursorThemePrivate::discoverCursors(const QString &packagePath)
|
||||
void KXcursorThemePrivate::discoverXCursors(const QString &packagePath)
|
||||
{
|
||||
const QDir dir(packagePath);
|
||||
QFileInfoList entries = dir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot);
|
||||
|
@ -191,7 +165,37 @@ void KXcursorThemePrivate::discoverCursors(const QString &packagePath)
|
|||
}
|
||||
}
|
||||
}
|
||||
registry.insert(shape, std::make_shared<KXcursorThemeEntry>(entry.absoluteFilePath()));
|
||||
registry.insert(shape, std::make_shared<KXcursorThemeEntry>(KXcursorThemeXEntryInfo{
|
||||
.path = entry.absoluteFilePath(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
void KXcursorThemePrivate::discoverSvgCursors(const QString &packagePath)
|
||||
{
|
||||
const QDir dir(packagePath);
|
||||
QFileInfoList entries = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot);
|
||||
std::partition(entries.begin(), entries.end(), [](const QFileInfo &fileInfo) {
|
||||
return !fileInfo.isSymLink();
|
||||
});
|
||||
|
||||
for (const QFileInfo &entry : std::as_const(entries)) {
|
||||
const QByteArray shape = QFile::encodeName(entry.fileName());
|
||||
if (registry.contains(shape)) {
|
||||
continue;
|
||||
}
|
||||
if (entry.isSymLink()) {
|
||||
const QFileInfo symLinkInfo(entry.symLinkTarget());
|
||||
if (symLinkInfo.absolutePath() == entry.absolutePath()) {
|
||||
if (auto alias = registry.value(QFile::encodeName(symLinkInfo.fileName()))) {
|
||||
registry.insert(shape, alias);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
registry.insert(shape, std::make_shared<KXcursorThemeEntry>(KXcursorThemeSvgEntryInfo{
|
||||
.path = entry.absoluteFilePath(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -240,7 +244,11 @@ void KXcursorThemePrivate::discover(const QStringList &searchPaths)
|
|||
if (!dir.exists()) {
|
||||
continue;
|
||||
}
|
||||
discoverCursors(dir.filePath(QStringLiteral("cursors")));
|
||||
if (const QDir package = dir.filePath(QLatin1String("cursors_scalable")); package.exists()) {
|
||||
discoverSvgCursors(package.path());
|
||||
} else if (const QDir package = dir.filePath(QLatin1String("cursors")); package.exists()) {
|
||||
discoverXCursors(package.path());
|
||||
}
|
||||
if (inherits.isEmpty()) {
|
||||
const KConfig config(dir.filePath(QStringLiteral("index.theme")), KConfig::NoGlobals);
|
||||
inherits << KConfigGroup(&config, QStringLiteral("Icon Theme")).readEntry("Inherits", QStringList());
|
||||
|
|
Loading…
Reference in a new issue