From 568f4e1900be16a8709fbee04da83351900a2d51 Mon Sep 17 00:00:00 2001 From: Mayank Suman Date: Sat, 6 Nov 2021 00:54:09 +0900 Subject: [PATCH 1/2] Use xdg-desktop-portal for screenshot on wayland --- cmake/templates/Projecteur.desktop.in | 4 + src/linuxdesktop.cc | 107 +++++++++++++++++++++++--- src/linuxdesktop.h | 38 ++++++++- src/main.cc | 1 + src/projecteurapp.cc | 3 +- 5 files changed, 139 insertions(+), 14 deletions(-) diff --git a/cmake/templates/Projecteur.desktop.in b/cmake/templates/Projecteur.desktop.in index 045810c0..f38d6850 100644 --- a/cmake/templates/Projecteur.desktop.in +++ b/cmake/templates/Projecteur.desktop.in @@ -6,3 +6,7 @@ GenericName=Linux/X11 application for the Logitech Spotlight device. Icon=projecteur Terminal=false Categories=Office;Presentation; +X-DBUS-StartupType=Unique +X-DBUS-ServiceName=org.projecteur.Projecteur +X-KDE-DBUS-Restricted-Interfaces=org.kde.kwin.Screenshot,org.kde.KWin.ScreenShot2 + diff --git a/src/linuxdesktop.cc b/src/linuxdesktop.cc index 72428b5c..ce277548 100644 --- a/src/linuxdesktop.cc +++ b/src/linuxdesktop.cc @@ -13,6 +13,7 @@ #include #include #include +#include #if HAS_Qt_DBus #include @@ -24,6 +25,61 @@ LOGGING_CATEGORY(desktop, "desktop") namespace { #if HAS_Qt_DBus // ----------------------------------------------------------------------------------------------- + // This function works. However, it is not user-friendly and automated. + // Please see https://github.com/flatpak/xdg-desktop-portal/issues/649 + QPixmap grabScreenDBusXDGdesktop() + { + QDBusInterface interface(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(), NULL); + + QEventLoop loop; + QPixmap pm; + const auto gotSignal = [&pm, &loop](uint status, const QVariantMap& map) { + if (status == 0) + { + QString uri = map.value("uri").toString().remove(0, 7); + pm = QPixmap(uri); + pm.setDevicePixelRatio(qApp->devicePixelRatio()); + QFile imgFile(uri); + imgFile.remove(); + } + loop.quit(); + }; + + // prevent racy situations and listen before calling screenshot + QMetaObject::Connection conn = QObject::connect( + request, &org::freedesktop::portal::Request::Response, gotSignal); + + interface.call(QStringLiteral("Screenshot"), + "", + QMap({ { "handle_token", QVariant(token) }, + { "interactive", QVariant(false) } })); + + loop.exec(); + QObject::disconnect(conn); + request->Close().waitForFinished(); + request->deleteLater(); + + if (pm.isNull()) + { + logError(desktop) << LinuxDesktop::tr("Screenshot via DBus interface failed."); + return QPixmap(); + } + return pm; + } + // ----------------------------------------------------------------------------------------------- + // This function do not work in Gnome 41+. Remove this in future as grabScreenDBusXDGdesktop is + // more universal way of capturing screen on wayland. QPixmap grabScreenDBusGnome() { const auto filepath = QDir::temp().absoluteFilePath("000_projecteur_zoom_screenshot.png"); @@ -84,6 +140,21 @@ namespace { } } // end anonymous namespace + +OrgFreedesktopPortalRequestInterface::OrgFreedesktopPortalRequestInterface( + const QString& service, + const QString& path, + const QDBusConnection& connection, + QObject* parent) + : QDBusAbstractInterface(service, + path, + "org.freedesktop.portal.Request", + connection, + parent) +{} + +OrgFreedesktopPortalRequestInterface::~OrgFreedesktopPortalRequestInterface() {} + LinuxDesktop::LinuxDesktop(QObject* parent) : QObject(parent) { @@ -91,14 +162,17 @@ LinuxDesktop::LinuxDesktop(QObject* parent) { // check for Kde and Gnome const auto kdeFullSession = env.value(QStringLiteral("KDE_FULL_SESSION")); const auto gnomeSessionId = env.value(QStringLiteral("GNOME_DESKTOP_SESSION_ID")); - const auto desktopSession = env.value(QStringLiteral("DESKTOP_SESSION")); const auto xdgCurrentDesktop = env.value(QStringLiteral("XDG_CURRENT_DESKTOP")); + if (gnomeSessionId.size() || xdgCurrentDesktop.contains("Gnome", Qt::CaseInsensitive)) { m_type = LinuxDesktop::Type::Gnome; } - else if (kdeFullSession.size() || desktopSession == "kde-plasma") { + else if (kdeFullSession.size() || xdgCurrentDesktop.contains("kde-plasma", Qt::CaseInsensitive)) { m_type = LinuxDesktop::Type::KDE; } + else if (xdgCurrentDesktop.contains(QLatin1String("sway"), Qt::CaseInsensitive)) { + m_type = LinuxDesktop::Type::Sway; + } } { // check for wayland session @@ -137,22 +211,33 @@ QPixmap LinuxDesktop::grabScreenWayland(QScreen* screen) const { #if HAS_Qt_DBus QPixmap pm; - switch (type()) + if (type() == LinuxDesktop::Type::Gnome) { - case LinuxDesktop::Type::Gnome: pm = grabScreenDBusGnome(); - break; - case LinuxDesktop::Type::KDE: + } else if (type() == LinuxDesktop::Type::KDE) + { pm = grabScreenDBusKde(); - break; - default: - logWarning(desktop) << tr("Currently zoom on Wayland is only supported via DBus on KDE and GNOME."); + } + + // grabScreenDBusGnome may fail with Gnome 41+. Use xdg-desktop-portal + // grab function for any wayland compositor. However this function is + // not used as default as it is not user friendly. Please see + // https://github.com/flatpak/xdg-desktop-portal/issues/649 + // If the PixelMap remain Null after this step then the compositor + // is not supported. + if (pm.isNull()) + { + pm = grabScreenDBusXDGdesktop(); + } + + if (pm.isNull()) + { + logWarning(desktop) << tr("Currently zoom on Wayland is only supported via DBus on KDE, GNOME and Sway."); } return pm.isNull() ? pm : pm.copy(screen->geometry()); #else Q_UNUSED(screen); - logWarning(desktop) << tr("Projecteur was compiled without Qt DBus. Currently zoom on Wayland is " - "only supported via DBus on KDE and GNOME."); + logWarning(desktop) << tr("Projecteur was compiled without Qt DBus."); return QPixmap(); #endif } diff --git a/src/linuxdesktop.h b/src/linuxdesktop.h index 1c9048a1..ef9747ef 100644 --- a/src/linuxdesktop.h +++ b/src/linuxdesktop.h @@ -4,6 +4,7 @@ #include #include +#include class QScreen; @@ -12,7 +13,7 @@ class LinuxDesktop : public QObject Q_OBJECT public: - enum class Type : uint8_t { KDE, Gnome, Other }; + enum class Type : uint8_t { KDE, Gnome, Sway, Other }; explicit LinuxDesktop(QObject* parent = nullptr); @@ -26,4 +27,37 @@ class LinuxDesktop : public QObject Type m_type = Type::Other; QPixmap grabScreenWayland(QScreen* screen) const; -}; \ No newline at end of file +}; + +/* + * 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; +} +} +} diff --git a/src/main.cc b/src/main.cc index fe2985e9..6d5aa41f 100644 --- a/src/main.cc +++ b/src/main.cc @@ -328,6 +328,7 @@ namespace { // ------------------------------------------------------------------------------------------------- int main(int argc, char *argv[]) { + QCoreApplication::setOrganizationName("projecteur"); QCoreApplication::setApplicationName("Projecteur"); QCoreApplication::setApplicationVersion(projecteur::version_string()); ProjecteurApplication::Options options; diff --git a/src/projecteurapp.cc b/src/projecteurapp.cc index f1e016e3..945cd5bb 100644 --- a/src/projecteurapp.cc +++ b/src/projecteurapp.cc @@ -78,7 +78,8 @@ ProjecteurApplication::ProjecteurApplication(int &argc, char **argv, const Optio }); const QString desktopEnv = m_linuxDesktop->type() == LinuxDesktop::Type::KDE ? "KDE" : - m_linuxDesktop->type() == LinuxDesktop::Type::Gnome ? "Gnome" + m_linuxDesktop->type() == LinuxDesktop::Type::Gnome ? "Gnome": + m_linuxDesktop->type() == LinuxDesktop::Type::Sway ? "Sway" : tr("Unknown"); logDebug(mainapp) << tr("Qt platform plugin: %1;").arg(QGuiApplication::platformName()) From 277707c1f45d7cd5cf0f1c06304fd61a1799fa60 Mon Sep 17 00:00:00 2001 From: Mayank Suman Date: Sat, 6 Nov 2021 19:56:13 +0900 Subject: [PATCH 2/2] Add parent to QMenu In wayland, every QMenu should have its parent. Please see https://community.kde.org/Guidelines_and_HOWTOs/Wayland_Porting_Notes. --- src/projecteurapp.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/projecteurapp.cc b/src/projecteurapp.cc index 945cd5bb..4792429c 100644 --- a/src/projecteurapp.cc +++ b/src/projecteurapp.cc @@ -44,7 +44,7 @@ namespace { ProjecteurApplication::ProjecteurApplication(int &argc, char **argv, const Options& options) : QApplication(argc, argv) , m_trayIcon(new QSystemTrayIcon()) - , m_trayMenu(new QMenu()) + , m_trayMenu(new QMenu(qobject_cast(m_trayIcon.get()))) , m_localServer(new QLocalServer(this)) , m_linuxDesktop(new LinuxDesktop(this)) , m_xcbOnWayland(QGuiApplication::platformName() == "xcb" && m_linuxDesktop->isWayland())