From 8fbd2f6a8a25e23f7a754fd917fd078e1646f339 Mon Sep 17 00:00:00 2001 From: Waozi Date: Sat, 11 Jul 2026 17:38:55 -0300 Subject: [PATCH] linux: use screenshot portal for Wayland QR scanning --- CMakeLists.txt | 9 ++ Dockerfile.linux | 13 ++- src/main/oshelper.cpp | 240 ++++++++++++++++++++++++++++++++++++++++-- src/main/oshelper.h | 24 ++++- 4 files changed, 272 insertions(+), 14 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 876029d3ed..4aca507550 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -122,6 +122,10 @@ set(QT5_LIBRARIES Qt5Xml ) +if(UNIX AND NOT APPLE AND NOT ANDROID) + list(APPEND QT5_LIBRARIES Qt5DBus) +endif() + if(WITH_SCANNER) list(APPEND QT5_LIBRARIES Qt5Multimedia) endif() @@ -300,6 +304,11 @@ if(STATIC) pkg_check_modules(FONTCONFIG REQUIRED fontconfig) list(APPEND QT5_LIBRARIES ${FONTCONFIG_STATIC_LIBRARIES}) endif() + + if(UNIX AND NOT APPLE AND NOT ANDROID) + pkg_check_modules(DBUS1_PKG_CONFIG REQUIRED dbus-1) + list(APPEND QT5_LIBRARIES ${DBUS1_PKG_CONFIG_STATIC_LIBRARIES} ${DBUS1_PKG_CONFIG_STATIC_LDFLAGS_OTHER}) + endif() endif() if(ANDROID) diff --git a/Dockerfile.linux b/Dockerfile.linux index 0f82d4bec9..bbc579fb53 100644 --- a/Dockerfile.linux +++ b/Dockerfile.linux @@ -9,10 +9,19 @@ ENV CXXFLAGS="-fPIC" ENV SOURCE_DATE_EPOCH=1397818193 RUN apt update && \ - apt install -y automake autopoint bison gettext git gperf libgl1-mesa-dev libglib2.0-dev ninja-build \ + apt install -y autoconf-archive automake autopoint bison gettext git gperf libexpat1-dev libgl1-mesa-dev libglib2.0-dev ninja-build \ libpng-dev libpthread-stubs0-dev libsodium-dev libtool-bin libudev-dev libusb-1.0-0-dev mesa-common-dev \ pkg-config python wget xutils-dev +RUN git clone -b dbus-1.12.2 --depth 1 https://gitlab.freedesktop.org/dbus/dbus.git && \ + cd dbus && \ + git reset --hard 0f0968336b9711349023e1d41f075b2bccf7c20b && \ + ./autogen.sh && \ + ./configure --disable-shared --enable-static --disable-systemd --disable-launchd --disable-selinux --disable-apparmor --disable-libaudit --disable-xml-docs --disable-doxygen-docs --disable-ducktype-docs --disable-modular-tests --disable-embedded-tests --disable-x11-autolaunch --without-x --with-systemdsystemunitdir=no --with-systemduserunitdir=no && \ + make -j$THREADS && \ + make -j$THREADS install && \ + rm -rf $(pwd) + RUN git clone -b xorgproto-2020.1 --depth 1 https://gitlab.freedesktop.org/xorg/proto/xorgproto && \ cd xorgproto && \ git reset --hard c62e8203402cafafa5ba0357b6d1c019156c9f36 && \ @@ -198,7 +207,7 @@ RUN rm /usr/lib/x86_64-linux-gnu/libX11.a && \ sed -i s/\\/usr\\/X11R6\\/lib64/\\/usr\\/local\\/lib/ qtbase/mkspecs/linux-g++-64/qmake.conf && \ ./configure --prefix=/usr -platform linux-g++-64 -opensource -confirm-license -release -static -no-avx \ -opengl desktop -qpa xcb -xcb -xcb-xlib -feature-xlib -system-freetype -fontconfig -glib \ - -no-dbus -no-feature-qml-worker-script -no-linuxfb -no-openssl -no-sql-sqlite -no-kms -no-use-gold-linker \ + -no-feature-qml-worker-script -no-linuxfb -no-openssl -no-sql-sqlite -no-kms -no-use-gold-linker \ -qt-harfbuzz -qt-libjpeg -qt-libpng -qt-pcre -qt-zlib \ -skip qt3d -skip qtandroidextras -skip qtcanvas3d -skip qtcharts -skip qtconnectivity -skip qtdatavis3d \ -skip qtdoc -skip qtgamepad -skip qtlocation -skip qtmacextras -skip qtnetworkauth -skip qtpurchasing \ diff --git a/src/main/oshelper.cpp b/src/main/oshelper.cpp index 5d189ac8ef..de442f58eb 100644 --- a/src/main/oshelper.cpp +++ b/src/main/oshelper.cpp @@ -54,6 +54,14 @@ #endif #if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) #include "qt/utils.h" +#include +#include +#include +#include +#include +#include +#include +#include #endif #include "QR-Code-scanner/Decoder.h" @@ -63,7 +71,7 @@ namespace { -QPixmap screenshot() +std::unordered_set hideVisibleWindows() { std::unordered_set hidden; const QWindowList windows = QGuiApplication::allWindows(); @@ -75,16 +83,43 @@ QPixmap screenshot() window->hide(); } } - const auto unhide = sg::make_scope_guard([&hidden]() { - for (QWindow *window : hidden) - { - window->show(); - } - }); + return hidden; +} + +void showWindows(const std::unordered_set &windows) +{ + for (QWindow *window : windows) + { + window->show(); + } +} + +#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) +bool isWayland() +{ + if (QGuiApplication::platformName().contains("wayland", Qt::CaseInsensitive)) + { + return true; + } + + if (!qEnvironmentVariableIsEmpty("WAYLAND_DISPLAY")) + { + return true; + } - return QGuiApplication::primaryScreen()->grabWindow(0); + return QString::fromLocal8Bit(qgetenv("XDG_SESSION_TYPE")).compare(QStringLiteral("wayland"), Qt::CaseInsensitive) == 0; } +QVariant unwrapDbusVariant(const QVariant &value) +{ + if (value.canConvert()) + { + return value.value().variant(); + } + return value; +} +#endif + } // namespace #if defined(Q_OS_WIN) @@ -119,6 +154,186 @@ OSHelper::OSHelper(QObject *parent) : QObject(parent) } +#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) +void OSHelper::resetScreenshotPortalResponse() +{ + m_screenshotPortalResponseReceived = false; + m_screenshotPortalResponse = 1; + m_screenshotPortalResults.clear(); +} + +void OSHelper::handleScreenshotPortalResponse(uint responseCode, const QVariantMap &responseResults) +{ + m_screenshotPortalResponseReceived = true; + m_screenshotPortalResponse = responseCode; + m_screenshotPortalResults = responseResults; + emit screenshotPortalResponseReceived(); +} + +QImage OSHelper::screenshotPortal() +{ + if (m_screenshotPortalInProgress) + { + qWarning() << "XDG screenshot portal request is already in progress"; + return QImage(); + } + + m_screenshotPortalInProgress = true; + const auto resetInProgress = sg::make_scope_guard([this]() { + m_screenshotPortalInProgress = false; + }); + + QDBusConnection sessionBus = QDBusConnection::sessionBus(); + QDBusInterface portal( + QStringLiteral("org.freedesktop.portal.Desktop"), + QStringLiteral("/org/freedesktop/portal/desktop"), + QStringLiteral("org.freedesktop.portal.Screenshot"), + sessionBus); + if (!portal.isValid()) + { + qWarning() << "XDG screenshot portal is unavailable:" << sessionBus.lastError().message(); + return QImage(); + } + + resetScreenshotPortalResponse(); + + const QString handleToken = QStringLiteral("monero_gui_%1_%2") + .arg(QRandomGenerator::global()->generate(), 8, 16, QLatin1Char('0')) + .arg(QRandomGenerator::global()->generate(), 8, 16, QLatin1Char('0')); + QString sender = sessionBus.baseService(); + if (sender.startsWith(QLatin1Char(':'))) + { + sender.remove(0, 1); + } + sender.replace(QLatin1Char('.'), QLatin1Char('_')); + if (sender.isEmpty()) + { + qWarning() << "XDG screenshot portal cannot determine DBus sender name"; + return QImage(); + } + + QString requestPath = QStringLiteral("/org/freedesktop/portal/desktop/request/%1/%2").arg(sender, handleToken); + if (!sessionBus.connect( + QStringLiteral("org.freedesktop.portal.Desktop"), + requestPath, + QStringLiteral("org.freedesktop.portal.Request"), + QStringLiteral("Response"), + this, + SLOT(handleScreenshotPortalResponse(uint,QVariantMap)))) + { + qWarning() << "Failed to connect to XDG screenshot portal response"; + return QImage(); + } + + const auto disconnectRequest = sg::make_scope_guard([this, sessionBus, &requestPath]() mutable { + sessionBus.disconnect( + QStringLiteral("org.freedesktop.portal.Desktop"), + requestPath, + QStringLiteral("org.freedesktop.portal.Request"), + QStringLiteral("Response"), + this, + SLOT(handleScreenshotPortalResponse(uint,QVariantMap))); + }); + + QVariantMap options; + options.insert(QStringLiteral("interactive"), true); + options.insert(QStringLiteral("modal"), true); + options.insert(QStringLiteral("handle_token"), handleToken); + + QDBusPendingReply reply = portal.asyncCallWithArgumentList( + QStringLiteral("Screenshot"), + QVariantList{QString(), options}); + reply.waitForFinished(); + if (reply.isError()) + { + qWarning() << "XDG screenshot portal request failed:" << reply.error().message(); + return QImage(); + } + + const QString returnedRequestPath = reply.value().path(); + if (returnedRequestPath != requestPath) + { + sessionBus.disconnect( + QStringLiteral("org.freedesktop.portal.Desktop"), + requestPath, + QStringLiteral("org.freedesktop.portal.Request"), + QStringLiteral("Response"), + this, + SLOT(handleScreenshotPortalResponse(uint,QVariantMap))); + + requestPath = returnedRequestPath; + if (!sessionBus.connect( + QStringLiteral("org.freedesktop.portal.Desktop"), + requestPath, + QStringLiteral("org.freedesktop.portal.Request"), + QStringLiteral("Response"), + this, + SLOT(handleScreenshotPortalResponse(uint,QVariantMap)))) + { + qWarning() << "Failed to connect to XDG screenshot portal response"; + return QImage(); + } + } + + QEventLoop loop; + QTimer timer; + timer.setSingleShot(true); + QObject::connect(this, SIGNAL(screenshotPortalResponseReceived()), &loop, SLOT(quit())); + QObject::connect(&timer, SIGNAL(timeout()), &loop, SLOT(quit())); + timer.start(60000); + + if (!m_screenshotPortalResponseReceived) + { + loop.exec(); + } + + if (!m_screenshotPortalResponseReceived) + { + qWarning() << "Timed out waiting for XDG screenshot portal response"; + return QImage(); + } + if (m_screenshotPortalResponse != 0) + { + qWarning() << "XDG screenshot portal request was not approved:" << m_screenshotPortalResponse; + return QImage(); + } + + const QVariant uriValue = unwrapDbusVariant(m_screenshotPortalResults.value(QStringLiteral("uri"))); + const QUrl uri(uriValue.toString()); + if (!uri.isLocalFile()) + { + qWarning() << "XDG screenshot portal returned an unsupported URI:" << uri; + return QImage(); + } + + const QString imagePath = uri.toLocalFile(); + QImage image(imagePath); + QFile::remove(imagePath); + if (image.isNull()) + { + qWarning() << "Failed to load XDG screenshot portal image:" << uri; + } + return image; +} +#endif + +QImage OSHelper::screenshot() +{ + const std::unordered_set hidden = hideVisibleWindows(); + const auto unhide = sg::make_scope_guard([&hidden]() { + showWindows(hidden); + }); + +#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) + if (isWayland()) + { + return screenshotPortal(); + } +#endif + + return QGuiApplication::primaryScreen()->grabWindow(0).toImage(); +} + void OSHelper::createDesktopEntry() const { #if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) @@ -131,13 +346,18 @@ QString OSHelper::downloadLocation() const return QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); } -QList OSHelper::grabQrCodesFromScreen() const +QList OSHelper::grabQrCodesFromScreen() { QList codes; try { - const QImage image = screenshot().toImage(); + const QImage image = screenshot(); + if (image.isNull()) + { + return codes; + } + const std::vector decoded = QrDecoder().decode(image); std::for_each(decoded.begin(), decoded.end(), [&codes](const std::string &code) { codes.push_back(QString::fromStdString(code)); diff --git a/src/main/oshelper.h b/src/main/oshelper.h index b6e98d3265..d686b1a655 100644 --- a/src/main/oshelper.h +++ b/src/main/oshelper.h @@ -30,8 +30,12 @@ #define OSHELPER_H #include +#include #include #include +#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) +#include +#endif /** * @brief The OSHelper class - exports to QML some OS-related functions */ @@ -45,7 +49,7 @@ class OSHelper : public QObject Q_INVOKABLE void createDesktopEntry() const; Q_INVOKABLE QString downloadLocation() const; - Q_INVOKABLE QList grabQrCodesFromScreen() const; + Q_INVOKABLE QList grabQrCodesFromScreen(); Q_INVOKABLE bool openFile(const QString &filePath) const; Q_INVOKABLE bool openContainingFolder(const QString &filePath) const; Q_INVOKABLE QString openSaveFileDialog(const QString &title, const QString &folder, const QString &filename) const; @@ -61,10 +65,26 @@ class OSHelper : public QObject static std::pair getNetworkTypeAndAddressFromFile(const QString &wallet); private: + QImage screenshot(); +#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) + QImage screenshotPortal(); + void resetScreenshotPortalResponse(); + + bool m_screenshotPortalResponseReceived = false; + uint m_screenshotPortalResponse = 1; + QVariantMap m_screenshotPortalResults; + bool m_screenshotPortalInProgress = false; +#endif signals: +#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) + void screenshotPortalResponseReceived(); +#endif -public slots: +private slots: +#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) + void handleScreenshotPortalResponse(uint responseCode, const QVariantMap &responseResults); +#endif }; #endif // OSHELPER_H