From 05f72bba15026d3186f81180915750013d8d13d0 Mon Sep 17 00:00:00 2001 From: mrbesen Date: Sun, 4 Feb 2024 19:51:37 +0100 Subject: [PATCH] initial --- .gitignore | 6 + CursorRotate.pro | 41 +++++ dmz3.svg | 427 +++++++++++++++++++++++++++++++++++++++++++++ downloadDmz.sh | 2 + src/main.cpp | 22 +++ src/mainwindow.cpp | 19 ++ src/mainwindow.h | 31 ++++ src/mainwindow.ui | 142 +++++++++++++++ src/rotator.cpp | 240 +++++++++++++++++++++++++ src/rotator.h | 61 +++++++ 10 files changed, 991 insertions(+) create mode 100644 .gitignore create mode 100644 CursorRotate.pro create mode 100644 dmz3.svg create mode 100755 downloadDmz.sh create mode 100644 src/main.cpp create mode 100644 src/mainwindow.cpp create mode 100644 src/mainwindow.h create mode 100644 src/mainwindow.ui create mode 100644 src/rotator.cpp create mode 100644 src/rotator.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d4c996c --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.qmake.stash +CursorRotate +*.pro.user +Makefile +.vscode/settings.json +build/ \ No newline at end of file diff --git a/CursorRotate.pro b/CursorRotate.pro new file mode 100644 index 0000000..986de67 --- /dev/null +++ b/CursorRotate.pro @@ -0,0 +1,41 @@ +QT += core gui svg + +greaterThan(QT_MAJOR_VERSION, 4): QT += widgets + +CONFIG += c++17 +CONFIG += debug + +# The following define makes your compiler emit warnings if you use +# any Qt feature that has been marked deprecated (the exact warnings +# depend on your compiler). Please consult the documentation of the +# deprecated API in order to know how to port your code away from it. +DEFINES += QT_DEPRECATED_WARNINGS + +# You can also make your code fail to compile if it uses deprecated APIs. +# In order to do so, uncomment the following line. +# You can also select to disable deprecated APIs only up to a certain version of Qt. +#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 + +MOC_DIR = build/generated/ +UI_DIR = build/ui/ +RCC_DIR = build/rcc/ +OBJECTS_DIR = build/objects/ + +SOURCES += \ + src/main.cpp \ + src/mainwindow.cpp \ + src/rotator.cpp + +HEADERS += \ + src/mainwindow.h \ + src/rotator.h + +FORMS += \ + src/mainwindow.ui + +# Default rules for deployment. +qnx: target.path = /tmp/$${TARGET}/bin +else: unix:!android: target.path = /opt/$${TARGET}/bin +!isEmpty(target.path): INSTALLS += target + +LIBS += -lX11 -lXcursor \ No newline at end of file diff --git a/dmz3.svg b/dmz3.svg new file mode 100644 index 0000000..d65c594 --- /dev/null +++ b/dmz3.svg @@ -0,0 +1,427 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/downloadDmz.sh b/downloadDmz.sh new file mode 100755 index 0000000..210fae0 --- /dev/null +++ b/downloadDmz.sh @@ -0,0 +1,2 @@ +#!/bin/bash +curl -so dmz.svg "https://salsa.debian.org/gnome-team/dmz-cursor-theme/-/raw/debian/master/DMZ-White/dmz.svg" diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..71e9c00 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,22 @@ +#include "mainwindow.h" + +#include +#include + +#include "rotator.h" + +int main(int argc, char *argv[]) +{ + QApplication a(argc, argv); + MainWindow w; + + Rotator r; + + QObject::connect(&r, &Rotator::angleUpdated, &w, &MainWindow::angleUpdated); + QObject::connect(&r, &Rotator::newImage, &w, &MainWindow::newImage); + QObject::connect(&r, &Rotator::force, &w, &MainWindow::setForce); + QObject::connect(&w, &MainWindow::reset, &r, &Rotator::reset); + + w.show(); + return a.exec(); +} diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp new file mode 100644 index 0000000..3b2b457 --- /dev/null +++ b/src/mainwindow.cpp @@ -0,0 +1,19 @@ +#include "mainwindow.h" +#include "ui_mainwindow.h" + +MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWindow) { + ui->setupUi(this); + + QObject::connect(this, &MainWindow::angleUpdated, this->ui->dial, &QDial::setValue); + QObject::connect(this, &MainWindow::newImage, this->ui->label, &QLabel::setPixmap); + QObject::connect(this->ui->resetButton, &QPushButton::clicked, this, &MainWindow::reset); +} + +MainWindow::~MainWindow() { + delete ui; +} + +void MainWindow::setForce(QPointF force) { + this->ui->forceX->setValue(force.x()); + this->ui->forceY->setValue(-force.y()); +} diff --git a/src/mainwindow.h b/src/mainwindow.h new file mode 100644 index 0000000..efa5a04 --- /dev/null +++ b/src/mainwindow.h @@ -0,0 +1,31 @@ +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include +#include +#include + +QT_BEGIN_NAMESPACE +namespace Ui { class MainWindow; } +QT_END_NAMESPACE + +class MainWindow : public QMainWindow { + Q_OBJECT + +public: + MainWindow(QWidget* parent = nullptr); + ~MainWindow(); + +signals: + void angleUpdated(double newAngle); + void newImage(QPixmap map); + void reset(); + +public slots: + void setForce(QPointF force); + +private: + Ui::MainWindow* ui; +}; + +#endif // MAINWINDOW_H diff --git a/src/mainwindow.ui b/src/mainwindow.ui new file mode 100644 index 0000000..b74ac5b --- /dev/null +++ b/src/mainwindow.ui @@ -0,0 +1,142 @@ + + + MainWindow + + + + 0 + 0 + 234 + 439 + + + + MainWindow + + + + + false + + + + 150 + 10 + 50 + 64 + + + + 360 + + + 1 + + + 1 + + + true + + + 0.000000000000000 + + + false + + + + + + 0 + 0 + 100 + 100 + + + + + 0 + 0 + + + + border-color:red; + + + + + + 130 + 70 + 100 + 27 + + + + &Reset + + + + + + 10 + 130 + 160 + 16 + + + + -50 + + + 50 + + + Qt::Horizontal + + + QSlider::TicksAbove + + + + + + 10 + 150 + 16 + 160 + + + + -50 + + + 50 + + + 0 + + + Qt::Vertical + + + QSlider::TicksAbove + + + + + + + 0 + 0 + 234 + 24 + + + + + + + + diff --git a/src/rotator.cpp b/src/rotator.cpp new file mode 100644 index 0000000..2b5b955 --- /dev/null +++ b/src/rotator.cpp @@ -0,0 +1,240 @@ +#include "rotator.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +const QPointF Rotator::Gravity(0, 50); + +static QPointF rotate(QPointF point, double angle) { + const double c = qCos(qDegreesToRadians(angle)); + const double s = qSin(qDegreesToRadians(angle)); + + return {point.x() * c - point.y() * s, point.x() * s + point.y() * c}; +} + +static double lengthOfPoint(QPointF point) { + return std::sqrt((point.x() * point.x()) + (point.y() * point.y())); +} + +static double clampDegrees(double deg) { + while(deg < 0) { + deg += 360.0; + } + + while(deg > 360) { + deg -= 360.0; + } + + return deg; +} + +static double clampAngleDiff(double diff) { + diff = fmod(diff + 180.0, 360.0); + if (diff < 0) { + diff += 360.0; + } + return diff - 180.0; +} + +static double pointToAngle(QPointF point) { + return clampDegrees(-qRadiansToDegrees(qAtan2(point.x(), point.y()))); +} + +Rotator::Rotator(QObject* parent) : QObject(parent), disp(XOpenDisplay(nullptr)), renderer(new QSvgRenderer(QString("dmz3.svg"), this)), loopTimer(new QTimer(nullptr)), +lastTimePoint(QDateTime::currentMSecsSinceEpoch()), lastPosition(-1, -1), angle(0), angleVelocity(0) { + QObject::connect(loopTimer, &QTimer::timeout, this, &Rotator::loop); + loopTimer->setInterval(std::chrono::milliseconds(10)); + loopTimer->start(); + + // test + /* + for(int i = -360; i < 360; ++i) { + qDebug() << i << clampAngleDiff(i); + } + throw std::exception(); + */ +} + +Rotator::~Rotator() { + if(disp) { + XCloseDisplay(disp); + } +} + +void Rotator::forAllWindows(std::function func) { + forAllWindows(func, DefaultRootWindow(disp)); +} + +void Rotator::reset() { + angle = 0; + angleVelocity = 0; +} + +void Rotator::loop() { + int x = 0, y = 0; + unsigned int mask = 0; + Window rootRet; + Window childRet; + int xChild = 0, yChild = 0; + if(XQueryPointer(disp, DefaultRootWindow(disp), &rootRet, &childRet, &x, &y, &xChild, &yChild, &mask)) { + recalculateCursor({(double) x, (double) y}); + } +} + +void Rotator::recalculateCursor(QPointF newPos) { + quint64 currentTimePoint = QDateTime::currentMSecsSinceEpoch(); + + if(lastPosition.x() == -1) { + lastPosition = newPos; + lastTimePoint = currentTimePoint; + return; + } + + const double timeDiff = (currentTimePoint - lastTimePoint) / 1000.0; + + QPointF mouseForceVector = lastPosition - newPos; + QPointF forceVector = ((mouseForceVector * mouseForceStrength) + (Gravity * gravityStrength)) * timeDiff; + + + emit this->force(forceVector); + + // has position 0,0 (need soffset) + QPointF rotatedEndCap = rotate(QPointF(0, length), angle); + + QPointF forcedCap = rotatedEndCap + forceVector; + + + // fix the distance to the origin (should be exactly length) + //double forcedCapLength = lengthOfPoint(forcedCap); + //forcedCap *= length / forcedCapLength; + //qDebug() << "normalized:" << forcedCap; + + // calculate angle + double newAngle = clampDegrees(pointToAngle(forcedCap)); + // qDebug() << "force:" << forceVector << "forcedCap:" << forcedCap << "angle:" << newAngle; + + double angleDiff = clampAngleDiff(newAngle - angle); + + if(qAbs(angleDiff) < 0.01 * timeDiff) { + lastPosition = newPos; + lastTimePoint = currentTimePoint; + return; + } + + newAngle += angleVelocity * timeDiff; + + angleVelocity += angleDiff / timeDiff; + qDebug() << "angleDiff:" << angleDiff << "angleVelocity:" << angleVelocity << "angle:" << newAngle; + + angleVelocity *= 0.95; + if(qAbs(angleVelocity) < 0.001) { + angleVelocity = 0.0; + } + + // calculate offset ( is this required? ) + // QPointF realCap = forcedCap + newPos; + angle = clampDegrees(newAngle); + + // just rotate + // angle += 300 * timeDiff; + + emit this->angleUpdated(angle); + + lastPosition = newPos; + lastTimePoint = currentTimePoint; + + rerender(); +} + +void Rotator::rerender() { + int size = 100; + + QPixmap px(size, size); + px.fill(QColorConstants::Transparent); + + QPainter painter(&px); + + QTransform transform; + qreal scaleF = 0.7; + qreal tX = 16 * (size/32.0); + qreal tY = 12 * (size/32.0); + qreal lateTranslate = size/2.0; + transform.translate(lateTranslate, lateTranslate).scale(scaleF, scaleF).rotate(angle).translate(-tX, -tY); + painter.setTransform(transform, false); + renderer->render(&painter); + + painter.setPen({QColorConstants::Red}); + painter.drawLine(tX-10, tY, tX+10, tY); + painter.drawLine(tX, tY-10, tX, tY+10); + painter.setTransform(QTransform(), false); + painter.drawRect(0, 0, size-1, size-1); + + emit this->newImage(px); + + // setCursor(px, {50, 50}); +} + +void Rotator::setCursor(const QPixmap& px, const QPointF& hotspot) { + // Get the raw XImage from the QPixmap + QImage image = px.toImage(); + uint32_t imageSize = image.width() * image.depth() * image.height(); + char* imageData = (char*) malloc(imageSize); + memcpy(imageData, image.bits(), imageSize); + XImage* xImage = XCreateImage( + disp, + DefaultVisual(disp, DefaultScreen(disp)), + DefaultDepth(disp, DefaultScreen(disp)), + ZPixmap, 0, imageData, + image.width(), image.height(), + image.depth(), 0 + ); + + // Create the XcursorImages + XcursorImage* cursorImage = XcursorImageCreate(image.width(), image.height()); + cursorImage->xhot = hotspot.x(); + cursorImage->yhot = hotspot.y(); + + // Copy the image data to XcursorImage + cursorImage->pixels = (XcursorPixel*) xImage->data; + cursorImage->delay = 0; + + // Create the cursor + Cursor cursor = XcursorImageLoadCursor(disp, cursorImage); + + // Set the cursor + XDefineCursor(disp, DefaultRootWindow(disp), cursor); + + // Clean up resources + // XFreeCursor(disp, cursor); + // XDestroyImage(xImage); +} + +void Rotator::forAllWindows(std::function func, Window root) +{ + func(root); + + Window* children = nullptr; + Window dummy; + unsigned int nchildren; + if (!XQueryTree(disp, root, &dummy, &dummy, &children, &nchildren)) { + // no children + return; + } + + for (unsigned int i = 0; i < nchildren; i++) { + forAllWindows(func, children[i]); + } + + if (children) { + XFree((char*) children); + } +} diff --git a/src/rotator.h b/src/rotator.h new file mode 100644 index 0000000..9806ea4 --- /dev/null +++ b/src/rotator.h @@ -0,0 +1,61 @@ +#pragma once + +#include + +#include +#include +#include + +class QSvgRenderer; +class QTimer; + +struct _XDisplay; +using Display = _XDisplay; +using Window = unsigned long; + +class Rotator : public QObject { + Q_OBJECT + +public: + explicit Rotator(QObject* parent = nullptr); + virtual ~Rotator(); + + void forAllWindows(std::function func); + +signals: + void angleUpdated(double newAngle); + void newImage(QPixmap map); + void force(QPointF force); + +public slots: + void reset(); + +private slots: + void loop(); + +private: + void recalculateCursor(QPointF newPos); + void rerender(); + void setCursor(const QPixmap& px, const QPointF& hostspot); + + void forAllWindows(std::function func, Window root); + + Display* disp; + QSvgRenderer* renderer; + QTimer* loopTimer; + + quint64 lastTimePoint; + + QPointF lastPosition; + double angle; // deg + double angleVelocity; // deg / s + + // TODO: make them changeable + double gravityStrength = 1.0; + double mouseForceStrength = 1.0; + double impulseStrength = 1.0; + + static constexpr double length = 10.0; + + static const QPointF Gravity; +};