feat(mouse): add cursor theme previews and size settings

main
reionwong 2 months ago
parent f0483981ab
commit 87ff66e56f

@ -18,6 +18,7 @@ set(QT Core Gui Widgets Quick QuickControls2 DBus Xml Concurrent LinguistTools)
find_package(Qt6 REQUIRED ${QT})
find_package(Freetype REQUIRED)
find_package(PkgConfig REQUIRED)
pkg_check_modules(Xcursor REQUIRED IMPORTED_TARGET xcursor)
find_package(Libcrypt REQUIRED)
# find_package(QApt REQUIRED)
@ -78,6 +79,7 @@ set_source_files_properties(${DBUS_SOURCES} PROPERTIES SKIP_AUTOGEN ON)
add_executable(${PROJECT_NAME} ${DBUS_SOURCES} ${SRCS} ${RESOURCES})
target_link_libraries(${PROJECT_NAME}
PkgConfig::Xcursor
Cutefish::Applications
Cutefish::Appearance
Qt6::Core

1
debian/control vendored

@ -9,6 +9,7 @@ Build-Depends: cmake,
libcrypt-dev,
libfreetype6-dev,
libfontconfig1-dev,
libxcursor-dev,
libkf6networkmanagerqt-dev,
libkf6config-dev,
libkf6modemmanagerqt-dev,

@ -11,6 +11,7 @@
#include <QIcon>
#include <QSettings>
#include <X11/Xcursor/Xcursor.h>
CursorTheme::CursorTheme(const QDir &dir, QObject *parent)
: QObject(parent)
@ -19,9 +20,12 @@ CursorTheme::CursorTheme(const QDir &dir, QObject *parent)
, m_sample(QStringLiteral("left_ptr"))
, m_hidden(false)
{
QSettings config(m_path + QStringLiteral("/index.theme"), QSettings::IniFormat);
QSettings config(m_path
+ (dir.exists("index.theme") ? QStringLiteral("/index.theme")
: QStringLiteral("/cursor.theme")),
QSettings::IniFormat);
config.beginGroup(QStringLiteral("Icon Theme"));
m_name = config.value(QStringLiteral("Name")).toString();
m_name = config.value(QStringLiteral("Name"), m_dirName).toString();
m_sample = config.value(QStringLiteral("Example"), m_sample).toString();
m_hidden = config.value(QStringLiteral("Hidden"), false).toBool();
m_inherits = config.value(QStringLiteral("Inherits")).toString();
@ -50,9 +54,19 @@ QString CursorTheme::inherits() const
QPixmap CursorTheme::pixmap() const
{
if (m_pixmap.isNull()) {
const QIcon icon = QIcon::fromTheme(m_sample,
QIcon::fromTheme(QStringLiteral("input-mouse")));
m_pixmap = icon.pixmap(36, 36);
XcursorImages *images = XcursorLibraryLoadImages(m_sample.toUtf8().constData(),
m_dirName.toUtf8().constData(), 36);
if (!images)
images = XcursorLibraryLoadImages("left_ptr", m_dirName.toUtf8().constData(), 36);
if (images) {
if (images->nimage > 0) {
const auto *frame = images->images[0];
QImage image(reinterpret_cast<const uchar *>(frame->pixels), frame->width,
frame->height, frame->width * 4, QImage::Format_ARGB32_Premultiplied);
m_pixmap = QPixmap::fromImage(image.copy());
}
XcursorImagesDestroy(images);
}
}
return m_pixmap;

@ -10,8 +10,8 @@
#ifndef CURSORTHEME_H
#define CURSORTHEME_H
#include <QObject>
#include <QDir>
#include <QObject>
#include <QPixmap>
class CursorTheme : public QObject

@ -19,9 +19,15 @@
#include "cursorthememodel.h"
#include "cursortheme.h"
#include <QFileInfo>
#include <QDBusConnection>
#include <QDBusMessage>
#include <QDBusPendingCallWatcher>
#include <QDBusPendingReply>
#include <QDBusServiceWatcher>
#include <QDebug>
#include <QDir>
#include <QFileInfo>
#include <QStandardPaths>
CursorThemeModel::CursorThemeModel(QObject *parent)
: QAbstractListModel(parent)
@ -30,6 +36,14 @@ CursorThemeModel::CursorThemeModel(QObject *parent)
initThemes();
m_currentTheme = m_settings.value("CursorTheme", "default").toString();
auto bus = QDBusConnection::sessionBus();
for (const auto *signal : {"cursorThemeChanged", "cursorSizeChanged"})
bus.connect("com.cutefish.Services", "/com/cutefish/Services/Appearance",
"com.cutefish.Services.Appearance", QLatin1String(signal), this,
SLOT(reload()));
auto *watcher = new QDBusServiceWatcher("com.cutefish.Services", bus,
QDBusServiceWatcher::WatchForOwnerChange, this);
connect(watcher, &QDBusServiceWatcher::serviceOwnerChanged, this, &CursorThemeModel::reload);
}
CursorThemeModel::~CursorThemeModel()
@ -87,12 +101,44 @@ QString CursorThemeModel::currentTheme() const
void CursorThemeModel::setCurrentTheme(const QString &theme)
{
if (m_currentTheme != theme) {
m_currentTheme = theme;
emit currentThemeChanged();
if (m_currentTheme != theme && themeIndex(theme) >= 0)
apply(QStringLiteral("setCursorTheme"), theme);
}
m_appearance.setCursorTheme(m_currentTheme);
}
int CursorThemeModel::cursorSize() const
{
return qBound(16, m_settings.value("CursorSize", 24).toInt(), 64);
}
void CursorThemeModel::setCursorSize(int size)
{
apply(QStringLiteral("setCursorSize"), qBound(16, size, 64));
}
void CursorThemeModel::reload()
{
m_settings.sync();
m_currentTheme = m_settings.value("CursorTheme", "default").toString();
emit currentThemeChanged();
emit cursorSizeChanged();
}
void CursorThemeModel::apply(const QString &method, const QVariant &value)
{
auto message = QDBusMessage::createMethodCall("com.cutefish.Services",
"/com/cutefish/Services/Appearance",
"com.cutefish.Services.Appearance", method);
message << value;
auto *watcher =
new QDBusPendingCallWatcher(QDBusConnection::sessionBus().asyncCall(message), this);
connect(watcher, &QDBusPendingCallWatcher::finished, this,
[this](QDBusPendingCallWatcher *call) {
const QDBusPendingReply<> reply = *call;
if (reply.isError())
qWarning() << "Cannot apply cursor theme:" << reply.error().message();
reload();
call->deleteLater();
});
}
int CursorThemeModel::themeIndex(const QString &theme)
@ -132,9 +178,18 @@ void CursorThemeModel::initThemes()
QStringList CursorThemeModel::paths()
{
QStringList list;
list << QDir::home().path() + ".icons";
list << "/usr/share/icons";
list = qEnvironmentVariable("XCURSOR_PATH").split(':', Qt::SkipEmptyParts);
if (!list.isEmpty()) {
for (auto &path : list)
if (path.startsWith("~/"))
path.replace(0, 1, QDir::homePath());
return list;
}
list << QDir::homePath() + "/.icons";
for (const auto &path : QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation))
list << path + "/icons";
list << "/usr/share/pixmaps";
list.removeDuplicates();
return list;
}
@ -152,7 +207,10 @@ void CursorThemeModel::processDir(const QDir &dir)
return;
}
if (!dir.exists("cursors") || QDir(dir.path() + "/cursor").entryList(QDir::Files | QDir::NoDotAndDotDot).isEmpty()) {
if (!dir.exists("cursors")
|| QDir(dir.path() + "/cursors")
.entryList(QDir::Files | QDir::NoDotAndDotDot)
.isEmpty()) {
if (dir.exists("index.theme")) {
CursorTheme theme(dir);
m_defaultTheme = theme.inherits();
@ -171,7 +229,7 @@ void CursorThemeModel::processDir(const QDir &dir)
for (int i = 0; i < m_list.size(); ++i) {
CursorTheme *t = m_list.at(i);
if (t->name() == theme->name()) {
if (t->id() == theme->id()) {
delete theme;
return;
}

@ -23,16 +23,17 @@
#include <QAbstractListModel>
#include <QSettings>
#include <appearance.h>
class QDir;
class CursorTheme;
class CursorThemeModel : public QAbstractListModel
{
Q_OBJECT
public:
Q_PROPERTY(QString currentTheme READ currentTheme WRITE setCurrentTheme NOTIFY currentThemeChanged)
Q_PROPERTY(
QString currentTheme READ currentTheme WRITE setCurrentTheme NOTIFY currentThemeChanged)
Q_PROPERTY(int cursorSize READ cursorSize WRITE setCursorSize NOTIFY cursorSizeChanged)
enum DataRoles {
NameRole = Qt::UserRole + 1,
@ -50,11 +51,17 @@ public:
QString currentTheme() const;
void setCurrentTheme(const QString &theme);
int cursorSize() const;
void setCursorSize(int size);
Q_INVOKABLE int themeIndex(const QString &theme);
signals:
void currentThemeChanged();
void cursorSizeChanged();
private slots:
void reload();
private:
void initThemes();
@ -62,7 +69,7 @@ private:
void processDir(const QDir &dir);
private:
Appearance m_appearance;
void apply(const QString &method, const QVariant &value);
QList<CursorTheme *> m_list;
QString m_defaultTheme;
QString m_currentTheme;

@ -135,6 +135,19 @@ ItemPage {
visible: _view.count > 0
}
RowLayout {
Label { text: qsTr("Cursor size") }
Slider {
Layout.fillWidth: true
from: 16
to: 64
stepSize: 8
value: cursorModel.cursorSize
onMoved: cursorModel.cursorSize = value
}
Label { text: cursorModel.cursorSize }
}
GridView {
id: _view
Layout.fillWidth: true
@ -151,7 +164,7 @@ ItemPage {
currentIndex: cursorModel.themeIndex(cursorModel.currentTheme)
property int rowCount: _view.width / itemWidth
property int rowCount: Math.max(1, Math.floor(_view.width / itemWidth))
property int itemWidth: 128
property int itemHeight: 128
@ -185,9 +198,7 @@ ItemPage {
anchors.fill: parent
anchors.margins: FishUI.Units.smallSpacing * 1.5
onClicked: {
_view.currentIndex = index
cursorModel.currentTheme = model.id
console.log(model.id)
}
}

@ -0,0 +1,15 @@
cmake_minimum_required(VERSION 3.22)
project(cursor_theme_test LANGUAGES CXX)
set(CMAKE_AUTOMOC ON)
set(CMAKE_CXX_STANDARD 17)
find_package(Qt6 REQUIRED COMPONENTS Core Gui DBus Test)
find_package(PkgConfig REQUIRED)
pkg_check_modules(Xcursor REQUIRED IMPORTED_TARGET xcursor)
find_program(DBUS_RUN_SESSION dbus-run-session REQUIRED)
add_executable(cursor_theme_test cursor_theme_test.cpp ../../src/cursor/cursorthememodel.cpp
../../src/cursor/cursortheme.cpp)
target_include_directories(cursor_theme_test PRIVATE ../../src)
target_link_libraries(cursor_theme_test PRIVATE Qt6::Core Qt6::Gui Qt6::DBus Qt6::Test PkgConfig::Xcursor)
enable_testing()
add_test(NAME cursor_theme_test COMMAND ${DBUS_RUN_SESSION} -- $<TARGET_FILE:cursor_theme_test>)
set_tests_properties(cursor_theme_test PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")

@ -0,0 +1,102 @@
#include "cursor/cursorthememodel.h"
#include <QDBusConnection>
#include <QDataStream>
#include <QDir>
#include <QFile>
#include <QGuiApplication>
#include <QPixmap>
#include <QSettings>
#include <QSignalSpy>
#include <QTemporaryDir>
#include <QTest>
class AppearanceService : public QObject
{
Q_OBJECT
Q_CLASSINFO("D-Bus Interface", "com.cutefish.Services.Appearance")
public slots:
void setCursorTheme(const QString &theme)
{
QSettings settings("cutefishos", "theme");
settings.setValue("CursorTheme", theme);
settings.sync();
emit cursorThemeChanged();
}
void setCursorSize(int size)
{
QSettings settings("cutefishos", "theme");
settings.setValue("CursorSize", size);
settings.sync();
emit cursorSizeChanged();
}
signals:
void cursorThemeChanged();
void cursorSizeChanged();
};
class CursorThemeTest : public QObject
{
Q_OBJECT
private slots:
void previewsAndServiceUpdates()
{
QTemporaryDir directory;
QVERIFY(directory.isValid());
qputenv("XCURSOR_PATH", directory.path().toUtf8());
QSettings::setPath(QSettings::NativeFormat, QSettings::UserScope, directory.path());
QVERIFY(QDir(directory.path()).mkpath("TestTheme/cursors"));
QFile index(directory.filePath("TestTheme/index.theme"));
QVERIFY(index.open(QIODevice::WriteOnly));
// Icon-theme visibility must not hide bundled cursor themes such as Adwaita.
index.write("[Icon Theme]\nName=Test Cursor\nHidden=true\n");
index.close();
QFile cursor(directory.filePath("TestTheme/cursors/left_ptr"));
QVERIFY(cursor.open(QIODevice::WriteOnly));
QDataStream out(&cursor);
out.setByteOrder(QDataStream::LittleEndian);
out << quint32(0x72756358) << quint32(16) << quint32(1) << quint32(1);
out << quint32(0xfffd0002) << quint32(36) << quint32(28);
out << quint32(36) << quint32(0xfffd0002) << quint32(36) << quint32(1);
out << quint32(1) << quint32(1) << quint32(0) << quint32(0) << quint32(10)
<< quint32(0xff123456);
cursor.close();
AppearanceService service;
auto bus = QDBusConnection::sessionBus();
QVERIFY(bus.registerObject("/com/cutefish/Services/Appearance", &service,
QDBusConnection::ExportAllSlots
| QDBusConnection::ExportAllSignals));
QVERIFY(bus.registerService("com.cutefish.Services"));
CursorThemeModel first;
CursorThemeModel second;
const int row = first.themeIndex("TestTheme");
QVERIFY(row >= 0);
const auto preview =
first.data(first.index(row), CursorThemeModel::ImageRole).value<QPixmap>();
QVERIFY(!preview.isNull());
QCOMPARE(preview.toImage().pixel(0, 0), QRgb(0xff123456));
QSignalSpy changed(&second, &CursorThemeModel::currentThemeChanged);
first.setCurrentTheme("TestTheme");
QTRY_COMPARE(second.currentTheme(), QStringLiteral("TestTheme"));
QVERIFY(!changed.isEmpty());
first.setCursorSize(48);
QTRY_COMPARE(second.cursorSize(), 48);
first.setCurrentTheme("../missing");
QCOMPARE(first.currentTheme(), QStringLiteral("TestTheme"));
bus.unregisterService("com.cutefish.Services");
bus.unregisterObject("/com/cutefish/Services/Appearance");
}
};
int main(int argc, char **argv)
{
QGuiApplication application(argc, argv);
CursorThemeTest test;
return QTest::qExec(&test, argc, argv);
}
#include "cursor_theme_test.moc"
Loading…
Cancel
Save