From f0fc8f14562d9b5fd7c75b540385b388ba8a4832 Mon Sep 17 00:00:00 2001 From: iphydf Date: Mon, 20 Jan 2025 17:47:09 +0000 Subject: [PATCH] feat(Screenshot): Add Freedesktop portal screenshot support. This uses the system implementation of a screenshot grabber, which works in sandboxes and on wayland. On systems with a desktop environment, this will fix issues with dual-screen, wayland, and Flatpak. On more barebones systems, this should make no difference. --- CMakeLists.txt | 5 + src/BUILD.bazel | 2 + src/platform/screenshot.cpp | 18 ++ src/platform/screenshot.h | 20 +++ src/platform/screenshot_dbus.cpp | 162 ++++++++++++++++++ src/platform/screenshot_dbus.h | 40 +++++ src/widget/form/chatform.cpp | 17 +- src/widget/form/chatform.h | 3 +- src/widget/form/settings/avform.cpp | 7 +- src/widget/tool/abstractscreenshotgrabber.cpp | 13 ++ src/widget/tool/abstractscreenshotgrabber.h | 26 +++ src/widget/tool/screenshotgrabber.cpp | 4 +- src/widget/tool/screenshotgrabber.h | 14 +- 13 files changed, 309 insertions(+), 22 deletions(-) create mode 100644 src/platform/screenshot.cpp create mode 100644 src/platform/screenshot.h create mode 100644 src/platform/screenshot_dbus.cpp create mode 100644 src/platform/screenshot_dbus.h create mode 100644 src/widget/tool/abstractscreenshotgrabber.cpp create mode 100644 src/widget/tool/abstractscreenshotgrabber.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 4d630b98c1..73d491455c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -447,6 +447,9 @@ set(${PROJECT_NAME}_SOURCES src/platform/desktop_notifications/desktopnotifybackend.h src/platform/posixsignalnotifier.cpp src/platform/posixsignalnotifier.h + src/platform/screenshot_dbus.cpp + src/platform/screenshot.cpp + src/platform/screenshot.h src/platform/stacktrace.cpp src/platform/stacktrace.h src/platform/timer.h @@ -578,6 +581,8 @@ set(${PROJECT_NAME}_SOURCES src/widget/splitterrestorer.h src/widget/style.cpp src/widget/style.h + src/widget/tool/abstractscreenshotgrabber.cpp + src/widget/tool/abstractscreenshotgrabber.h src/widget/tool/activatedialog.cpp src/widget/tool/activatedialog.h src/widget/tool/adjustingscrollarea.cpp diff --git a/src/BUILD.bazel b/src/BUILD.bazel index 7a279f4b71..d1fcdcd441 100644 --- a/src/BUILD.bazel +++ b/src/BUILD.bazel @@ -60,6 +60,7 @@ qt_moc( "platform/desktop_notifications/desktopnotify.h", "platform/desktop_notifications/desktopnotifybackend.h", "platform/posixsignalnotifier.h", + "platform/screenshot_dbus.h", "video/camerasource.h", "video/corevideosource.h", "video/netcamview.h", @@ -110,6 +111,7 @@ qt_moc( "widget/qrwidget.h", "widget/searchform.h", "widget/style.h", + "widget/tool/abstractscreenshotgrabber.h", "widget/tool/activatedialog.h", "widget/tool/adjustingscrollarea.h", "widget/tool/callconfirmwidget.h", diff --git a/src/platform/screenshot.cpp b/src/platform/screenshot.cpp new file mode 100644 index 0000000000..764a5eb309 --- /dev/null +++ b/src/platform/screenshot.cpp @@ -0,0 +1,18 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright © 2025 The TokTok team. + */ + +#include "screenshot.h" + +#include "screenshot_dbus.h" +#include "src/widget/tool/abstractscreenshotgrabber.h" + +AbstractScreenshotGrabber* Platform::createScreenshotGrabber(QWidget* parent) +{ +#if QT_CONFIG(dbus) + return DBusScreenshotGrabber::create(parent); +#else + std::ignore = parent; + return nullptr; +#endif +} diff --git a/src/platform/screenshot.h b/src/platform/screenshot.h new file mode 100644 index 0000000000..5d2c115ff7 --- /dev/null +++ b/src/platform/screenshot.h @@ -0,0 +1,20 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright © 2025 The TokTok team. + */ + +#pragma once + +class AbstractScreenshotGrabber; +class QWidget; + +namespace Platform { +/** + * @brief Create a platform-dependent screenshot grabber. + * + * If no platform-specific screenshot grabber is available, this function returns nullptr. + * The caller should then create a default screenshot grabber. + * + * @param parent The parent widget for the screenshot grabber. + */ +AbstractScreenshotGrabber* createScreenshotGrabber(QWidget* parent); +} // namespace Platform diff --git a/src/platform/screenshot_dbus.cpp b/src/platform/screenshot_dbus.cpp new file mode 100644 index 0000000000..1e51a43d7b --- /dev/null +++ b/src/platform/screenshot_dbus.cpp @@ -0,0 +1,162 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright © 2025 The TokTok team. + */ + +#include "screenshot_dbus.h" + +#include + +#if QT_CONFIG(dbus) +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace { +const QString PORTAL_DBUS_NAME = QStringLiteral("org.freedesktop.portal.Desktop"); +const QString PORTAL_DBUS_CORE_OBJECT = QStringLiteral("/org/freedesktop/portal/desktop"); +const QString PORTAL_DBUS_REQUEST_INTERFACE = QStringLiteral("org.freedesktop.portal.Request"); +const QString PORTAL_DBUS_SCREENSHOT_INTERFACE = + QStringLiteral("org.freedesktop.portal.Screenshot"); + +QString winIdString(WId winId) +{ + // If we're wayland, return wayland:$id. + if (QGuiApplication::platformName() == QStringLiteral("wayland")) { + return QStringLiteral("wayland:%1").arg(winId); + } + if (QGuiApplication::platformName() == QStringLiteral("xcb")) { + return QStringLiteral("x11:0x%1").arg(winId, 0, 16); + } + qWarning() << "Unknown platform:" << QGuiApplication::platformName() << "cannot take screenshots"; + return {}; +} +} // namespace + +struct DBusScreenshotGrabber::Data +{ + QDBusInterface portal; + WId winId; + QString path; + + explicit Data(QWidget* parent) + : portal(PORTAL_DBUS_NAME, PORTAL_DBUS_CORE_OBJECT, PORTAL_DBUS_SCREENSHOT_INTERFACE, + QDBusConnection::sessionBus(), parent) + , winId{parent->topLevelWidget()->winId()} + { + } + + QString grabScreen() + { + if (!portal.connection().isConnected()) { + qWarning() << "DBus connection failed"; + return {}; + } + + if (!portal.isValid()) { + qWarning() << "Failed to connect to org.freedesktop.portal.Screenshot"; + return {}; + } + + const QString wid = winIdString(winId); + if (wid.isEmpty()) { + return {}; + } + + const QDBusMessage reply = portal.call(QStringLiteral("Screenshot"), wid, + QVariantMap{ + {"modal", true}, + {"interactive", true}, + {"handle_token", "1"}, + }); + + if (reply.type() == QDBusMessage::ErrorMessage) { + qWarning() << "Failed to take screenshot:" << reply.errorMessage(); + return {}; + } + + return reply.arguments().value(0).value().path(); + } +}; + +DBusScreenshotGrabber::DBusScreenshotGrabber(QWidget* parent) + : AbstractScreenshotGrabber(parent) + , d(std::make_unique(parent)) +{ +} + +DBusScreenshotGrabber::~DBusScreenshotGrabber() = default; + +DBusScreenshotGrabber* DBusScreenshotGrabber::create(QWidget* parent) +{ + auto grabber = std::make_unique(parent); + if (!grabber->isValid()) { + return nullptr; + } + + const QString path = grabber->d->grabScreen(); + if (path.isEmpty()) { + // Some connection problem or unsupported platform. We're not trying this again. + return nullptr; + } + + qDebug() << "Opened screenshot dialog; waiting for response on" << path; + QDBusConnection::sessionBus().connect( + // org.freedesktop.portal.Request::Response + PORTAL_DBUS_NAME, path, PORTAL_DBUS_REQUEST_INTERFACE, QStringLiteral("Response"), + grabber.get(), SLOT(screenshotResponse(uint, QVariantMap))); + + return grabber.release(); +} + +bool DBusScreenshotGrabber::isValid() const +{ + return d->portal.isValid(); +} + +void DBusScreenshotGrabber::showGrabber() +{ + // Nothing to do here. We've done everything in create(). +} + +/** + * https://docs.flatpak.org/en/latest/portal-api-reference.html#gdbus-signal-org-freedesktop-portal-Request.Response + * + * Emitted when the user interaction for a portal request is over. + * + * The response indicates how the user interaction ended: + * + * 0: Success, the request is carried out + * 1: The user cancelled the interaction + * 2: The user interaction was ended in some other way + */ +void DBusScreenshotGrabber::screenshotResponse(uint response, QVariantMap results) +{ + switch (response) { + case 0: { + const QUrl uri{results[QStringLiteral("uri")].toString()}; + qDebug() << "Screenshot taken:" << uri.toString(); + emit screenshotTaken(QPixmap(uri.toLocalFile())); + break; + } + case 1: + qDebug() << "User cancelled screenshot request"; + emit rejected(); + break; + default: + qWarning() << "Screenshot request failed:" << response; + emit rejected(); + break; + } + + // We're done, clean up. + deleteLater(); +} +#endif // QT_CONFIG(dbus) diff --git a/src/platform/screenshot_dbus.h b/src/platform/screenshot_dbus.h new file mode 100644 index 0000000000..e82b6de43d --- /dev/null +++ b/src/platform/screenshot_dbus.h @@ -0,0 +1,40 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright © 2025 The TokTok team. + */ + +#pragma once + +#include "src/widget/tool/abstractscreenshotgrabber.h" + +#include + +#include + +#if QT_CONFIG(dbus) +/** @brief Grabs screenshot using org.freedesktop.portal.Screenshot. + * + * https://docs.flatpak.org/en/latest/portal-api-reference.html#gdbus-org.freedesktop.portal.Screenshot + */ +class DBusScreenshotGrabber : public AbstractScreenshotGrabber +{ + Q_OBJECT + +public: + // Don't use these directly, use create() instead. + explicit DBusScreenshotGrabber(QWidget* parent); + ~DBusScreenshotGrabber() override; + + // Create a screenshot grabber. Returns nullptr if no DBus connection could be established. + static DBusScreenshotGrabber* create(QWidget* parent); + + bool isValid() const; + void showGrabber() override; + +private slots: + void screenshotResponse(uint response, QVariantMap results); + +private: + struct Data; + std::unique_ptr d; +}; +#endif // QT_CONFIG(dbus) diff --git a/src/widget/form/chatform.cpp b/src/widget/form/chatform.cpp index 00a39b4a94..2097ba703a 100644 --- a/src/widget/form/chatform.cpp +++ b/src/widget/form/chatform.cpp @@ -4,7 +4,6 @@ */ #include "chatform.h" -#include "src/chatlog/chatlinecontentproxy.h" #include "src/chatlog/chatmessage.h" #include "src/chatlog/chatwidget.h" #include "src/chatlog/content/filetransferwidget.h" @@ -16,17 +15,17 @@ #include "src/model/status.h" #include "src/nexus.h" #include "src/persistence/history.h" -#include "src/persistence/offlinemsgengine.h" #include "src/persistence/profile.h" #include "src/persistence/settings.h" +#include "src/platform/screenshot.h" #include "src/video/netcamview.h" #include "src/widget/chatformheader.h" #include "src/widget/contentdialogmanager.h" #include "src/widget/form/loadhistorydialog.h" #include "src/widget/imagepreviewwidget.h" -#include "src/widget/maskablepixmapwidget.h" #include "src/widget/searchform.h" #include "src/widget/style.h" +#include "src/widget/tool/abstractscreenshotgrabber.h" #include "src/widget/tool/callconfirmwidget.h" #include "src/widget/tool/chattextedit.h" #include "src/widget/tool/croppinglabel.h" @@ -36,6 +35,7 @@ #include #include +#include #include #include #include @@ -567,11 +567,14 @@ void ChatForm::onScreenshotClicked() QTimer::singleShot(SCREENSHOT_GRABBER_OPENING_DELAY, this, &ChatForm::hideFileMenu); } -void ChatForm::doScreenshot() const +void ChatForm::doScreenshot() { - // note: grabber is self-managed and will destroy itself when done - ScreenshotGrabber* grabber = new ScreenshotGrabber; - connect(grabber, &ScreenshotGrabber::screenshotTaken, this, &ChatForm::previewImage); + // Note: grabber is self-managed and will destroy itself when done. + AbstractScreenshotGrabber* grabber = Platform::createScreenshotGrabber(this); + if (grabber == nullptr) { + grabber = new ScreenshotGrabber(this); + } + connect(grabber, &AbstractScreenshotGrabber::screenshotTaken, this, &ChatForm::previewImage); grabber->showGrabber(); } diff --git a/src/widget/form/chatform.h b/src/widget/form/chatform.h index cd000f7d49..aa60927550 100644 --- a/src/widget/form/chatform.h +++ b/src/widget/form/chatform.h @@ -16,7 +16,6 @@ #include "src/model/status.h" #include "src/persistence/history.h" #include "src/video/netcamview.h" -#include "src/widget/tool/screenshotgrabber.h" class CallConfirmWidget; class ContentDialogManager; @@ -97,7 +96,7 @@ private slots: void previewImage(const QPixmap& pixmap); void cancelImagePreview(); void sendImageFromPreview(); - void doScreenshot() const; + void doScreenshot(); void onCopyStatusMessage(); void callUpdateFriendActivity(); diff --git a/src/widget/form/settings/avform.cpp b/src/widget/form/settings/avform.cpp index e6b5546762..51f9da2ce4 100644 --- a/src/widget/form/settings/avform.cpp +++ b/src/widget/form/settings/avform.cpp @@ -194,8 +194,11 @@ void AVForm::on_videoModesComboBox_currentIndexChanged(int index) open(devName, mode); }; - // note: grabber is self-managed and will destroy itself when done - ScreenshotGrabber* screenshotGrabber = new ScreenshotGrabber; + // We're not using the platform-specific grabber here, because all we want is a rectangle + // selection. We don't actually need a screenshot to be taken. + // + // Note: grabber is self-managed and will destroy itself when done. + ScreenshotGrabber* screenshotGrabber = new ScreenshotGrabber(this); connect(screenshotGrabber, &ScreenshotGrabber::regionChosen, this, onGrabbed, Qt::QueuedConnection); diff --git a/src/widget/tool/abstractscreenshotgrabber.cpp b/src/widget/tool/abstractscreenshotgrabber.cpp new file mode 100644 index 0000000000..457e8babff --- /dev/null +++ b/src/widget/tool/abstractscreenshotgrabber.cpp @@ -0,0 +1,13 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright © 2015-2019 by The qTox Project Contributors + * Copyright © 2024-2025 The TokTok team. + */ + +#include "abstractscreenshotgrabber.h" + +AbstractScreenshotGrabber::AbstractScreenshotGrabber(QObject* parent) + : QObject(parent) +{ +} + +AbstractScreenshotGrabber::~AbstractScreenshotGrabber() = default; diff --git a/src/widget/tool/abstractscreenshotgrabber.h b/src/widget/tool/abstractscreenshotgrabber.h new file mode 100644 index 0000000000..5eeed4b4ed --- /dev/null +++ b/src/widget/tool/abstractscreenshotgrabber.h @@ -0,0 +1,26 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright © 2015-2019 by The qTox Project Contributors + * Copyright © 2024-2025 The TokTok team. + */ + +#pragma once + +#include +#include +#include + +class AbstractScreenshotGrabber : public QObject +{ + Q_OBJECT + +public: + explicit AbstractScreenshotGrabber(QObject* parent); + ~AbstractScreenshotGrabber() override; + + virtual void showGrabber() = 0; + +signals: + void screenshotTaken(const QPixmap& pixmap); + void regionChosen(QRect region); + void rejected(); +}; diff --git a/src/widget/tool/screenshotgrabber.cpp b/src/widget/tool/screenshotgrabber.cpp index 538071d132..6c787daa2d 100644 --- a/src/widget/tool/screenshotgrabber.cpp +++ b/src/widget/tool/screenshotgrabber.cpp @@ -20,8 +20,8 @@ #include "toolboxgraphicsitem.h" #include "src/widget/widget.h" -ScreenshotGrabber::ScreenshotGrabber() - : QObject() +ScreenshotGrabber::ScreenshotGrabber(QObject* parent) + : AbstractScreenshotGrabber(parent) , mKeysBlocked(false) , scene(nullptr) , mQToxVisible(true) diff --git a/src/widget/tool/screenshotgrabber.h b/src/widget/tool/screenshotgrabber.h index 417d37a3b6..022fef92e5 100644 --- a/src/widget/tool/screenshotgrabber.h +++ b/src/widget/tool/screenshotgrabber.h @@ -5,6 +5,7 @@ #pragma once +#include "src/widget/tool/abstractscreenshotgrabber.h" #include #include #include @@ -20,26 +21,21 @@ class ScreenGrabberChooserRectItem; class ScreenGrabberOverlayItem; class ToolBoxGraphicsItem; -class ScreenshotGrabber : public QObject +class ScreenshotGrabber : public AbstractScreenshotGrabber { Q_OBJECT public: - ScreenshotGrabber(); + explicit ScreenshotGrabber(QObject* parent); ~ScreenshotGrabber() override; bool eventFilter(QObject* object, QEvent* event) override; - void showGrabber(); + void showGrabber() override; -public slots: +private slots: void acceptRegion(); void reInit(); -signals: - void screenshotTaken(const QPixmap& pixmap); - void regionChosen(QRect region); - void rejected(); - private: friend class ScreenGrabberOverlayItem; bool mKeysBlocked;