Skip to content

Commit 63409e9

Browse files
committed
linux: use screenshot portal for Wayland QR scanning
1 parent 76d5f8c commit 63409e9

4 files changed

Lines changed: 272 additions & 14 deletions

File tree

CMakeLists.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,10 @@ set(QT5_LIBRARIES
131131
Qt5Xml
132132
)
133133

134+
if(UNIX AND NOT APPLE AND NOT ANDROID)
135+
list(APPEND QT5_LIBRARIES Qt5DBus)
136+
endif()
137+
134138
if(WITH_SCANNER)
135139
list(APPEND QT5_LIBRARIES Qt5Multimedia)
136140
endif()
@@ -316,6 +320,11 @@ if(STATIC)
316320
pkg_check_modules(FONTCONFIG REQUIRED fontconfig)
317321
list(APPEND QT5_LIBRARIES ${FONTCONFIG_STATIC_LIBRARIES})
318322
endif()
323+
324+
if(UNIX AND NOT APPLE AND NOT ANDROID)
325+
pkg_check_modules(DBUS1_PKG_CONFIG REQUIRED dbus-1)
326+
list(APPEND QT5_LIBRARIES ${DBUS1_PKG_CONFIG_STATIC_LIBRARIES} ${DBUS1_PKG_CONFIG_STATIC_LDFLAGS_OTHER})
327+
endif()
319328
endif()
320329

321330
if(ANDROID)

Dockerfile.linux

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,19 @@ ENV CXXFLAGS="-fPIC"
99
ENV SOURCE_DATE_EPOCH=1397818193
1010

1111
RUN apt update && \
12-
apt install -y automake autopoint bison gettext git gperf libgl1-mesa-dev libglib2.0-dev \
12+
apt install -y autoconf-archive automake autopoint bison gettext git gperf libexpat1-dev libgl1-mesa-dev libglib2.0-dev \
1313
libpng-dev libpthread-stubs0-dev libsodium-dev libtool-bin libudev-dev libusb-1.0-0-dev mesa-common-dev \
1414
pkg-config python wget xutils-dev
1515

16+
RUN git clone -b dbus-1.12.2 --depth 1 https://gitlab.freedesktop.org/dbus/dbus.git && \
17+
cd dbus && \
18+
git reset --hard 0f0968336b9711349023e1d41f075b2bccf7c20b && \
19+
./autogen.sh && \
20+
./configure --disable-shared --enable-static --disable-systemd --disable-launchd --disable-selinux --disable-apparmor --disable-libaudit --disable-xml-docs --disable-doxygen-docs --disable-ducktype-docs --disable-modular-tests --disable-embedded-tests --disable-x11-autolaunch --without-x --with-systemdsystemunitdir=no --with-systemduserunitdir=no && \
21+
make -j$THREADS && \
22+
make -j$THREADS install && \
23+
rm -rf $(pwd)
24+
1625
RUN git clone -b xorgproto-2020.1 --depth 1 https://gitlab.freedesktop.org/xorg/proto/xorgproto && \
1726
cd xorgproto && \
1827
git reset --hard c62e8203402cafafa5ba0357b6d1c019156c9f36 && \
@@ -198,7 +207,7 @@ RUN rm /usr/lib/x86_64-linux-gnu/libX11.a && \
198207
sed -i s/\\/usr\\/X11R6\\/lib64/\\/usr\\/local\\/lib/ qtbase/mkspecs/linux-g++-64/qmake.conf && \
199208
./configure --prefix=/usr -platform linux-g++-64 -opensource -confirm-license -release -static -no-avx \
200209
-opengl desktop -qpa xcb -xcb -xcb-xlib -feature-xlib -system-freetype -fontconfig -glib \
201-
-no-dbus -no-feature-qml-worker-script -no-linuxfb -no-openssl -no-sql-sqlite -no-kms -no-use-gold-linker \
210+
-no-feature-qml-worker-script -no-linuxfb -no-openssl -no-sql-sqlite -no-kms -no-use-gold-linker \
202211
-qt-harfbuzz -qt-libjpeg -qt-libpng -qt-pcre -qt-zlib \
203212
-skip qt3d -skip qtandroidextras -skip qtcanvas3d -skip qtcharts -skip qtconnectivity -skip qtdatavis3d \
204213
-skip qtdoc -skip qtgamepad -skip qtlocation -skip qtmacextras -skip qtnetworkauth -skip qtpurchasing \

src/main/oshelper.cpp

Lines changed: 230 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@
6161
#undef FocusOut
6262
// #undef those Xlib #defines that conflict with QEvent::Type enum
6363
#include "qt/utils.h"
64+
#include <QDBusConnection>
65+
#include <QDBusError>
66+
#include <QDBusInterface>
67+
#include <QDBusObjectPath>
68+
#include <QDBusPendingReply>
69+
#include <QDBusVariant>
70+
#include <QEventLoop>
71+
#include <QTimer>
6472
#endif
6573

6674
#include "QR-Code-scanner/Decoder.h"
@@ -70,7 +78,7 @@
7078
namespace
7179
{
7280

73-
QPixmap screenshot()
81+
std::unordered_set<QWindow *> hideVisibleWindows()
7482
{
7583
std::unordered_set<QWindow *> hidden;
7684
const QWindowList windows = QGuiApplication::allWindows();
@@ -82,16 +90,43 @@ QPixmap screenshot()
8290
window->hide();
8391
}
8492
}
85-
const auto unhide = sg::make_scope_guard([&hidden]() {
86-
for (QWindow *window : hidden)
87-
{
88-
window->show();
89-
}
90-
});
93+
return hidden;
94+
}
95+
96+
void showWindows(const std::unordered_set<QWindow *> &windows)
97+
{
98+
for (QWindow *window : windows)
99+
{
100+
window->show();
101+
}
102+
}
103+
104+
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
105+
bool isWayland()
106+
{
107+
if (QGuiApplication::platformName().contains("wayland", Qt::CaseInsensitive))
108+
{
109+
return true;
110+
}
111+
112+
if (!qEnvironmentVariableIsEmpty("WAYLAND_DISPLAY"))
113+
{
114+
return true;
115+
}
91116

92-
return QGuiApplication::primaryScreen()->grabWindow(0);
117+
return QString::fromLocal8Bit(qgetenv("XDG_SESSION_TYPE")).compare(QStringLiteral("wayland"), Qt::CaseInsensitive) == 0;
93118
}
94119

120+
QVariant unwrapDbusVariant(const QVariant &value)
121+
{
122+
if (value.canConvert<QDBusVariant>())
123+
{
124+
return value.value<QDBusVariant>().variant();
125+
}
126+
return value;
127+
}
128+
#endif
129+
95130
} // namespace
96131

97132
#if defined(Q_OS_WIN)
@@ -126,6 +161,186 @@ OSHelper::OSHelper(QObject *parent) : QObject(parent)
126161

127162
}
128163

164+
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
165+
void OSHelper::resetScreenshotPortalResponse()
166+
{
167+
m_screenshotPortalResponseReceived = false;
168+
m_screenshotPortalResponse = 1;
169+
m_screenshotPortalResults.clear();
170+
}
171+
172+
void OSHelper::handleScreenshotPortalResponse(uint responseCode, const QVariantMap &responseResults)
173+
{
174+
m_screenshotPortalResponseReceived = true;
175+
m_screenshotPortalResponse = responseCode;
176+
m_screenshotPortalResults = responseResults;
177+
emit screenshotPortalResponseReceived();
178+
}
179+
180+
QImage OSHelper::screenshotPortal()
181+
{
182+
if (m_screenshotPortalInProgress)
183+
{
184+
qWarning() << "XDG screenshot portal request is already in progress";
185+
return QImage();
186+
}
187+
188+
m_screenshotPortalInProgress = true;
189+
const auto resetInProgress = sg::make_scope_guard([this]() {
190+
m_screenshotPortalInProgress = false;
191+
});
192+
193+
QDBusConnection sessionBus = QDBusConnection::sessionBus();
194+
QDBusInterface portal(
195+
QStringLiteral("org.freedesktop.portal.Desktop"),
196+
QStringLiteral("/org/freedesktop/portal/desktop"),
197+
QStringLiteral("org.freedesktop.portal.Screenshot"),
198+
sessionBus);
199+
if (!portal.isValid())
200+
{
201+
qWarning() << "XDG screenshot portal is unavailable:" << sessionBus.lastError().message();
202+
return QImage();
203+
}
204+
205+
resetScreenshotPortalResponse();
206+
207+
const QString handleToken = QStringLiteral("monero_gui_%1_%2")
208+
.arg(QRandomGenerator::global()->generate(), 8, 16, QLatin1Char('0'))
209+
.arg(QRandomGenerator::global()->generate(), 8, 16, QLatin1Char('0'));
210+
QString sender = sessionBus.baseService();
211+
if (sender.startsWith(QLatin1Char(':')))
212+
{
213+
sender.remove(0, 1);
214+
}
215+
sender.replace(QLatin1Char('.'), QLatin1Char('_'));
216+
if (sender.isEmpty())
217+
{
218+
qWarning() << "XDG screenshot portal cannot determine DBus sender name";
219+
return QImage();
220+
}
221+
222+
QString requestPath = QStringLiteral("/org/freedesktop/portal/desktop/request/%1/%2").arg(sender, handleToken);
223+
if (!sessionBus.connect(
224+
QStringLiteral("org.freedesktop.portal.Desktop"),
225+
requestPath,
226+
QStringLiteral("org.freedesktop.portal.Request"),
227+
QStringLiteral("Response"),
228+
this,
229+
SLOT(handleScreenshotPortalResponse(uint,QVariantMap))))
230+
{
231+
qWarning() << "Failed to connect to XDG screenshot portal response";
232+
return QImage();
233+
}
234+
235+
const auto disconnectRequest = sg::make_scope_guard([this, sessionBus, &requestPath]() mutable {
236+
sessionBus.disconnect(
237+
QStringLiteral("org.freedesktop.portal.Desktop"),
238+
requestPath,
239+
QStringLiteral("org.freedesktop.portal.Request"),
240+
QStringLiteral("Response"),
241+
this,
242+
SLOT(handleScreenshotPortalResponse(uint,QVariantMap)));
243+
});
244+
245+
QVariantMap options;
246+
options.insert(QStringLiteral("interactive"), true);
247+
options.insert(QStringLiteral("modal"), true);
248+
options.insert(QStringLiteral("handle_token"), handleToken);
249+
250+
QDBusPendingReply<QDBusObjectPath> reply = portal.asyncCallWithArgumentList(
251+
QStringLiteral("Screenshot"),
252+
QVariantList{QString(), options});
253+
reply.waitForFinished();
254+
if (reply.isError())
255+
{
256+
qWarning() << "XDG screenshot portal request failed:" << reply.error().message();
257+
return QImage();
258+
}
259+
260+
const QString returnedRequestPath = reply.value().path();
261+
if (returnedRequestPath != requestPath)
262+
{
263+
sessionBus.disconnect(
264+
QStringLiteral("org.freedesktop.portal.Desktop"),
265+
requestPath,
266+
QStringLiteral("org.freedesktop.portal.Request"),
267+
QStringLiteral("Response"),
268+
this,
269+
SLOT(handleScreenshotPortalResponse(uint,QVariantMap)));
270+
271+
requestPath = returnedRequestPath;
272+
if (!sessionBus.connect(
273+
QStringLiteral("org.freedesktop.portal.Desktop"),
274+
requestPath,
275+
QStringLiteral("org.freedesktop.portal.Request"),
276+
QStringLiteral("Response"),
277+
this,
278+
SLOT(handleScreenshotPortalResponse(uint,QVariantMap))))
279+
{
280+
qWarning() << "Failed to connect to XDG screenshot portal response";
281+
return QImage();
282+
}
283+
}
284+
285+
QEventLoop loop;
286+
QTimer timer;
287+
timer.setSingleShot(true);
288+
QObject::connect(this, SIGNAL(screenshotPortalResponseReceived()), &loop, SLOT(quit()));
289+
QObject::connect(&timer, SIGNAL(timeout()), &loop, SLOT(quit()));
290+
timer.start(60000);
291+
292+
if (!m_screenshotPortalResponseReceived)
293+
{
294+
loop.exec();
295+
}
296+
297+
if (!m_screenshotPortalResponseReceived)
298+
{
299+
qWarning() << "Timed out waiting for XDG screenshot portal response";
300+
return QImage();
301+
}
302+
if (m_screenshotPortalResponse != 0)
303+
{
304+
qWarning() << "XDG screenshot portal request was not approved:" << m_screenshotPortalResponse;
305+
return QImage();
306+
}
307+
308+
const QVariant uriValue = unwrapDbusVariant(m_screenshotPortalResults.value(QStringLiteral("uri")));
309+
const QUrl uri(uriValue.toString());
310+
if (!uri.isLocalFile())
311+
{
312+
qWarning() << "XDG screenshot portal returned an unsupported URI:" << uri;
313+
return QImage();
314+
}
315+
316+
const QString imagePath = uri.toLocalFile();
317+
QImage image(imagePath);
318+
QFile::remove(imagePath);
319+
if (image.isNull())
320+
{
321+
qWarning() << "Failed to load XDG screenshot portal image:" << uri;
322+
}
323+
return image;
324+
}
325+
#endif
326+
327+
QImage OSHelper::screenshot()
328+
{
329+
const std::unordered_set<QWindow *> hidden = hideVisibleWindows();
330+
const auto unhide = sg::make_scope_guard([&hidden]() {
331+
showWindows(hidden);
332+
});
333+
334+
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
335+
if (isWayland())
336+
{
337+
return screenshotPortal();
338+
}
339+
#endif
340+
341+
return QGuiApplication::primaryScreen()->grabWindow(0).toImage();
342+
}
343+
129344
void OSHelper::createDesktopEntry() const
130345
{
131346
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
@@ -138,13 +353,18 @@ QString OSHelper::downloadLocation() const
138353
return QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
139354
}
140355

141-
QList<QString> OSHelper::grabQrCodesFromScreen() const
356+
QList<QString> OSHelper::grabQrCodesFromScreen()
142357
{
143358
QList<QString> codes;
144359

145360
try
146361
{
147-
const QImage image = screenshot().toImage();
362+
const QImage image = screenshot();
363+
if (image.isNull())
364+
{
365+
return codes;
366+
}
367+
148368
const std::vector<std::string> decoded = QrDecoder().decode(image);
149369
std::for_each(decoded.begin(), decoded.end(), [&codes](const std::string &code) {
150370
codes.push_back(QString::fromStdString(code));

src/main/oshelper.h

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,12 @@
3030
#define OSHELPER_H
3131

3232
#include <QList>
33+
#include <QImage>
3334
#include <QObject>
3435
#include <QString>
36+
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
37+
#include <QVariantMap>
38+
#endif
3539
/**
3640
* @brief The OSHelper class - exports to QML some OS-related functions
3741
*/
@@ -45,7 +49,7 @@ class OSHelper : public QObject
4549

4650
Q_INVOKABLE void createDesktopEntry() const;
4751
Q_INVOKABLE QString downloadLocation() const;
48-
Q_INVOKABLE QList<QString> grabQrCodesFromScreen() const;
52+
Q_INVOKABLE QList<QString> grabQrCodesFromScreen();
4953
Q_INVOKABLE bool openFile(const QString &filePath) const;
5054
Q_INVOKABLE bool openContainingFolder(const QString &filePath) const;
5155
Q_INVOKABLE QString openSaveFileDialog(const QString &title, const QString &folder, const QString &filename) const;
@@ -61,10 +65,26 @@ class OSHelper : public QObject
6165

6266
static std::pair<quint8, QString> getNetworkTypeAndAddressFromFile(const QString &wallet);
6367
private:
68+
QImage screenshot();
69+
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
70+
QImage screenshotPortal();
71+
void resetScreenshotPortalResponse();
72+
73+
bool m_screenshotPortalResponseReceived = false;
74+
uint m_screenshotPortalResponse = 1;
75+
QVariantMap m_screenshotPortalResults;
76+
bool m_screenshotPortalInProgress = false;
77+
#endif
6478

6579
signals:
80+
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
81+
void screenshotPortalResponseReceived();
82+
#endif
6683

67-
public slots:
84+
private slots:
85+
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
86+
void handleScreenshotPortalResponse(uint responseCode, const QVariantMap &responseResults);
87+
#endif
6888
};
6989

7090
#endif // OSHELPER_H

0 commit comments

Comments
 (0)