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