Skip to content

Commit

Permalink
feat(Screenshot): Add Freedesktop portal screenshot support.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
iphydf committed Jan 21, 2025
1 parent 7070425 commit f0fc8f1
Show file tree
Hide file tree
Showing 13 changed files with 309 additions and 22 deletions.
5 changes: 5 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
18 changes: 18 additions & 0 deletions src/platform/screenshot.cpp
Original file line number Diff line number Diff line change
@@ -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
}
20 changes: 20 additions & 0 deletions src/platform/screenshot.h
Original file line number Diff line number Diff line change
@@ -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
162 changes: 162 additions & 0 deletions src/platform/screenshot_dbus.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/* SPDX-License-Identifier: GPL-3.0-or-later
* Copyright © 2025 The TokTok team.
*/

#include "screenshot_dbus.h"

#include <QtGlobal>

#if QT_CONFIG(dbus)
#include <QDBusConnection>
#include <QDBusInterface>
#include <QDBusMessage>
#include <QDebug>
#include <QGuiApplication>
#include <QPixmap>
#include <QRect>
#include <QUrl>
#include <QWidget>

#include <memory>

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<QDBusObjectPath>().path();
}
};

DBusScreenshotGrabber::DBusScreenshotGrabber(QWidget* parent)
: AbstractScreenshotGrabber(parent)
, d(std::make_unique<Data>(parent))
{
}

DBusScreenshotGrabber::~DBusScreenshotGrabber() = default;

DBusScreenshotGrabber* DBusScreenshotGrabber::create(QWidget* parent)
{
auto grabber = std::make_unique<DBusScreenshotGrabber>(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)
40 changes: 40 additions & 0 deletions src/platform/screenshot_dbus.h
Original file line number Diff line number Diff line change
@@ -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 <QVariantMap>

#include <memory>

#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<Data> d;
};
#endif // QT_CONFIG(dbus)
17 changes: 10 additions & 7 deletions src/widget/form/chatform.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand All @@ -36,6 +35,7 @@

#include <QApplication>
#include <QClipboard>
#include <QDragEnterEvent>
#include <QFileDialog>
#include <QFileInfo>
#include <QMessageBox>
Expand Down Expand Up @@ -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();
}

Expand Down
3 changes: 1 addition & 2 deletions src/widget/form/chatform.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -97,7 +96,7 @@ private slots:
void previewImage(const QPixmap& pixmap);
void cancelImagePreview();
void sendImageFromPreview();
void doScreenshot() const;
void doScreenshot();
void onCopyStatusMessage();

void callUpdateFriendActivity();
Expand Down
7 changes: 5 additions & 2 deletions src/widget/form/settings/avform.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
13 changes: 13 additions & 0 deletions src/widget/tool/abstractscreenshotgrabber.cpp
Original file line number Diff line number Diff line change
@@ -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;
Loading

0 comments on commit f0fc8f1

Please sign in to comment.