1// Copyright (C) 2017-2018 Red Hat, Inc
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qxdgdesktopportalfiledialog_p.h"
5
6#include <private/qgenericunixservices_p.h>
7#include <private/qguiapplication_p.h>
8#include <qpa/qplatformintegration.h>
9
10#include <QDBusConnection>
11#include <QDBusMessage>
12#include <QDBusPendingCall>
13#include <QDBusPendingCallWatcher>
14#include <QDBusPendingReply>
15#include <QDBusMetaType>
16
17#include <QEventLoop>
18#include <QFile>
19#include <QFileInfo>
20#include <QMetaType>
21#include <QMimeType>
22#include <QMimeDatabase>
23#include <QRandomGenerator>
24#include <QWindow>
25#include <QRegularExpression>
26
27QT_BEGIN_NAMESPACE
28
29using namespace Qt::StringLiterals;
30
31QDBusArgument &operator <<(QDBusArgument &arg, const QXdgDesktopPortalFileDialog::FilterCondition &filterCondition)
32{
33 arg.beginStructure();
34 arg << filterCondition.type << filterCondition.pattern;
35 arg.endStructure();
36 return arg;
37}
38
39const QDBusArgument &operator >>(const QDBusArgument &arg, QXdgDesktopPortalFileDialog::FilterCondition &filterCondition)
40{
41 uint type;
42 QString filterPattern;
43 arg.beginStructure();
44 arg >> type >> filterPattern;
45 filterCondition.type = (QXdgDesktopPortalFileDialog::ConditionType)type;
46 filterCondition.pattern = filterPattern;
47 arg.endStructure();
48
49 return arg;
50}
51
52QDBusArgument &operator <<(QDBusArgument &arg, const QXdgDesktopPortalFileDialog::Filter filter)
53{
54 arg.beginStructure();
55 arg << filter.name << filter.filterConditions;
56 arg.endStructure();
57 return arg;
58}
59
60const QDBusArgument &operator >>(const QDBusArgument &arg, QXdgDesktopPortalFileDialog::Filter &filter)
61{
62 QString name;
63 QXdgDesktopPortalFileDialog::FilterConditionList filterConditions;
64 arg.beginStructure();
65 arg >> name >> filterConditions;
66 filter.name = name;
67 filter.filterConditions = filterConditions;
68 arg.endStructure();
69
70 return arg;
71}
72
73class QXdgDesktopPortalFileDialogPrivate
74{
75public:
76 QXdgDesktopPortalFileDialogPrivate(QPlatformFileDialogHelper *nativeFileDialog, uint fileChooserPortalVersion)
77 : nativeFileDialog(nativeFileDialog)
78 , fileChooserPortalVersion(fileChooserPortalVersion)
79 { }
80
81 QEventLoop loop;
82 QString acceptLabel;
83 QString directory;
84 QString title;
85 QStringList nameFilters;
86 QStringList mimeTypesFilters;
87 // maps user-visible name for portal to full name filter
88 QMap<QString, QString> userVisibleToNameFilter;
89 QString selectedMimeTypeFilter;
90 QString selectedNameFilter;
91 QStringList selectedFiles;
92 std::unique_ptr<QPlatformFileDialogHelper> nativeFileDialog;
93 uint fileChooserPortalVersion = 0;
94 bool failedToOpen = false;
95 bool directoryMode = false;
96 bool multipleFiles = false;
97 bool saveFile = false;
98};
99
100QXdgDesktopPortalFileDialog::QXdgDesktopPortalFileDialog(QPlatformFileDialogHelper *nativeFileDialog, uint fileChooserPortalVersion)
101 : QPlatformFileDialogHelper()
102 , d_ptr(new QXdgDesktopPortalFileDialogPrivate(nativeFileDialog, fileChooserPortalVersion))
103{
104 Q_D(QXdgDesktopPortalFileDialog);
105
106 if (d->nativeFileDialog) {
107 connect(sender: d->nativeFileDialog.get(), SIGNAL(accept()), receiver: this, SIGNAL(accept()));
108 connect(sender: d->nativeFileDialog.get(), SIGNAL(reject()), receiver: this, SIGNAL(reject()));
109 }
110
111 d->loop.connect(asender: this, SIGNAL(accept()), SLOT(quit()));
112 d->loop.connect(asender: this, SIGNAL(reject()), SLOT(quit()));
113}
114
115QXdgDesktopPortalFileDialog::~QXdgDesktopPortalFileDialog()
116{
117}
118
119void QXdgDesktopPortalFileDialog::initializeDialog()
120{
121 Q_D(QXdgDesktopPortalFileDialog);
122
123 if (d->nativeFileDialog)
124 d->nativeFileDialog->setOptions(options());
125
126 if (options()->fileMode() == QFileDialogOptions::ExistingFiles)
127 d->multipleFiles = true;
128
129 if (options()->fileMode() == QFileDialogOptions::Directory || options()->fileMode() == QFileDialogOptions::DirectoryOnly)
130 d->directoryMode = true;
131
132 if (options()->isLabelExplicitlySet(label: QFileDialogOptions::Accept))
133 d->acceptLabel = options()->labelText(label: QFileDialogOptions::Accept);
134
135 if (!options()->windowTitle().isEmpty())
136 d->title = options()->windowTitle();
137
138 if (options()->acceptMode() == QFileDialogOptions::AcceptSave)
139 d->saveFile = true;
140
141 if (!options()->nameFilters().isEmpty())
142 d->nameFilters = options()->nameFilters();
143
144 if (!options()->mimeTypeFilters().isEmpty())
145 d->mimeTypesFilters = options()->mimeTypeFilters();
146
147 if (!options()->initiallySelectedMimeTypeFilter().isEmpty())
148 d->selectedMimeTypeFilter = options()->initiallySelectedMimeTypeFilter();
149
150 if (!options()->initiallySelectedNameFilter().isEmpty())
151 d->selectedNameFilter = options()->initiallySelectedNameFilter();
152
153 setDirectory(options()->initialDirectory());
154}
155
156void QXdgDesktopPortalFileDialog::openPortal(Qt::WindowFlags windowFlags, Qt::WindowModality windowModality, QWindow *parent)
157{
158 Q_D(QXdgDesktopPortalFileDialog);
159
160 QDBusMessage message = QDBusMessage::createMethodCall(destination: "org.freedesktop.portal.Desktop"_L1,
161 path: "/org/freedesktop/portal/desktop"_L1,
162 interface: "org.freedesktop.portal.FileChooser"_L1,
163 method: d->saveFile ? "SaveFile"_L1 : "OpenFile"_L1);
164 QVariantMap options;
165 if (!d->acceptLabel.isEmpty())
166 options.insert(key: "accept_label"_L1, value: d->acceptLabel);
167
168 options.insert(key: "modal"_L1, value: windowModality != Qt::NonModal);
169 options.insert(key: "multiple"_L1, value: d->multipleFiles);
170 options.insert(key: "directory"_L1, value: d->directoryMode);
171
172 if (!d->directory.isEmpty())
173 options.insert(key: "current_folder"_L1, value: QFile::encodeName(fileName: d->directory).append(c: '\0'));
174
175 if (d->saveFile && !d->selectedFiles.isEmpty()) {
176 // current_file for the file to be pre-selected, current_name for the file name to be
177 // pre-filled current_file accepts absolute path and requires the file to exist while
178 // current_name accepts just file name
179 QFileInfo selectedFileInfo(d->selectedFiles.first());
180 if (selectedFileInfo.exists())
181 options.insert(key: "current_file"_L1,
182 value: QFile::encodeName(fileName: d->selectedFiles.first()).append(c: '\0'));
183 options.insert(key: "current_name"_L1, value: selectedFileInfo.fileName());
184 }
185
186 // Insert filters
187 qDBusRegisterMetaType<FilterCondition>();
188 qDBusRegisterMetaType<FilterConditionList>();
189 qDBusRegisterMetaType<Filter>();
190 qDBusRegisterMetaType<FilterList>();
191
192 FilterList filterList;
193 auto selectedFilterIndex = filterList.size() - 1;
194
195 d->userVisibleToNameFilter.clear();
196
197 if (!d->mimeTypesFilters.isEmpty()) {
198 for (const QString &mimeTypefilter : d->mimeTypesFilters) {
199 QMimeDatabase mimeDatabase;
200 QMimeType mimeType = mimeDatabase.mimeTypeForName(nameOrAlias: mimeTypefilter);
201
202 // Creates e.g. (1, "image/png")
203 FilterCondition filterCondition;
204 filterCondition.type = MimeType;
205 filterCondition.pattern = mimeTypefilter;
206
207 // Creates e.g. [((1, "image/png"))]
208 FilterConditionList filterConditions;
209 filterConditions << filterCondition;
210
211 // Creates e.g. [("Images", [((1, "image/png"))])]
212 Filter filter;
213 filter.name = mimeType.comment();
214 filter.filterConditions = filterConditions;
215
216 filterList << filter;
217
218 if (!d->selectedMimeTypeFilter.isEmpty() && d->selectedMimeTypeFilter == mimeTypefilter)
219 selectedFilterIndex = filterList.size() - 1;
220 }
221 } else if (!d->nameFilters.isEmpty()) {
222 for (const QString &nameFilter : d->nameFilters) {
223 // Do parsing:
224 // Supported format is ("Images (*.png *.jpg)")
225 QRegularExpression regexp(QPlatformFileDialogHelper::filterRegExp);
226 QRegularExpressionMatch match = regexp.match(subject: nameFilter);
227 if (match.hasMatch()) {
228 QString userVisibleName = match.captured(nth: 1);
229 QStringList filterStrings = match.captured(nth: 2).split(sep: u' ', behavior: Qt::SkipEmptyParts);
230
231 if (filterStrings.isEmpty()) {
232 qWarning() << "Filter " << userVisibleName << " is empty and will be ignored.";
233 continue;
234 }
235
236 FilterConditionList filterConditions;
237 for (const QString &filterString : filterStrings) {
238 FilterCondition filterCondition;
239 filterCondition.type = GlobalPattern;
240 filterCondition.pattern = filterString;
241 filterConditions << filterCondition;
242 }
243
244 Filter filter;
245 filter.name = userVisibleName;
246 filter.filterConditions = filterConditions;
247
248 filterList << filter;
249
250 d->userVisibleToNameFilter.insert(key: userVisibleName, value: nameFilter);
251
252 if (!d->selectedNameFilter.isEmpty() && d->selectedNameFilter == nameFilter)
253 selectedFilterIndex = filterList.size() - 1;
254 }
255 }
256 }
257
258 if (!filterList.isEmpty())
259 options.insert(key: "filters"_L1, value: QVariant::fromValue(value: filterList));
260
261 if (selectedFilterIndex != -1)
262 options.insert(key: "current_filter"_L1, value: QVariant::fromValue(value: filterList[selectedFilterIndex]));
263
264 options.insert(key: "handle_token"_L1, QStringLiteral("qt%1").arg(a: QRandomGenerator::global()->generate()));
265
266 // TODO choices a(ssa(ss)s)
267 // List of serialized combo boxes to add to the file chooser.
268
269 auto unixServices = dynamic_cast<QGenericUnixServices *>(
270 QGuiApplicationPrivate::platformIntegration()->services());
271 if (parent && unixServices)
272 message << unixServices->portalWindowIdentifier(window: parent);
273 else
274 message << QString();
275
276 message << d->title << options;
277
278 QDBusPendingCall pendingCall = QDBusConnection::sessionBus().asyncCall(message);
279 QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pendingCall);
280 connect(sender: watcher, signal: &QDBusPendingCallWatcher::finished, context: this, slot: [this, d, windowFlags, windowModality, parent] (QDBusPendingCallWatcher *watcher) {
281 QDBusPendingReply<QDBusObjectPath> reply = *watcher;
282 // Any error means the dialog is not shown and we need to fallback
283 d->failedToOpen = reply.isError();
284 if (d->failedToOpen) {
285 if (d->nativeFileDialog) {
286 d->nativeFileDialog->show(windowFlags, windowModality, parent);
287 if (d->loop.isRunning())
288 d->nativeFileDialog->exec();
289 } else {
290 Q_EMIT reject();
291 }
292 } else {
293 QDBusConnection::sessionBus().connect(service: nullptr,
294 path: reply.value().path(),
295 interface: "org.freedesktop.portal.Request"_L1,
296 name: "Response"_L1,
297 receiver: this,
298 SLOT(gotResponse(uint,QVariantMap)));
299 }
300 watcher->deleteLater();
301 });
302}
303
304bool QXdgDesktopPortalFileDialog::defaultNameFilterDisables() const
305{
306 return false;
307}
308
309void QXdgDesktopPortalFileDialog::setDirectory(const QUrl &directory)
310{
311 Q_D(QXdgDesktopPortalFileDialog);
312
313 if (d->nativeFileDialog) {
314 d->nativeFileDialog->setOptions(options());
315 d->nativeFileDialog->setDirectory(directory);
316 }
317
318 d->directory = directory.path();
319}
320
321QUrl QXdgDesktopPortalFileDialog::directory() const
322{
323 Q_D(const QXdgDesktopPortalFileDialog);
324
325 if (d->nativeFileDialog && useNativeFileDialog())
326 return d->nativeFileDialog->directory();
327
328 return d->directory;
329}
330
331void QXdgDesktopPortalFileDialog::selectFile(const QUrl &filename)
332{
333 Q_D(QXdgDesktopPortalFileDialog);
334
335 if (d->nativeFileDialog) {
336 d->nativeFileDialog->setOptions(options());
337 d->nativeFileDialog->selectFile(filename);
338 }
339
340 d->selectedFiles << filename.path();
341}
342
343QList<QUrl> QXdgDesktopPortalFileDialog::selectedFiles() const
344{
345 Q_D(const QXdgDesktopPortalFileDialog);
346
347 if (d->nativeFileDialog && useNativeFileDialog())
348 return d->nativeFileDialog->selectedFiles();
349
350 QList<QUrl> files;
351 for (const QString &file : d->selectedFiles) {
352 files << QUrl(file);
353 }
354 return files;
355}
356
357void QXdgDesktopPortalFileDialog::setFilter()
358{
359 Q_D(QXdgDesktopPortalFileDialog);
360
361 if (d->nativeFileDialog) {
362 d->nativeFileDialog->setOptions(options());
363 d->nativeFileDialog->setFilter();
364 }
365}
366
367void QXdgDesktopPortalFileDialog::selectMimeTypeFilter(const QString &filter)
368{
369 Q_D(QXdgDesktopPortalFileDialog);
370 if (d->nativeFileDialog) {
371 d->nativeFileDialog->setOptions(options());
372 d->nativeFileDialog->selectMimeTypeFilter(filter);
373 }
374}
375
376QString QXdgDesktopPortalFileDialog::selectedMimeTypeFilter() const
377{
378 Q_D(const QXdgDesktopPortalFileDialog);
379 return d->selectedMimeTypeFilter;
380}
381
382void QXdgDesktopPortalFileDialog::selectNameFilter(const QString &filter)
383{
384 Q_D(QXdgDesktopPortalFileDialog);
385
386 if (d->nativeFileDialog) {
387 d->nativeFileDialog->setOptions(options());
388 d->nativeFileDialog->selectNameFilter(filter);
389 }
390}
391
392QString QXdgDesktopPortalFileDialog::selectedNameFilter() const
393{
394 Q_D(const QXdgDesktopPortalFileDialog);
395 return d->selectedNameFilter;
396}
397
398void QXdgDesktopPortalFileDialog::exec()
399{
400 Q_D(QXdgDesktopPortalFileDialog);
401
402 if (d->nativeFileDialog && useNativeFileDialog()) {
403 d->nativeFileDialog->exec();
404 return;
405 }
406
407 // HACK we have to avoid returning until we emit that the dialog was accepted or rejected
408 d->loop.exec();
409}
410
411void QXdgDesktopPortalFileDialog::hide()
412{
413 Q_D(QXdgDesktopPortalFileDialog);
414
415 if (d->nativeFileDialog)
416 d->nativeFileDialog->hide();
417}
418
419bool QXdgDesktopPortalFileDialog::show(Qt::WindowFlags windowFlags, Qt::WindowModality windowModality, QWindow *parent)
420{
421 Q_D(QXdgDesktopPortalFileDialog);
422
423 initializeDialog();
424
425 if (d->nativeFileDialog && useNativeFileDialog(fallbackType: OpenFallback))
426 return d->nativeFileDialog->show(windowFlags, windowModality, parent);
427
428 openPortal(windowFlags, windowModality, parent);
429
430 return true;
431}
432
433void QXdgDesktopPortalFileDialog::gotResponse(uint response, const QVariantMap &results)
434{
435 Q_D(QXdgDesktopPortalFileDialog);
436
437 if (!response) {
438 if (results.contains(key: "uris"_L1))
439 d->selectedFiles = results.value(key: "uris"_L1).toStringList();
440
441 if (results.contains(key: "current_filter"_L1)) {
442 const Filter selectedFilter = qdbus_cast<Filter>(v: results.value(QStringLiteral("current_filter")));
443 if (!selectedFilter.filterConditions.empty() && selectedFilter.filterConditions[0].type == MimeType) {
444 // s.a. QXdgDesktopPortalFileDialog::openPortal which basically does the inverse
445 d->selectedMimeTypeFilter = selectedFilter.filterConditions[0].pattern;
446 d->selectedNameFilter.clear();
447 } else {
448 d->selectedNameFilter = d->userVisibleToNameFilter.value(key: selectedFilter.name);
449 d->selectedMimeTypeFilter.clear();
450 }
451 }
452 Q_EMIT accept();
453 } else {
454 Q_EMIT reject();
455 }
456}
457
458bool QXdgDesktopPortalFileDialog::useNativeFileDialog(QXdgDesktopPortalFileDialog::FallbackType fallbackType) const
459{
460 Q_D(const QXdgDesktopPortalFileDialog);
461
462 if (d->failedToOpen && fallbackType != OpenFallback)
463 return true;
464
465 if (d->fileChooserPortalVersion < 3) {
466 if (options()->fileMode() == QFileDialogOptions::Directory)
467 return true;
468 else if (options()->fileMode() == QFileDialogOptions::DirectoryOnly)
469 return true;
470 }
471
472 return false;
473}
474
475QT_END_NAMESPACE
476
477#include "moc_qxdgdesktopportalfiledialog_p.cpp"
478

source code of qtbase/src/plugins/platformthemes/xdgdesktopportal/qxdgdesktopportalfiledialog.cpp