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
2462class PortalResponseHandler : public QObject
2563{
2664 Q_OBJECT
2765public:
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
3171public 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
5497private:
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
61140QString 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