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