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 
operator <<(QDBusArgument & arg,const QXdgDesktopPortalFileDialog::FilterCondition & filterCondition)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 
operator >>(const QDBusArgument & arg,QXdgDesktopPortalFileDialog::FilterCondition & filterCondition)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 
operator <<(QDBusArgument & arg,const QXdgDesktopPortalFileDialog::Filter filter)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 
operator >>(const QDBusArgument & arg,QXdgDesktopPortalFileDialog::Filter & filter)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:
QXdgDesktopPortalFileDialogPrivate(QPlatformFileDialogHelper * nativeFileDialog)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 
QXdgDesktopPortalFileDialog(QPlatformFileDialogHelper * nativeFileDialog)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(d->nativeFileDialog, SIGNAL(accept()), this, SIGNAL(accept()));
135         connect(d->nativeFileDialog, SIGNAL(reject()), this, SIGNAL(reject()));
136     }
137 }
138 
~QXdgDesktopPortalFileDialog()139 QXdgDesktopPortalFileDialog::~QXdgDesktopPortalFileDialog()
140 {
141 }
142 
initializeDialog()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(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 
openPortal()180 void 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         watcher->deleteLater();
306     });
307 }
308 
defaultNameFilterDisables() const309 bool QXdgDesktopPortalFileDialog::defaultNameFilterDisables() const
310 {
311     return false;
312 }
313 
setDirectory(const QUrl & directory)314 void QXdgDesktopPortalFileDialog::setDirectory(const QUrl &directory)
315 {
316     Q_D(QXdgDesktopPortalFileDialog);
317 
318     if (d->nativeFileDialog) {
319         d->nativeFileDialog->setOptions(options());
320         d->nativeFileDialog->setDirectory(directory);
321     }
322 
323     d->directory = directory.path();
324 }
325 
directory() const326 QUrl QXdgDesktopPortalFileDialog::directory() const
327 {
328     Q_D(const QXdgDesktopPortalFileDialog);
329 
330     if (d->nativeFileDialog && (options()->fileMode() == QFileDialogOptions::Directory || options()->fileMode() == QFileDialogOptions::DirectoryOnly))
331         return d->nativeFileDialog->directory();
332 
333     return d->directory;
334 }
335 
selectFile(const QUrl & filename)336 void QXdgDesktopPortalFileDialog::selectFile(const QUrl &filename)
337 {
338     Q_D(QXdgDesktopPortalFileDialog);
339 
340     if (d->nativeFileDialog) {
341         d->nativeFileDialog->setOptions(options());
342         d->nativeFileDialog->selectFile(filename);
343     }
344 
345     d->selectedFiles << filename.path();
346 }
347 
selectedFiles() const348 QList<QUrl> QXdgDesktopPortalFileDialog::selectedFiles() const
349 {
350     Q_D(const QXdgDesktopPortalFileDialog);
351 
352     if (d->nativeFileDialog && (options()->fileMode() == QFileDialogOptions::Directory || options()->fileMode() == QFileDialogOptions::DirectoryOnly))
353         return d->nativeFileDialog->selectedFiles();
354 
355     QList<QUrl> files;
356     for (const QString &file : d->selectedFiles) {
357         files << QUrl(file);
358     }
359     return files;
360 }
361 
setFilter()362 void QXdgDesktopPortalFileDialog::setFilter()
363 {
364     Q_D(QXdgDesktopPortalFileDialog);
365 
366     if (d->nativeFileDialog) {
367         d->nativeFileDialog->setOptions(options());
368         d->nativeFileDialog->setFilter();
369     }
370 }
371 
selectMimeTypeFilter(const QString & filter)372 void QXdgDesktopPortalFileDialog::selectMimeTypeFilter(const QString &filter)
373 {
374     Q_D(QXdgDesktopPortalFileDialog);
375     if (d->nativeFileDialog) {
376         d->nativeFileDialog->setOptions(options());
377         d->nativeFileDialog->selectMimeTypeFilter(filter);
378     }
379 }
380 
selectedMimeTypeFilter() const381 QString QXdgDesktopPortalFileDialog::selectedMimeTypeFilter() const
382 {
383     Q_D(const QXdgDesktopPortalFileDialog);
384     return d->selectedMimeTypeFilter;
385 }
386 
selectNameFilter(const QString & filter)387 void QXdgDesktopPortalFileDialog::selectNameFilter(const QString &filter)
388 {
389     Q_D(QXdgDesktopPortalFileDialog);
390 
391     if (d->nativeFileDialog) {
392         d->nativeFileDialog->setOptions(options());
393         d->nativeFileDialog->selectNameFilter(filter);
394     }
395 }
396 
selectedNameFilter() const397 QString QXdgDesktopPortalFileDialog::selectedNameFilter() const
398 {
399     Q_D(const QXdgDesktopPortalFileDialog);
400     return d->selectedNameFilter;
401 }
402 
exec()403 void QXdgDesktopPortalFileDialog::exec()
404 {
405     Q_D(QXdgDesktopPortalFileDialog);
406 
407     if (d->nativeFileDialog && (options()->fileMode() == QFileDialogOptions::Directory || options()->fileMode() == QFileDialogOptions::DirectoryOnly)) {
408         d->nativeFileDialog->exec();
409         return;
410     }
411 
412     // HACK we have to avoid returning until we emit that the dialog was accepted or rejected
413     QEventLoop loop;
414     loop.connect(this, SIGNAL(accept()), SLOT(quit()));
415     loop.connect(this, SIGNAL(reject()), SLOT(quit()));
416     loop.exec();
417 }
418 
hide()419 void QXdgDesktopPortalFileDialog::hide()
420 {
421     Q_D(QXdgDesktopPortalFileDialog);
422 
423     if (d->nativeFileDialog)
424         d->nativeFileDialog->hide();
425 }
426 
show(Qt::WindowFlags windowFlags,Qt::WindowModality windowModality,QWindow * parent)427 bool QXdgDesktopPortalFileDialog::show(Qt::WindowFlags windowFlags, Qt::WindowModality windowModality, QWindow *parent)
428 {
429     Q_D(QXdgDesktopPortalFileDialog);
430 
431     initializeDialog();
432 
433     d->modal = windowModality != Qt::NonModal;
434     d->winId = parent ? parent->winId() : 0;
435 
436     if (d->nativeFileDialog && (options()->fileMode() == QFileDialogOptions::Directory || options()->fileMode() == QFileDialogOptions::DirectoryOnly))
437         return d->nativeFileDialog->show(windowFlags, windowModality, parent);
438 
439     openPortal();
440 
441     return true;
442 }
443 
gotResponse(uint response,const QVariantMap & results)444 void QXdgDesktopPortalFileDialog::gotResponse(uint response, const QVariantMap &results)
445 {
446     Q_D(QXdgDesktopPortalFileDialog);
447 
448     if (!response) {
449         if (results.contains(QLatin1String("uris")))
450             d->selectedFiles = results.value(QLatin1String("uris")).toStringList();
451 
452         if (results.contains(QLatin1String("current_filter"))) {
453             const Filter selectedFilter = qdbus_cast<Filter>(results.value(QStringLiteral("current_filter")));
454             if (!selectedFilter.filterConditions.empty() && selectedFilter.filterConditions[0].type == MimeType) {
455                 // s.a. QXdgDesktopPortalFileDialog::openPortal which basically does the inverse
456                 d->selectedMimeTypeFilter = selectedFilter.filterConditions[0].pattern;
457                 d->selectedNameFilter.clear();
458             } else {
459                 d->selectedNameFilter = d->userVisibleToNameFilter.value(selectedFilter.name);
460                 d->selectedMimeTypeFilter.clear();
461             }
462         }
463         Q_EMIT accept();
464     } else {
465         Q_EMIT reject();
466     }
467 }
468 
469 QT_END_NAMESPACE
470