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 | |
27 | QT_BEGIN_NAMESPACE |
28 | |
29 | using namespace Qt::StringLiterals; |
30 | |
31 | QDBusArgument &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 | |
39 | const 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 | |
52 | QDBusArgument &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 | |
60 | const 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 | |
73 | class QXdgDesktopPortalFileDialogPrivate |
74 | { |
75 | public: |
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 | |
100 | QXdgDesktopPortalFileDialog::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 | |
115 | QXdgDesktopPortalFileDialog::~QXdgDesktopPortalFileDialog() |
116 | { |
117 | } |
118 | |
119 | void 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 | |
156 | void 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 | |
304 | bool QXdgDesktopPortalFileDialog::defaultNameFilterDisables() const |
305 | { |
306 | return false; |
307 | } |
308 | |
309 | void 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 | |
321 | QUrl 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 | |
331 | void 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 | |
343 | QList<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 | |
357 | void QXdgDesktopPortalFileDialog::setFilter() |
358 | { |
359 | Q_D(QXdgDesktopPortalFileDialog); |
360 | |
361 | if (d->nativeFileDialog) { |
362 | d->nativeFileDialog->setOptions(options()); |
363 | d->nativeFileDialog->setFilter(); |
364 | } |
365 | } |
366 | |
367 | void 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 | |
376 | QString QXdgDesktopPortalFileDialog::selectedMimeTypeFilter() const |
377 | { |
378 | Q_D(const QXdgDesktopPortalFileDialog); |
379 | return d->selectedMimeTypeFilter; |
380 | } |
381 | |
382 | void 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 | |
392 | QString QXdgDesktopPortalFileDialog::selectedNameFilter() const |
393 | { |
394 | Q_D(const QXdgDesktopPortalFileDialog); |
395 | return d->selectedNameFilter; |
396 | } |
397 | |
398 | void 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 | |
411 | void QXdgDesktopPortalFileDialog::hide() |
412 | { |
413 | Q_D(QXdgDesktopPortalFileDialog); |
414 | |
415 | if (d->nativeFileDialog) |
416 | d->nativeFileDialog->hide(); |
417 | } |
418 | |
419 | bool 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 | |
433 | void 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 | |
458 | bool 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 | |
475 | QT_END_NAMESPACE |
476 | |
477 | #include "moc_qxdgdesktopportalfiledialog_p.cpp" |
478 | |