Skip to content

Commit 8a3f6b2

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

4 files changed

Lines changed: 214 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: 173 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+
}
9195

92-
return QGuiApplication::primaryScreen()->grabWindow(0);
96+
void showWindows(const std::unordered_set<QWindow *> &windows)
97+
{
98+
for (QWindow *window : windows)
99+
{
100+
window->show();
101+
}
93102
}
94103

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+
}
116+
117+
return QString::fromLocal8Bit(qgetenv("XDG_SESSION_TYPE")).compare(QStringLiteral("wayland"), Qt::CaseInsensitive) == 0;
118+
}
119+
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,129 @@ 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+
QDBusInterface portal(
183+
QStringLiteral("org.freedesktop.portal.Desktop"),
184+
QStringLiteral("/org/freedesktop/portal/desktop"),
185+
QStringLiteral("org.freedesktop.portal.Screenshot"),
186+
QDBusConnection::sessionBus());
187+
if (!portal.isValid())
188+
{
189+
qWarning() << "XDG screenshot portal is unavailable:" << QDBusConnection::sessionBus().lastError().message();
190+
return QImage();
191+
}
192+
193+
QVariantMap options;
194+
options.insert(QStringLiteral("interactive"), true);
195+
options.insert(QStringLiteral("modal"), true);
196+
197+
QDBusPendingReply<QDBusObjectPath> reply = portal.asyncCallWithArgumentList(
198+
QStringLiteral("Screenshot"),
199+
QVariantList{QString(), options});
200+
reply.waitForFinished();
201+
if (reply.isError())
202+
{
203+
qWarning() << "XDG screenshot portal request failed:" << reply.error().message();
204+
return QImage();
205+
}
206+
207+
resetScreenshotPortalResponse();
208+
209+
const QDBusObjectPath requestPath = reply.value();
210+
if (!QDBusConnection::sessionBus().connect(
211+
QStringLiteral("org.freedesktop.portal.Desktop"),
212+
requestPath.path(),
213+
QStringLiteral("org.freedesktop.portal.Request"),
214+
QStringLiteral("Response"),
215+
this,
216+
SLOT(handleScreenshotPortalResponse(uint,QVariantMap))))
217+
{
218+
qWarning() << "Failed to connect to XDG screenshot portal response";
219+
return QImage();
220+
}
221+
222+
QEventLoop loop;
223+
QTimer timer;
224+
timer.setSingleShot(true);
225+
QObject::connect(this, SIGNAL(screenshotPortalResponseReceived()), &loop, SLOT(quit()));
226+
QObject::connect(&timer, SIGNAL(timeout()), &loop, SLOT(quit()));
227+
timer.start(60000);
228+
229+
if (!m_screenshotPortalResponseReceived)
230+
{
231+
loop.exec();
232+
}
233+
234+
QDBusConnection::sessionBus().disconnect(
235+
QStringLiteral("org.freedesktop.portal.Desktop"),
236+
requestPath.path(),
237+
QStringLiteral("org.freedesktop.portal.Request"),
238+
QStringLiteral("Response"),
239+
this,
240+
SLOT(handleScreenshotPortalResponse(uint,QVariantMap)));
241+
242+
if (!m_screenshotPortalResponseReceived)
243+
{
244+
qWarning() << "Timed out waiting for XDG screenshot portal response";
245+
return QImage();
246+
}
247+
if (m_screenshotPortalResponse != 0)
248+
{
249+
qWarning() << "XDG screenshot portal request was not approved:" << m_screenshotPortalResponse;
250+
return QImage();
251+
}
252+
253+
const QVariant uriValue = unwrapDbusVariant(m_screenshotPortalResults.value(QStringLiteral("uri")));
254+
const QUrl uri(uriValue.toString());
255+
if (!uri.isLocalFile())
256+
{
257+
qWarning() << "XDG screenshot portal returned an unsupported URI:" << uri;
258+
return QImage();
259+
}
260+
261+
QImage image(uri.toLocalFile());
262+
if (image.isNull())
263+
{
264+
qWarning() << "Failed to load XDG screenshot portal image:" << uri;
265+
}
266+
return image;
267+
}
268+
#endif
269+
270+
QImage OSHelper::screenshot()
271+
{
272+
const std::unordered_set<QWindow *> hidden = hideVisibleWindows();
273+
const auto unhide = sg::make_scope_guard([&hidden]() {
274+
showWindows(hidden);
275+
});
276+
277+
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
278+
if (isWayland())
279+
{
280+
return screenshotPortal();
281+
}
282+
#endif
283+
284+
return QGuiApplication::primaryScreen()->grabWindow(0).toImage();
285+
}
286+
129287
void OSHelper::createDesktopEntry() const
130288
{
131289
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
@@ -138,13 +296,18 @@ QString OSHelper::downloadLocation() const
138296
return QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
139297
}
140298

141-
QList<QString> OSHelper::grabQrCodesFromScreen() const
299+
QList<QString> OSHelper::grabQrCodesFromScreen()
142300
{
143301
QList<QString> codes;
144302

145303
try
146304
{
147-
const QImage image = screenshot().toImage();
305+
const QImage image = screenshot();
306+
if (image.isNull())
307+
{
308+
return codes;
309+
}
310+
148311
const std::vector<std::string> decoded = QrDecoder().decode(image);
149312
std::for_each(decoded.begin(), decoded.end(), [&codes](const std::string &code) {
150313
codes.push_back(QString::fromStdString(code));

src/main/oshelper.h

Lines changed: 21 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,25 @@ 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+
#endif
6477

6578
signals:
79+
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
80+
void screenshotPortalResponseReceived();
81+
#endif
6682

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

7089
#endif // OSHELPER_H

0 commit comments

Comments
 (0)