File indexing completed on 2024-05-05 17:42:26

0001 /*
0002 
0003     SPDX-FileCopyrightText: 2017-2018 Red Hat Inc
0004     Contact: https://www.qt.io/licensing/
0005 
0006     This file is part of the plugins of the Qt Toolkit.
0007 
0008     SPDX-License-Identifier: LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KFQF-Accepted-GPL OR LicenseRef-Qt-Commercial
0009 
0010 */
0011 
0012 #include "qxdgdesktopportalfiledialog_p.h"
0013 
0014 #include <qeventloop.h>
0015 
0016 #include <QDBusConnection>
0017 #include <QDBusMessage>
0018 #include <QDBusPendingCall>
0019 #include <QDBusPendingCallWatcher>
0020 #include <QDBusPendingReply>
0021 #include <QtDBus>
0022 
0023 #include <QFile>
0024 #include <QMetaType>
0025 #include <QMimeDatabase>
0026 #include <QMimeType>
0027 #include <QRandomGenerator>
0028 #include <QWindow>
0029 
0030 QT_BEGIN_NAMESPACE
0031 
0032 QDBusArgument &operator<<(QDBusArgument &arg, const QXdgDesktopPortalFileDialog::FilterCondition &filterCondition)
0033 {
0034     arg.beginStructure();
0035     arg << filterCondition.type << filterCondition.pattern;
0036     arg.endStructure();
0037     return arg;
0038 }
0039 
0040 const QDBusArgument &operator>>(const QDBusArgument &arg, QXdgDesktopPortalFileDialog::FilterCondition &filterCondition)
0041 {
0042     uint type;
0043     QString filterPattern;
0044     arg.beginStructure();
0045     arg >> type >> filterPattern;
0046     filterCondition.type = (QXdgDesktopPortalFileDialog::ConditionType)type;
0047     filterCondition.pattern = filterPattern;
0048     arg.endStructure();
0049 
0050     return arg;
0051 }
0052 
0053 QDBusArgument &operator<<(QDBusArgument &arg, const QXdgDesktopPortalFileDialog::Filter filter)
0054 {
0055     arg.beginStructure();
0056     arg << filter.name << filter.filterConditions;
0057     arg.endStructure();
0058     return arg;
0059 }
0060 
0061 const QDBusArgument &operator>>(const QDBusArgument &arg, QXdgDesktopPortalFileDialog::Filter &filter)
0062 {
0063     QString name;
0064     QXdgDesktopPortalFileDialog::FilterConditionList filterConditions;
0065     arg.beginStructure();
0066     arg >> name >> filterConditions;
0067     filter.name = name;
0068     filter.filterConditions = filterConditions;
0069     arg.endStructure();
0070 
0071     return arg;
0072 }
0073 
0074 class QXdgDesktopPortalFileDialogPrivate
0075 {
0076 public:
0077     QXdgDesktopPortalFileDialogPrivate(QPlatformFileDialogHelper *nativeFileDialog)
0078         : nativeFileDialog(nativeFileDialog)
0079     {
0080     }
0081 
0082     WId winId = 0;
0083     bool modal = false;
0084     bool multipleFiles = false;
0085     bool selectDirectory = false;
0086     bool saveFile = false;
0087     QString acceptLabel;
0088     QUrl directory;
0089     QString title;
0090     QStringList nameFilters;
0091     QStringList mimeTypesFilters;
0092     QList<QUrl> selectedFiles;
0093     QPlatformFileDialogHelper *nativeFileDialog = nullptr;
0094 };
0095 
0096 QXdgDesktopPortalFileDialog::QXdgDesktopPortalFileDialog(QPlatformFileDialogHelper *nativeFileDialog)
0097     : QPlatformFileDialogHelper()
0098     , d_ptr(new QXdgDesktopPortalFileDialogPrivate(nativeFileDialog))
0099 {
0100     Q_D(QXdgDesktopPortalFileDialog);
0101 
0102     if (d->nativeFileDialog) {
0103         connect(d->nativeFileDialog, SIGNAL(accept()), this, SIGNAL(accept()));
0104         connect(d->nativeFileDialog, SIGNAL(reject()), this, SIGNAL(reject()));
0105     }
0106 }
0107 
0108 QXdgDesktopPortalFileDialog::~QXdgDesktopPortalFileDialog()
0109 {
0110 }
0111 
0112 void QXdgDesktopPortalFileDialog::initializeDialog()
0113 {
0114     Q_D(QXdgDesktopPortalFileDialog);
0115 
0116     if (d->nativeFileDialog)
0117         d->nativeFileDialog->setOptions(options());
0118 
0119     if (options()->fileMode() == QFileDialogOptions::ExistingFiles)
0120         d->multipleFiles = true;
0121 
0122     if (options()->fileMode() == QFileDialogOptions::Directory || options()->fileMode() == QFileDialogOptions::DirectoryOnly) {
0123         d->selectDirectory = true;
0124     }
0125 
0126     if (options()->isLabelExplicitlySet(QFileDialogOptions::Accept))
0127         d->acceptLabel = options()->labelText(QFileDialogOptions::Accept);
0128 
0129     if (!options()->windowTitle().isEmpty())
0130         d->title = options()->windowTitle();
0131 
0132     if (options()->acceptMode() == QFileDialogOptions::AcceptSave)
0133         d->saveFile = true;
0134 
0135     if (!options()->nameFilters().isEmpty())
0136         d->nameFilters = options()->nameFilters();
0137 
0138     if (!options()->mimeTypeFilters().isEmpty())
0139         d->mimeTypesFilters = options()->mimeTypeFilters();
0140 
0141     setDirectory(options()->initialDirectory());
0142 }
0143 
0144 void QXdgDesktopPortalFileDialog::openPortal()
0145 {
0146     Q_D(const QXdgDesktopPortalFileDialog);
0147 
0148     QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.portal.Desktop"),
0149                                                           QStringLiteral("/org/freedesktop/portal/desktop"),
0150                                                           QStringLiteral("org.freedesktop.portal.FileChooser"),
0151                                                           d->saveFile ? QStringLiteral("SaveFile") : QStringLiteral("OpenFile"));
0152     QString parentWindowId = QStringLiteral("x11:") + QString::number(d->winId, 16);
0153 
0154     QVariantMap options;
0155     if (!d->acceptLabel.isEmpty())
0156         options.insert(QStringLiteral("accept_label"), d->acceptLabel);
0157 
0158     options.insert(QStringLiteral("modal"), d->modal);
0159     options.insert(QStringLiteral("multiple"), d->multipleFiles);
0160     options.insert(QStringLiteral("directory"), d->selectDirectory);
0161 
0162     if (d->saveFile) {
0163         if (!d->directory.isEmpty())
0164             options.insert(QStringLiteral("current_folder"), QFile::encodeName(d->directory.toLocalFile()).append('\0'));
0165 
0166         if (!d->selectedFiles.isEmpty())
0167             options.insert(QStringLiteral("current_file"), QFile::encodeName(d->selectedFiles.first().toLocalFile()).append('\0'));
0168     }
0169 
0170     // Insert filters
0171     qDBusRegisterMetaType<FilterCondition>();
0172     qDBusRegisterMetaType<FilterConditionList>();
0173     qDBusRegisterMetaType<Filter>();
0174     qDBusRegisterMetaType<FilterList>();
0175 
0176     FilterList filterList;
0177 
0178     if (!d->mimeTypesFilters.isEmpty()) {
0179         for (const QString &mimeTypefilter : d->mimeTypesFilters) {
0180             QMimeDatabase mimeDatabase;
0181             QMimeType mimeType = mimeDatabase.mimeTypeForName(mimeTypefilter);
0182 
0183             // Creates e.g. (1, "image/png")
0184             FilterCondition filterCondition;
0185             filterCondition.type = MimeType;
0186             filterCondition.pattern = mimeTypefilter;
0187 
0188             // Creates e.g. [((1, "image/png"))]
0189             FilterConditionList filterConditions;
0190             filterConditions << filterCondition;
0191 
0192             // Creates e.g. [("Images", [((1, "image/png"))])]
0193             Filter filter;
0194             filter.name = mimeType.comment();
0195             filter.filterConditions = filterConditions;
0196 
0197             filterList << filter;
0198         }
0199     } else if (!d->nameFilters.isEmpty()) {
0200         for (const QString &filter : d->nameFilters) {
0201             // Do parsing:
0202             // Supported format is ("Images (*.png *.jpg)")
0203             QRegularExpression regexp(QString::fromLatin1(QPlatformFileDialogHelper::filterRegExp));
0204             QRegularExpressionMatch match = regexp.match(filter);
0205             if (match.hasMatch()) {
0206                 QString userVisibleName = match.captured(1);
0207                 QStringList filterStrings = match.captured(2).split(QLatin1Char(' '), Qt::SkipEmptyParts);
0208 
0209                 FilterConditionList filterConditions;
0210                 for (const QString &filterString : qAsConst(filterStrings)) {
0211                     FilterCondition filterCondition;
0212                     filterCondition.type = GlobalPattern;
0213                     filterCondition.pattern = filterString;
0214                     filterConditions << filterCondition;
0215                 }
0216 
0217                 Filter filter;
0218                 filter.name = userVisibleName;
0219                 filter.filterConditions = filterConditions;
0220 
0221                 filterList << filter;
0222             }
0223         }
0224     }
0225 
0226     if (!filterList.isEmpty())
0227         options.insert(QStringLiteral("filters"), QVariant::fromValue(filterList));
0228 
0229     options.insert(QStringLiteral("handle_token"), QStringLiteral("qt%1").arg(QRandomGenerator::global()->generate()));
0230 
0231     // TODO choices a(ssa(ss)s)
0232     // List of serialized combo boxes to add to the file chooser.
0233 
0234     message << parentWindowId << d->title << options;
0235 
0236     QDBusPendingCall pendingCall = QDBusConnection::sessionBus().asyncCall(message);
0237     QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pendingCall);
0238     connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) {
0239         QDBusPendingReply<QDBusObjectPath> reply = *watcher;
0240         if (reply.isError()) {
0241             Q_EMIT reject();
0242         } else {
0243             QDBusConnection::sessionBus().connect({},
0244                                                   reply.value().path(),
0245                                                   QStringLiteral("org.freedesktop.portal.Request"),
0246                                                   QStringLiteral("Response"),
0247                                                   this,
0248                                                   SLOT(gotResponse(uint, QVariantMap)));
0249         }
0250     });
0251 }
0252 
0253 bool QXdgDesktopPortalFileDialog::defaultNameFilterDisables() const
0254 {
0255     return false;
0256 }
0257 
0258 void QXdgDesktopPortalFileDialog::setDirectory(const QUrl &directory)
0259 {
0260     Q_D(QXdgDesktopPortalFileDialog);
0261 
0262     if (d->nativeFileDialog) {
0263         d->nativeFileDialog->setOptions(options());
0264         d->nativeFileDialog->setDirectory(directory);
0265     }
0266 
0267     d->directory = directory;
0268 }
0269 
0270 QUrl QXdgDesktopPortalFileDialog::directory() const
0271 {
0272     Q_D(const QXdgDesktopPortalFileDialog);
0273 
0274     if (d->nativeFileDialog && (options()->fileMode() == QFileDialogOptions::Directory || options()->fileMode() == QFileDialogOptions::DirectoryOnly))
0275         return d->nativeFileDialog->directory();
0276 
0277     return d->directory;
0278 }
0279 
0280 void QXdgDesktopPortalFileDialog::selectFile(const QUrl &filename)
0281 {
0282     Q_D(QXdgDesktopPortalFileDialog);
0283 
0284     if (d->nativeFileDialog) {
0285         d->nativeFileDialog->setOptions(options());
0286         d->nativeFileDialog->selectFile(filename);
0287     }
0288 
0289     d->selectedFiles << filename;
0290 }
0291 
0292 QList<QUrl> QXdgDesktopPortalFileDialog::selectedFiles() const
0293 {
0294     Q_D(const QXdgDesktopPortalFileDialog);
0295 
0296     if (d->nativeFileDialog && (options()->fileMode() == QFileDialogOptions::Directory || options()->fileMode() == QFileDialogOptions::DirectoryOnly))
0297         return d->nativeFileDialog->selectedFiles();
0298 
0299     return d->selectedFiles;
0300 }
0301 
0302 void QXdgDesktopPortalFileDialog::setFilter()
0303 {
0304     Q_D(QXdgDesktopPortalFileDialog);
0305 
0306     if (d->nativeFileDialog) {
0307         d->nativeFileDialog->setOptions(options());
0308         d->nativeFileDialog->setFilter();
0309     }
0310 }
0311 
0312 void QXdgDesktopPortalFileDialog::selectNameFilter(const QString &filter)
0313 {
0314     Q_D(QXdgDesktopPortalFileDialog);
0315 
0316     if (d->nativeFileDialog) {
0317         d->nativeFileDialog->setOptions(options());
0318         d->nativeFileDialog->selectNameFilter(filter);
0319     }
0320 }
0321 
0322 QString QXdgDesktopPortalFileDialog::selectedNameFilter() const
0323 {
0324     // TODO
0325     return QString();
0326 }
0327 
0328 void QXdgDesktopPortalFileDialog::exec()
0329 {
0330     Q_D(QXdgDesktopPortalFileDialog);
0331 
0332     if (d->nativeFileDialog && (options()->fileMode() == QFileDialogOptions::Directory || options()->fileMode() == QFileDialogOptions::DirectoryOnly)) {
0333         d->nativeFileDialog->exec();
0334         return;
0335     }
0336 
0337     // HACK we have to avoid returning until we emit that the dialog was accepted or rejected
0338     QEventLoop loop;
0339     QObject::connect(this, &QXdgDesktopPortalFileDialog::accept, &loop, &QEventLoop::quit);
0340     QObject::connect(this, &QXdgDesktopPortalFileDialog::reject, &loop, &QEventLoop::quit);
0341     loop.exec();
0342 }
0343 
0344 void QXdgDesktopPortalFileDialog::hide()
0345 {
0346     Q_D(QXdgDesktopPortalFileDialog);
0347 
0348     if (d->nativeFileDialog)
0349         d->nativeFileDialog->hide();
0350 }
0351 
0352 bool QXdgDesktopPortalFileDialog::show(Qt::WindowFlags windowFlags, Qt::WindowModality windowModality, QWindow *parent)
0353 {
0354     Q_D(QXdgDesktopPortalFileDialog);
0355 
0356     initializeDialog();
0357 
0358     d->modal = windowModality != Qt::NonModal;
0359     d->winId = parent ? parent->winId() : 0;
0360 
0361     if (d->nativeFileDialog && (options()->fileMode() == QFileDialogOptions::Directory || options()->fileMode() == QFileDialogOptions::DirectoryOnly))
0362         return d->nativeFileDialog->show(windowFlags, windowModality, parent);
0363 
0364     openPortal();
0365 
0366     return true;
0367 }
0368 
0369 void QXdgDesktopPortalFileDialog::gotResponse(uint response, const QVariantMap &results)
0370 {
0371     Q_D(QXdgDesktopPortalFileDialog);
0372 
0373     if (!response) {
0374         if (results.contains(QStringLiteral("uris"))) {
0375             const QStringList uris = results.value(QStringLiteral("uris")).toStringList();
0376             d->selectedFiles.clear();
0377             d->selectedFiles.reserve(uris.size());
0378             for (const QString &uri : uris) {
0379                 // uris are expected to have proper "file:" scheme set
0380                 d->selectedFiles.append(QUrl(uri));
0381             }
0382         }
0383         Q_EMIT accept();
0384     } else {
0385         Q_EMIT reject();
0386     }
0387 }
0388 
0389 QT_END_NAMESPACE