Skip to content

Commit f2eee93

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

4 files changed

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

141-
QList<QString> OSHelper::grabQrCodesFromScreen() const
312+
QList<QString> OSHelper::grabQrCodesFromScreen()
142313
{
143314
QList<QString> codes;
144315

145316
try
146317
{
147-
const QImage image = screenshot().toImage();
318+
const QImage image = screenshot();
319+
if (image.isNull())
320+
{
321+
return codes;
322+
}
323+
148324
const std::vector<std::string> decoded = QrDecoder().decode(image);
149325
std::for_each(decoded.begin(), decoded.end(), [&codes](const std::string &code) {
150326
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)