From 5833461e711c76697907867d20d32825d95d13aa Mon Sep 17 00:00:00 2001 From: parhelia512 <0011d3@gmail.com> Date: Wed, 9 Apr 2025 22:04:00 -0400 Subject: [PATCH] Add Wayland support (#353) * Fix deploy_linux64.sh Add missing QTlsBackendOpenSSLPlugin. * Add Wayland support * Fix Wayland support * Fix Wayland support * Revert "Fix deploy_linux64.sh" This reverts commit 2a6779f5262108de2c91b0d00d72b54035eb2194. * Update mainwindow.cpp * Force QT_QPA_PLATFORM=xcb on Linux Desktop --- .github/workflows/build.yml | 18 ++--- CMakeLists.txt | 4 +- cmake/linux/linux.cmake | 2 +- include/sys/linux/desktopinfo.h | 31 +++++++++ include/ui/mainwindow.h | 38 +++++++++++ src/main.cpp | 9 +++ src/sys/linux/desktopinfo.cpp | 54 +++++++++++++++ src/ui/mainwindow.cpp | 115 +++++++++++++++++++++++++++----- 8 files changed, 244 insertions(+), 27 deletions(-) create mode 100644 include/sys/linux/desktopinfo.h create mode 100644 src/sys/linux/desktopinfo.cpp diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 90932fe..66cd08a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,13 +4,13 @@ on: workflow_dispatch: inputs: tag: - description: 'Release Tag' + description: "Release Tag" required: true publish: - description: 'Publish: If want ignore' + description: "Publish: If want ignore" required: false artifact-pack: - description: 'artifact-pack: If want ignore' + description: "artifact-pack: If want ignore" required: false jobs: build-go: @@ -141,12 +141,12 @@ jobs: shell: bash if: matrix.platform == 'ubuntu-22.04' run: | - sudo apt update && sudo apt upgrade -y - mkdir build - cd build - cmake -GNinja -DQT_VERSION_MAJOR=6 -DCMAKE_BUILD_TYPE=Release .. - ninja - cd .. + sudo apt --fix-broken update && sudo apt upgrade -y + mkdir build + cd build + cmake -GNinja -DQT_VERSION_MAJOR=6 -DCMAKE_BUILD_TYPE=Release .. + ninja + cd .. ./script/deploy_linux64.sh - name: macOS - Generate MakeFile and Build shell: bash diff --git a/CMakeLists.txt b/CMakeLists.txt index 273f810..9e399ee 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,7 +11,7 @@ if (NOT CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND WIN32) set(CMAKE_INTERPROCEDURAL_OPTIMIZATION ON) endif () -find_package(Qt6 REQUIRED COMPONENTS Widgets Network LinguistTools Charts) +find_package(Qt6 REQUIRED COMPONENTS Widgets Network LinguistTools Charts DBus) if (NKR_CROSS) set_property(TARGET Qt6::moc PROPERTY IMPORTED_LOCATION /usr/bin/moc) @@ -259,7 +259,7 @@ target_sources(nekoray PRIVATE ${CMAKE_BINARY_DIR}/translations.qrc) # Target Link target_link_libraries(nekoray PRIVATE - Qt6::Widgets Qt6::Network Qt6::Charts + Qt6::Widgets Qt6::Network Qt6::Charts Qt6::DBus Threads::Threads ${NKR_EXTERNAL_TARGETS} ${PLATFORM_LIBRARIES} diff --git a/cmake/linux/linux.cmake b/cmake/linux/linux.cmake index a4dc9f5..c653a1d 100644 --- a/cmake/linux/linux.cmake +++ b/cmake/linux/linux.cmake @@ -1,2 +1,2 @@ -set(PLATFORM_SOURCES src/sys/linux/LinuxCap.cpp) +set(PLATFORM_SOURCES src/sys/linux/LinuxCap.cpp src/sys/linux/desktopinfo.cpp) set(PLATFORM_LIBRARIES dl) diff --git a/include/sys/linux/desktopinfo.h b/include/sys/linux/desktopinfo.h new file mode 100644 index 0000000..2daa226 --- /dev/null +++ b/include/sys/linux/desktopinfo.h @@ -0,0 +1,31 @@ +#pragma once + +#include + +class DesktopInfo +{ +public: + DesktopInfo(); + + enum WM + { + GNOME, + KDE, + OTHER, + QTILE, + SWAY, + HYPRLAND + }; + + bool waylandDetected(); + WM windowManager(); + +private: + QString XDG_CURRENT_DESKTOP; + QString XDG_SESSION_TYPE; + QString WAYLAND_DISPLAY; + QString KDE_FULL_SESSION; + QString GNOME_DESKTOP_SESSION_ID; + QString GDMSESSION; + QString DESKTOP_SESSION; +}; diff --git a/include/ui/mainwindow.h b/include/ui/mainwindow.h index ced9916..21369ec 100644 --- a/include/ui/mainwindow.h +++ b/include/ui/mainwindow.h @@ -5,6 +5,9 @@ #include "include/global/NekoGui.hpp" #include "include/stats/connections/connectionLister.hpp" #include "utils/TrafficChart.h" +#ifdef Q_OS_LINUX +#include +#endif #ifndef MW_INTERFACE @@ -243,3 +246,38 @@ inline MainWindow *GetMainWindow() { } void UI_InitMainWindow(); + +#ifdef Q_OS_LINUX +/* + * Proxy class for interface org.freedesktop.portal.Request + */ +class OrgFreedesktopPortalRequestInterface : public QDBusAbstractInterface +{ + Q_OBJECT +public: + OrgFreedesktopPortalRequestInterface(const QString& service, + const QString& path, + const QDBusConnection& connection, + QObject* parent = nullptr); + + ~OrgFreedesktopPortalRequestInterface(); + +public Q_SLOTS: + inline QDBusPendingReply<> Close() + { + QList argumentList; + return asyncCallWithArgumentList(QStringLiteral("Close"), argumentList); + } + +Q_SIGNALS: // SIGNALS + void Response(uint response, QVariantMap results); +}; + +namespace org { +namespace freedesktop { +namespace portal { +typedef ::OrgFreedesktopPortalRequestInterface Request; +} +} +} +#endif diff --git a/src/main.cpp b/src/main.cpp index edfc646..cae2f74 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -20,6 +20,9 @@ #include "include/sys/windows/vcCheck.h" #include "include/sys/windows/eventHandler.h" #endif +#ifdef Q_OS_LINUX +#include "include/sys/linux/desktopinfo.h" +#endif void signal_handler(int signum) { if (GetMainWindow()) { @@ -60,6 +63,12 @@ int main(int argc, char* argv[]) { #ifdef Q_OS_WIN Windows_SetCrashHandler(); #endif +#ifdef Q_OS_LINUX + DesktopInfo info; + if (info.waylandDetected()) { + qputenv("QT_QPA_PLATFORM", "xcb"); + } +#endif QApplication::setAttribute(Qt::AA_DontUseNativeDialogs); QApplication::setQuitOnLastWindowClosed(false); diff --git a/src/sys/linux/desktopinfo.cpp b/src/sys/linux/desktopinfo.cpp new file mode 100644 index 0000000..7225dd0 --- /dev/null +++ b/src/sys/linux/desktopinfo.cpp @@ -0,0 +1,54 @@ +#include "include/sys/linux/desktopinfo.h" +#include + +DesktopInfo::DesktopInfo() +{ + auto e = QProcessEnvironment::systemEnvironment(); + XDG_CURRENT_DESKTOP = e.value(QStringLiteral("XDG_CURRENT_DESKTOP")); + XDG_SESSION_TYPE = e.value(QStringLiteral("XDG_SESSION_TYPE")); + WAYLAND_DISPLAY = e.value(QStringLiteral("WAYLAND_DISPLAY")); + KDE_FULL_SESSION = e.value(QStringLiteral("KDE_FULL_SESSION")); + GNOME_DESKTOP_SESSION_ID = + e.value(QStringLiteral("GNOME_DESKTOP_SESSION_ID")); + DESKTOP_SESSION = e.value(QStringLiteral("DESKTOP_SESSION")); +} + +bool DesktopInfo::waylandDetected() +{ + return XDG_SESSION_TYPE == QLatin1String("wayland") || + WAYLAND_DISPLAY.contains(QLatin1String("wayland"), + Qt::CaseInsensitive); +} + +DesktopInfo::WM DesktopInfo::windowManager() +{ + DesktopInfo::WM res = DesktopInfo::OTHER; + QStringList desktops = XDG_CURRENT_DESKTOP.split(QChar(':')); + for (auto& desktop : desktops) { + if (desktop.contains(QLatin1String("GNOME"), Qt::CaseInsensitive)) { + return DesktopInfo::GNOME; + } + if (desktop.contains(QLatin1String("qtile"), Qt::CaseInsensitive)) { + return DesktopInfo::QTILE; + } + if (desktop.contains(QLatin1String("sway"), Qt::CaseInsensitive)) { + return DesktopInfo::SWAY; + } + if (desktop.contains(QLatin1String("Hyprland"), Qt::CaseInsensitive)) { + return DesktopInfo::HYPRLAND; + } + if (desktop.contains(QLatin1String("kde-plasma"))) { + return DesktopInfo::KDE; + } + } + + if (!GNOME_DESKTOP_SESSION_ID.isEmpty()) { + return DesktopInfo::GNOME; + } + + if (!KDE_FULL_SESSION.isEmpty()) { + return DesktopInfo::KDE; + } + + return res; +} diff --git a/src/ui/mainwindow.cpp b/src/ui/mainwindow.cpp index 9cbd170..75e7599 100644 --- a/src/ui/mainwindow.cpp +++ b/src/ui/mainwindow.cpp @@ -25,6 +25,10 @@ #else #ifdef Q_OS_LINUX #include "include/sys/linux/LinuxCap.h" +#include "include/sys/linux/desktopinfo.h" +#include +#include +#include #endif #include #endif @@ -1581,6 +1585,84 @@ void MainWindow::display_qr_link(bool nkrFormat) { w->deleteLater(); } +#ifdef Q_OS_LINUX +OrgFreedesktopPortalRequestInterface::OrgFreedesktopPortalRequestInterface( + const QString& service, + const QString& path, + const QDBusConnection& connection, + QObject* parent) + : QDBusAbstractInterface(service, + path, + "org.freedesktop.portal.Request", + connection, + parent) +{} + +OrgFreedesktopPortalRequestInterface::~OrgFreedesktopPortalRequestInterface() {} +#endif + +QPixmap grabScreen(QScreen* screen, bool& ok) +{ + QPixmap p; + QRect geom = screen->geometry(); +#ifdef Q_OS_LINUX + DesktopInfo m_info; + if (m_info.waylandDetected()) { + QDBusInterface screenshotInterface( + QStringLiteral("org.freedesktop.portal.Desktop"), + QStringLiteral("/org/freedesktop/portal/desktop"), + QStringLiteral("org.freedesktop.portal.Screenshot")); + + // unique token + QString token = + QUuid::createUuid().toString().remove('-').remove('{').remove('}'); + + // premake interface + auto* request = new OrgFreedesktopPortalRequestInterface( + QStringLiteral("org.freedesktop.portal.Desktop"), + "/org/freedesktop/portal/desktop/request/" + + QDBusConnection::sessionBus().baseService().remove(':').replace('.','_') + + "/" + token, + QDBusConnection::sessionBus()); + + QEventLoop loop; + const auto gotSignal = [&p, &loop](uint status, const QVariantMap& map) { + if (status == 0) { + // Parse this as URI to handle unicode properly + QUrl uri = map.value("uri").toString(); + QString uriString = uri.toLocalFile(); + p = QPixmap(uriString); + p.setDevicePixelRatio(qApp->devicePixelRatio()); + QFile imgFile(uriString); + imgFile.remove(); + } + loop.quit(); + }; + + // prevent racy situations and listen before calling screenshot + QMetaObject::Connection conn = QObject::connect( + request, &org::freedesktop::portal::Request::Response, gotSignal); + + screenshotInterface.call( + QStringLiteral("Screenshot"), + "", + QMap({ { "handle_token", QVariant(token) }, + { "interactive", QVariant(false) } })); + + loop.exec(); + QObject::disconnect(conn); + request->Close().waitForFinished(); + request->deleteLater(); + + if (p.isNull()) { + ok = false; + } + return p; + } else +#endif + return screen->grabWindow(0, geom.x(), geom.y(), geom.width(), geom.height()); +} + void MainWindow::on_menu_scan_qr_triggered() { #ifndef NKR_NO_ZXING using namespace ZXingQt; @@ -1588,24 +1670,27 @@ void MainWindow::on_menu_scan_qr_triggered() { hide(); QThread::sleep(1); - auto screen = QGuiApplication::primaryScreen(); - auto geom = screen->geometry(); - auto qpx = screen->grabWindow(0, geom.x(), geom.y(), geom.width(), geom.height()); + bool ok = true; + QPixmap qpx(grabScreen(QGuiApplication::primaryScreen(), ok)); show(); + if (ok) { + auto hints = DecodeHints() + .setFormats(BarcodeFormat::QRCode) + .setTryRotate(false) + .setBinarizer(Binarizer::FixedThreshold); - auto hints = DecodeHints() - .setFormats(BarcodeFormat::QRCode) - .setTryRotate(false) - .setBinarizer(Binarizer::FixedThreshold); - - auto result = ReadBarcode(qpx.toImage(), hints); - const auto &text = result.text(); - if (text.isEmpty()) { - MessageBoxInfo(software_name, tr("QR Code not found")); - } else { - show_log_impl("QR Code Result:\n" + text); - NekoGui_sub::groupUpdater->AsyncUpdate(text); + auto result = ReadBarcode(qpx.toImage(), hints); + const auto &text = result.text(); + if (text.isEmpty()) { + MessageBoxInfo(software_name, tr("QR Code not found")); + } else { + show_log_impl("QR Code Result:\n" + text); + NekoGui_sub::groupUpdater->AsyncUpdate(text); + } + } + else { + MessageBoxInfo(software_name, tr("Unable to capture screen")); } #endif }