Skip to content

Commit 4aaea62

Browse files
paulobertdewey-rpi
authored andcommitted
fix(NativeFileDialog/Linux): Disable app while dialog is open
Signed-off-by: paul.oberosler <[email protected]>
1 parent fe78ae7 commit 4aaea62

File tree

1 file changed

+162
-70
lines changed

1 file changed

+162
-70
lines changed

src/linux/nativefiledialog_linux.cpp

Lines changed: 162 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,56 @@
1717
#include <QTimer>
1818
#include <QDBusPendingCallWatcher>
1919
#include <QDBusVariant>
20+
#include <QEvent>
2021
#include <unistd.h>
2122
#include <QProcess>
23+
#include <qnativeinterface.h>
24+
25+
// Helper class to block all input events on a window
26+
class InputBlockerEventFilter : public QObject
27+
{
28+
Q_OBJECT
29+
public:
30+
InputBlockerEventFilter(QObject *parent = nullptr) : QObject(parent) {}
31+
32+
protected:
33+
bool eventFilter(QObject *obj, QEvent *event) override {
34+
// Block all input events and provide feedback
35+
switch (event->type()) {
36+
case QEvent::MouseButtonPress:
37+
case QEvent::MouseButtonRelease:
38+
case QEvent::MouseButtonDblClick:
39+
case QEvent::MouseMove:
40+
case QEvent::KeyPress:
41+
case QEvent::KeyRelease:
42+
case QEvent::Wheel:
43+
case QEvent::TouchBegin:
44+
case QEvent::TouchUpdate:
45+
case QEvent::TouchEnd:
46+
case QEvent::TabletPress:
47+
case QEvent::TabletMove:
48+
case QEvent::TabletRelease:
49+
// Block the event
50+
// Note: We cannot directly focus the portal dialog window as it's managed
51+
// by the system's file picker. The modal flag should keep it on top.
52+
// Users can click directly on the file dialog to ensure it has focus.
53+
return true;
54+
default:
55+
// Allow other events to pass through
56+
return QObject::eventFilter(obj, event);
57+
}
58+
}
59+
};
2260

2361
// Helper class to handle D-Bus portal response signals
2462
class PortalResponseHandler : public QObject
2563
{
2664
Q_OBJECT
2765
public:
28-
PortalResponseHandler(QString &result, bool &dialogFinished)
29-
: m_result(result), m_dialogFinished(dialogFinished) {}
66+
PortalResponseHandler(QEventLoop *loop)
67+
: m_loop(loop) {}
68+
69+
QString result() const { return m_result; }
3070

3171
public slots:
3272
void handleResponse(uint response, const QVariantMap &results) {
@@ -48,21 +88,59 @@ public slots:
4888
qDebug() << "NativeFileDialog: Dialog cancelled or failed, response code:" << response;
4989
}
5090

51-
m_dialogFinished = true;
91+
// Quit the event loop immediately when we get a response
92+
if (m_loop) {
93+
m_loop->quit();
94+
}
5295
}
5396

5497
private:
55-
QString &m_result;
56-
bool &m_dialogFinished;
98+
QString m_result;
99+
QEventLoop *m_loop;
57100
};
58101

59-
// Anonymous namespace removed - filter conversion now done inline
102+
static QString portalParentHandleForWindow(QWindow *window)
103+
{
104+
if (!window)
105+
return QString();
106+
107+
// Platform detection is RUNTIME-based, not compile-time
108+
// Qt automatically selects the platform when the app starts
109+
// To test different backends, set the QT_QPA_PLATFORM environment variable:
110+
// QT_QPA_PLATFORM=xcb ./rpi-imager (force X11)
111+
// QT_QPA_PLATFORM=wayland ./rpi-imager (force Wayland)
112+
const QString platform = QGuiApplication::platformName().toLower();
113+
114+
// X11/XCB backend
115+
if (platform.contains("xcb") || platform == "xcb") {
116+
// On X11, provide the window ID in hex format for proper parenting
117+
WId wid = window->winId();
118+
if (wid != 0) {
119+
QString handle = QStringLiteral("x11:%1").arg(QString::number(wid, 16));
120+
qDebug() << "NativeFileDialog: X11 detected, parent handle:" << handle;
121+
return handle;
122+
}
123+
qWarning() << "NativeFileDialog: X11 detected but could not get window ID";
124+
return QString();
125+
}
126+
127+
// Wayland backend
128+
if (platform.contains("wayland")) {
129+
// Wayland window export requires the xdg-foreign protocol
130+
// Qt doesn't provide a simple API for this, so we rely on modal behavior
131+
qDebug() << "NativeFileDialog: Wayland detected, using modal dialog without parent handle";
132+
return QString();
133+
}
134+
135+
// Unknown platform
136+
qWarning() << "NativeFileDialog: Unknown platform detected:" << platform;
137+
return QString();
138+
}
60139

61140
QString NativeFileDialog::getFileNameNative(const QString &title,
62141
const QString &initialDir, const QString &filter,
63142
bool saveDialog, void *parentWindow)
64143
{
65-
66144
QDBusConnection bus = QDBusConnection::sessionBus();
67145
if (!bus.isConnected()) {
68146
qDebug() << "NativeFileDialog: No D-Bus session bus available";
@@ -81,14 +159,16 @@ QString NativeFileDialog::getFileNameNative(const QString &title,
81159

82160
// Prepare parent window identifier for modal behavior
83161
QString parentWindowId = "";
162+
QWindow *window = nullptr;
163+
InputBlockerEventFilter *inputBlocker = nullptr;
164+
84165
if (parentWindow) {
85-
QWindow *window = static_cast<QWindow*>(parentWindow);
86-
// Format: "x11:<xid>" for X11 windows
87-
// For Wayland it would be "wayland:<handle>" but that's more complex
88-
WId winId = window->winId();
89-
if (winId != 0) {
90-
parentWindowId = QString("x11:%1").arg(winId, 0, 16);
91-
}
166+
window = static_cast<QWindow*>(parentWindow);
167+
parentWindowId = portalParentHandleForWindow(window);
168+
169+
// Install event filter to block all input events while dialog is open
170+
inputBlocker = new InputBlockerEventFilter();
171+
window->installEventFilter(inputBlocker);
92172
}
93173

94174
// Prepare arguments for the portal call
@@ -105,59 +185,60 @@ QString NativeFileDialog::getFileNameNative(const QString &title,
105185
options["current_folder"] = QByteArray(dirUrl.toEncoded());
106186
}
107187

108-
// Convert and set file filters - Portal expects specific format
109-
if (!filter.isEmpty()) {
110-
// Parse Qt filter format: "Images (*.png *.jpg);;All files (*)"
111-
QStringList filterParts = filter.split(";;");
112-
QVariantList filters;
113-
114-
for (const QString &filterPart : filterParts) {
115-
if (filterPart.contains('(') && filterPart.contains(')')) {
116-
QString name = filterPart.section('(', 0, 0).trimmed();
117-
QString patterns = filterPart.section('(', 1, 1).section(')', 0, 0);
118-
QStringList patternList = patterns.split(' ', Qt::SkipEmptyParts);
119-
120-
// Portal filter format: [name, [[pattern1, pattern2], ...]]
121-
QVariantList filterEntry;
122-
filterEntry << name;
123-
QVariantList patternVariants;
124-
for (const QString &pattern : patternList) {
125-
patternVariants << QVariant(pattern);
126-
}
127-
filterEntry << QVariant(patternVariants);
128-
filters << QVariant(filterEntry);
129-
}
130-
}
131-
132-
if (!filters.isEmpty()) {
133-
options["filters"] = QVariant(filters);
134-
}
135-
}
136-
137188
// Generate unique request token
138189
static uint requestCounter = 0;
139190
QString token = QString("rpi_imager_%1_%2").arg(getpid()).arg(++requestCounter);
140191
options["handle_token"] = token;
141192

193+
// Note: File type filters are not supported on Linux
194+
// The XDG Desktop Portal requires complex D-Bus type marshalling (a(sa(us)))
195+
// that would require significant boilerplate code for minimal benefit.
196+
// The dialog will show all files - users can navigate and select any file.
197+
Q_UNUSED(filter);
198+
142199
QString method = saveDialog ? "SaveFile" : "OpenFile";
143200

144-
// Make the async call with parent window identifier for modal behavior
145-
QDBusReply<QDBusObjectPath> reply = interface.call(method, parentWindowId, title, options);
201+
// Use QDBusMessage for better control over argument types
202+
QDBusMessage message = QDBusMessage::createMethodCall(
203+
"org.freedesktop.portal.Desktop",
204+
"/org/freedesktop/portal/desktop",
205+
"org.freedesktop.portal.FileChooser",
206+
method);
207+
208+
message << parentWindowId << title << options;
209+
210+
// Make the call and get the reply
211+
QDBusMessage replyMsg = bus.call(message);
212+
213+
if (replyMsg.type() == QDBusMessage::ErrorMessage) {
214+
qDebug() << "NativeFileDialog: Portal call failed:" << replyMsg.errorMessage();
215+
// Restore window interactivity on error
216+
if (window && inputBlocker) {
217+
window->removeEventFilter(inputBlocker);
218+
delete inputBlocker;
219+
}
220+
return QString(); // QML callsites will handle fallback
221+
}
222+
223+
QDBusReply<QDBusObjectPath> reply(replyMsg);
146224

147225
if (!reply.isValid()) {
148226
qDebug() << "NativeFileDialog: Portal call failed:" << reply.error().message();
227+
// Restore window interactivity on error
228+
if (window && inputBlocker) {
229+
window->removeEventFilter(inputBlocker);
230+
delete inputBlocker;
231+
}
149232
return QString(); // QML callsites will handle fallback
150233
}
151234

152235
QString requestPath = reply.value().path();
153-
qDebug() << "NativeFileDialog: Portal request created at:" << requestPath;
154236

155-
// Connect to the Response signal to get the result
156-
QString result;
157-
bool dialogFinished = false;
237+
// Create event loop for blocking until we get a response
238+
QEventLoop loop;
158239

159240
// Create handler for the portal response
160-
PortalResponseHandler handler(result, dialogFinished);
241+
PortalResponseHandler handler(&loop);
161242

162243
// Connect to the D-Bus signal using QDBusConnection
163244
bool connected = QDBusConnection::sessionBus().connect(
@@ -171,35 +252,39 @@ QString NativeFileDialog::getFileNameNative(const QString &title,
171252

172253
if (!connected) {
173254
qDebug() << "NativeFileDialog: Could not connect to portal Response signal";
255+
// Restore window interactivity on error
256+
if (window && inputBlocker) {
257+
window->removeEventFilter(inputBlocker);
258+
delete inputBlocker;
259+
}
174260
return QString(); // QML callsites will handle fallback
175261
}
176262

177-
// Run a local event loop until we get the response
178-
QEventLoop loop;
263+
// Set up timeout timer (5 minutes should be more than enough)
179264
QTimer timeoutTimer;
180265
timeoutTimer.setSingleShot(true);
181-
timeoutTimer.setInterval(30000); // 30 second timeout
182-
183-
QObject::connect(&timeoutTimer, &QTimer::timeout, [&loop, &dialogFinished]() {
184-
qWarning() << "NativeFileDialog: Portal dialog timed out";
185-
dialogFinished = true;
266+
QObject::connect(&timeoutTimer, &QTimer::timeout, [&loop]() {
267+
qWarning() << "NativeFileDialog: Portal dialog timed out after 5 minutes";
186268
loop.quit();
187269
});
270+
timeoutTimer.start(300000); // 5 minute timeout
188271

189-
// Check for completion every 100ms
190-
QTimer checkTimer;
191-
checkTimer.setInterval(100);
192-
QObject::connect(&checkTimer, &QTimer::timeout, [&loop, &dialogFinished]() {
193-
if (dialogFinished) {
194-
loop.quit();
195-
}
196-
});
272+
// Wait for response or timeout - this blocks until the user interacts with the dialog
273+
// This makes the application non-interactive while the dialog is open
274+
loop.exec();
197275

198-
timeoutTimer.start();
199-
checkTimer.start();
276+
// Stop the timeout timer if it's still running
277+
timeoutTimer.stop();
200278

201-
// Wait for response or timeout
202-
loop.exec();
279+
// Disconnect the signal
280+
QDBusConnection::sessionBus().disconnect(
281+
"org.freedesktop.portal.Desktop",
282+
requestPath,
283+
"org.freedesktop.portal.Request",
284+
"Response",
285+
&handler,
286+
SLOT(handleResponse(uint, QVariantMap))
287+
);
203288

204289
// Clean up the request object
205290
QDBusInterface requestInterface("org.freedesktop.portal.Desktop",
@@ -210,6 +295,14 @@ QString NativeFileDialog::getFileNameNative(const QString &title,
210295
requestInterface.call("Close");
211296
}
212297

298+
QString result = handler.result();
299+
300+
// Restore window interactivity now that dialog is closed
301+
if (window && inputBlocker) {
302+
window->removeEventFilter(inputBlocker);
303+
delete inputBlocker;
304+
}
305+
213306
if (result.isEmpty()) {
214307
qDebug() << "NativeFileDialog: No file selected or portal failed";
215308
return QString(); // QML callsites will handle fallback
@@ -242,4 +335,3 @@ bool NativeFileDialog::areNativeDialogsAvailablePlatform()
242335
}
243336

244337
#include "nativefiledialog_linux.moc"
245-

0 commit comments

Comments
 (0)