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 QUrl 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.toLocalFile()).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.constFirst());
180 if (selectedFileInfo.exists())
181 options.insert(key: "current_file"_L1,
182 value: QFile::encodeName(fileName: d->selectedFiles.constFirst()).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 if (filter.name.isEmpty())
217 filter.name = mimeTypefilter;
218
219 filterList << filter;
220
221 if (!d->selectedMimeTypeFilter.isEmpty() && d->selectedMimeTypeFilter == mimeTypefilter)
222 selectedFilterIndex = filterList.size() - 1;
223 }
224 } else if (!d->nameFilters.isEmpty()) {
225 for (const QString &nameFilter : d->nameFilters) {
226 // Do parsing:
227 // Supported format is ("Images (*.png *.jpg)")
228 QRegularExpression regexp(QPlatformFileDialogHelper::filterRegExp);
229 QRegularExpressionMatch match = regexp.match(subject: nameFilter);
230 if (match.hasMatch()) {
231 QString userVisibleName = match.captured(nth: 1);
232 QStringList filterStrings = match.captured(nth: 2).split(sep: u' ', behavior: Qt::SkipEmptyParts);
233
234 if (filterStrings.isEmpty()) {
235 qWarning() << "Filter " << userVisibleName << " is empty and will be ignored.";
236 continue;
237 }
238
239 FilterConditionList filterConditions;
240 for (const QString &filterString : filterStrings) {
241 FilterCondition filterCondition;
242 filterCondition.type = GlobalPattern;
243 filterCondition.pattern = filterString;
244 filterConditions << filterCondition;
245 }
246
247 Filter filter;
248 filter.name = userVisibleName;
249 filter.filterConditions = filterConditions;
250
251 filterList << filter;
252
253 d->userVisibleToNameFilter.insert(key: userVisibleName, value: nameFilter);
254
255 if (!d->selectedNameFilter.isEmpty() && d->selectedNameFilter == nameFilter)
256 selectedFilterIndex = filterList.size() - 1;
257 }
258 }
259 }
260
261 if (!filterList.isEmpty())
262 options.insert(key: "filters"_L1, value: QVariant::fromValue(value: filterList));
263
264 if (selectedFilterIndex != -1)
265 options.insert(key: "current_filter"_L1, value: QVariant::fromValue(value: filterList[selectedFilterIndex]));
266
267 options.insert(key: "handle_token"_L1, QStringLiteral("qt%1").arg(a: QRandomGenerator::global()->generate()));
268
269 // TODO choices a(ssa(ss)s)
270 // List of serialized combo boxes to add to the file chooser.
271
272 auto unixServices = dynamic_cast<QGenericUnixServices *>(
273 QGuiApplicationPrivate::platformIntegration()->services());
274 if (parent && unixServices)
275 message << unixServices->portalWindowIdentifier(window: parent);
276 else
277 message << QString();
278
279 message << d->title << options;
280
281 QDBusPendingCall pendingCall = QDBusConnection::sessionBus().asyncCall(message);
282 QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pendingCall);
283 connect(sender: watcher, signal: &QDBusPendingCallWatcher::finished, context: this, slot: [this, d, windowFlags, windowModality, parent] (QDBusPendingCallWatcher *watcher) {
284 QDBusPendingReply<QDBusObjectPath> reply = *watcher;
285 // Any error means the dialog is not shown and we need to fallback
286 d->failedToOpen = reply.isError();
287 if (d->failedToOpen) {
288 if (d->nativeFileDialog) {
289 d->nativeFileDialog->show(windowFlags, windowModality, parent);
290 if (d->loop.isRunning())
291 d->nativeFileDialog->exec();
292 } else {
293 Q_EMIT reject();
294 }
295 } else {
296 QDBusConnection::sessionBus().connect(service: nullptr,
297 path: reply.value().path(),
298 interface: "org.freedesktop.portal.Request"_L1,
299 name: "Response"_L1,
300 receiver: this,
301 SLOT(gotResponse(uint,QVariantMap)));
302 }
303 watcher->deleteLater();
304 });
305}
306
307bool QXdgDesktopPortalFileDialog::defaultNameFilterDisables() const
308{
309 return false;
310}
311
312void QXdgDesktopPortalFileDialog::setDirectory(const QUrl &directory)
313{
314 Q_D(QXdgDesktopPortalFileDialog);
315
316 if (d->nativeFileDialog) {
317 d->nativeFileDialog->setOptions(options());
318 d->nativeFileDialog->setDirectory(directory);
319 }
320
321 d->directory = directory;
322}
323
324QUrl QXdgDesktopPortalFileDialog::directory() const
325{
326 Q_D(const QXdgDesktopPortalFileDialog);
327
328 if (d->nativeFileDialog && useNativeFileDialog())
329 return d->nativeFileDialog->directory();
330
331 return d->directory;
332}
333
334void QXdgDesktopPortalFileDialog::selectFile(const QUrl &filename)
335{
336 Q_D(QXdgDesktopPortalFileDialog);
337
338 if (d->nativeFileDialog) {
339 d->nativeFileDialog->setOptions(options());
340 d->nativeFileDialog->selectFile(filename);
341 }
342
343 d->selectedFiles << filename.path();
344}
345
346QList<QUrl> QXdgDesktopPortalFileDialog::selectedFiles() const
347{
348 Q_D(const QXdgDesktopPortalFileDialog);
349
350 if (d->nativeFileDialog && useNativeFileDialog())
351 return d->nativeFileDialog->selectedFiles();
352
353 QList<QUrl> files;
354 for (const QString &file : d->selectedFiles) {
355 files << QUrl(file);
356 }
357 return files;
358}
359
360void QXdgDesktopPortalFileDialog::setFilter()
361{
362 Q_D(QXdgDesktopPortalFileDialog);
363
364 if (d->nativeFileDialog) {
365 d->nativeFileDialog->setOptions(options());
366 d->nativeFileDialog->setFilter();
367 }
368}
369
370void QXdgDesktopPortalFileDialog::selectMimeTypeFilter(const QString &filter)
371{
372 Q_D(QXdgDesktopPortalFileDialog);
373 if (d->nativeFileDialog) {
374 d->nativeFileDialog->setOptions(options());
375 d->nativeFileDialog->selectMimeTypeFilter(filter);
376 }
377}
378
379QString QXdgDesktopPortalFileDialog::selectedMimeTypeFilter() const
380{
381 Q_D(const QXdgDesktopPortalFileDialog);
382 return d->selectedMimeTypeFilter;
383}
384
385void QXdgDesktopPortalFileDialog::selectNameFilter(const QString &filter)
386{
387 Q_D(QXdgDesktopPortalFileDialog);
388
389 if (d->nativeFileDialog) {
390 d->nativeFileDialog->setOptions(options());
391 d->nativeFileDialog->selectNameFilter(filter);
392 }
393}
394
395QString QXdgDesktopPortalFileDialog::selectedNameFilter() const
396{
397 Q_D(const QXdgDesktopPortalFileDialog);
398 return d->selectedNameFilter;
399}
400
401void QXdgDesktopPortalFileDialog::exec()
402{
403 Q_D(QXdgDesktopPortalFileDialog);
404
405 if (d->nativeFileDialog && useNativeFileDialog()) {
406 d->nativeFileDialog->exec();
407 return;
408 }
409
410 // HACK we have to avoid returning until we emit that the dialog was accepted or rejected
411 d->loop.exec();
412}
413
414void QXdgDesktopPortalFileDialog::hide()
415{
416 Q_D(QXdgDesktopPortalFileDialog);
417
418 if (d->nativeFileDialog)
419 d->nativeFileDialog->hide();
420}
421
422bool QXdgDesktopPortalFileDialog::show(Qt::WindowFlags windowFlags, Qt::WindowModality windowModality, QWindow *parent)
423{
424 Q_D(QXdgDesktopPortalFileDialog);
425
426 initializeDialog();
427
428 if (d->nativeFileDialog && useNativeFileDialog(fallbackType: OpenFallback))
429 return d->nativeFileDialog->show(windowFlags, windowModality, parent);
430
431 openPortal(windowFlags, windowModality, parent);
432
433 return true;
434}
435
436void QXdgDesktopPortalFileDialog::gotResponse(uint response, const QVariantMap &results)
437{
438 Q_D(QXdgDesktopPortalFileDialog);
439
440 if (!response) {
441 if (results.contains(key: "uris"_L1))
442 d->selectedFiles = results.value(key: "uris"_L1).toStringList();
443
444 if (results.contains(key: "current_filter"_L1)) {
445 const Filter selectedFilter = qdbus_cast<Filter>(v: results.value(QStringLiteral("current_filter")));
446 if (!selectedFilter.filterConditions.empty() && selectedFilter.filterConditions[0].type == MimeType) {
447 // s.a. QXdgDesktopPortalFileDialog::openPortal which basically does the inverse
448 d->selectedMimeTypeFilter = selectedFilter.filterConditions[0].pattern;
449 d->selectedNameFilter.clear();
450 } else {
451 d->selectedNameFilter = d->userVisibleToNameFilter.value(key: selectedFilter.name);
452 d->selectedMimeTypeFilter.clear();
453 }
454 }
455 Q_EMIT accept();
456 } else {
457 Q_EMIT reject();
458 }
459}
460
461bool QXdgDesktopPortalFileDialog::useNativeFileDialog(QXdgDesktopPortalFileDialog::FallbackType fallbackType) const
462{
463 Q_D(const QXdgDesktopPortalFileDialog);
464
465 if (d->failedToOpen && fallbackType != OpenFallback)
466 return true;
467
468 if (d->fileChooserPortalVersion < 3) {
469 if (options()->fileMode() == QFileDialogOptions::Directory)
470 return true;
471 else if (options()->fileMode() == QFileDialogOptions::DirectoryOnly)
472 return true;
473 }
474
475 return false;
476}
477
478QT_END_NAMESPACE
479
480#include "moc_qxdgdesktopportalfiledialog_p.cpp"
481

Provided by KDAB

Privacy Policy
Learn Advanced QML with KDAB
Find out more

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