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
58QT_BEGIN_NAMESPACE
59
60QDBusArgument &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
68const 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
81QDBusArgument &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
89const 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
102class QXdgDesktopPortalFileDialogPrivate
103{
104public:
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
127QXdgDesktopPortalFileDialog::QXdgDesktopPortalFileDialog(QPlatformFileDialogHelper *nativeFileDialog)
128 : QPlatformFileDialogHelper()
129 , d_ptr(new QXdgDesktopPortalFileDialogPrivate(nativeFileDialog))
130{
131 Q_D(QXdgDesktopPortalFileDialog);
132
133 if (d->nativeFileDialog) {
134 connect(d->nativeFileDialog, SIGNAL(accept()), this, SIGNAL(accept()));
135 connect(d->nativeFileDialog, SIGNAL(reject()), this, SIGNAL(reject()));
136 }
137}
138
139QXdgDesktopPortalFileDialog::~QXdgDesktopPortalFileDialog()
140{
141}
142
143void 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(QFileDialogOptions::Accept))
157 d->acceptLabel = options()->labelText(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
180void QXdgDesktopPortalFileDialog::openPortal()
181{
182 Q_D(QXdgDesktopPortalFileDialog);
183
184 QDBusMessage message = QDBusMessage::createMethodCall(QLatin1String("org.freedesktop.portal.Desktop"),
185 QLatin1String("/org/freedesktop/portal/desktop"),
186 QLatin1String("org.freedesktop.portal.FileChooser"),
187 d->saveFile ? QLatin1String("SaveFile") : QLatin1String("OpenFile"));
188 QString parentWindowId = QLatin1String("x11:") + QString::number(d->winId);
189
190 QVariantMap options;
191 if (!d->acceptLabel.isEmpty())
192 options.insert(QLatin1String("accept_label"), d->acceptLabel);
193
194 options.insert(QLatin1String("modal"), d->modal);
195 options.insert(QLatin1String("multiple"), d->multipleFiles);
196 options.insert(QLatin1String("directory"), d->directoryMode);
197
198 if (d->saveFile) {
199 if (!d->directory.isEmpty())
200 options.insert(QLatin1String("current_folder"), QFile::encodeName(d->directory).append('\0'));
201
202 if (!d->selectedFiles.isEmpty())
203 options.insert(QLatin1String("current_file"), QFile::encodeName(d->selectedFiles.first()).append('\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(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(nameFilter);
247 if (match.hasMatch()) {
248 QString userVisibleName = match.captured(1);
249 QStringList filterStrings = match.captured(2).split(QLatin1Char(' '), 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(userVisibleName, 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(QLatin1String("filters"), QVariant::fromValue(filterList));
280
281 if (selectedFilterIndex != -1)
282 options.insert(QLatin1String("current_filter"), QVariant::fromValue(filterList[selectedFilterIndex]));
283
284 options.insert(QLatin1String("handle_token"), QStringLiteral("qt%1").arg(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(watcher, &QDBusPendingCallWatcher::finished, this, [this] (QDBusPendingCallWatcher *watcher) {
294 QDBusPendingReply<QDBusObjectPath> reply = *watcher;
295 if (reply.isError()) {
296 Q_EMIT reject();
297 } else {
298 QDBusConnection::sessionBus().connect(nullptr,
299 reply.value().path(),
300 QLatin1String("org.freedesktop.portal.Request"),
301 QLatin1String("Response"),
302 this,
303 SLOT(gotResponse(uint,QVariantMap)));
304 }
305 });
306}
307
308bool QXdgDesktopPortalFileDialog::defaultNameFilterDisables() const
309{
310 return false;
311}
312
313void 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
325QUrl 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
335void 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
347QList<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
361void QXdgDesktopPortalFileDialog::setFilter()
362{
363 Q_D(QXdgDesktopPortalFileDialog);
364
365 if (d->nativeFileDialog) {
366 d->nativeFileDialog->setOptions(options());
367 d->nativeFileDialog->setFilter();
368 }
369}
370
371void 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
380QString QXdgDesktopPortalFileDialog::selectedMimeTypeFilter() const
381{
382 Q_D(const QXdgDesktopPortalFileDialog);
383 return d->selectedMimeTypeFilter;
384}
385
386void 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
396QString QXdgDesktopPortalFileDialog::selectedNameFilter() const
397{
398 Q_D(const QXdgDesktopPortalFileDialog);
399 return d->selectedNameFilter;
400}
401
402void 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(this, SIGNAL(accept()), SLOT(quit()));
414 loop.connect(this, SIGNAL(reject()), SLOT(quit()));
415 loop.exec();
416}
417
418void QXdgDesktopPortalFileDialog::hide()
419{
420 Q_D(QXdgDesktopPortalFileDialog);
421
422 if (d->nativeFileDialog)
423 d->nativeFileDialog->hide();
424}
425
426bool 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
443void QXdgDesktopPortalFileDialog::gotResponse(uint response, const QVariantMap &results)
444{
445 Q_D(QXdgDesktopPortalFileDialog);
446
447 if (!response) {
448 if (results.contains(QLatin1String("uris")))
449 d->selectedFiles = results.value(QLatin1String("uris")).toStringList();
450
451 if (results.contains(QLatin1String("current_filter"))) {
452 const Filter selectedFilter = qdbus_cast<Filter>(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(selectedFilter.name);
459 d->selectedMimeTypeFilter.clear();
460 }
461 }
462 Q_EMIT accept();
463 } else {
464 Q_EMIT reject();
465 }
466}
467
468QT_END_NAMESPACE
469