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 2a6779f526.

* Update mainwindow.cpp

* Force QT_QPA_PLATFORM=xcb on Linux Desktop
This commit is contained in:
parhelia512 2025-04-09 22:04:00 -04:00 committed by GitHub
parent 0cbfee2dd1
commit 5833461e71
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 244 additions and 27 deletions

View File

@ -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

View File

@ -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}

View File

@ -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)

View File

@ -0,0 +1,31 @@
#pragma once
#include <QString>
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;
};

View File

@ -5,6 +5,9 @@
#include "include/global/NekoGui.hpp"
#include "include/stats/connections/connectionLister.hpp"
#include "utils/TrafficChart.h"
#ifdef Q_OS_LINUX
#include <QtDBus>
#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<QVariant> 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

View File

@ -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);

View File

@ -0,0 +1,54 @@
#include "include/sys/linux/desktopinfo.h"
#include <QProcessEnvironment>
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;
}

View File

@ -25,6 +25,10 @@
#else
#ifdef Q_OS_LINUX
#include "include/sys/linux/LinuxCap.h"
#include "include/sys/linux/desktopinfo.h"
#include <QDBusInterface>
#include <QDBusReply>
#include <QUuid>
#endif
#include <unistd.h>
#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<QString, QVariant>({ { "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
}