File indexing completed on 2024-12-08 11:01:45

0001 /*
0002  * SPDX-FileCopyrightText: 2016-2018 Red Hat Inc
0003  *
0004  * SPDX-License-Identifier: LGPL-2.0-or-later
0005  *
0006  * SPDX-FileCopyrightText: 2016-2018 Jan Grulich <jgrulich@redhat.com>
0007  * SPDX-FileCopyrightText: 2022 Harald Sitter <sitter@kde.org>
0008  */
0009 
0010 #include "filechooser.h"
0011 #include "filechooser_debug.h"
0012 #include "utils.h"
0013 
0014 #include <QDBusArgument>
0015 #include <QDBusMetaType>
0016 #include <QDialogButtonBox>
0017 #include <QFile>
0018 #include <QFileDialog>
0019 #include <QFileInfo>
0020 #include <QGridLayout>
0021 #include <QLabel>
0022 #include <QPushButton>
0023 #include <QQmlApplicationEngine>
0024 #include <QStandardPaths>
0025 #include <QUrl>
0026 #include <QVBoxLayout>
0027 #include <QWindow>
0028 
0029 #include <KFileFilterCombo>
0030 #include <KFileWidget>
0031 #include <KLocalizedString>
0032 #include <KSharedConfig>
0033 #include <KWindowConfig>
0034 
0035 #include "documents_interface.h"
0036 #include "fuse_interface.h"
0037 #include "request.h"
0038 #include <mobilefiledialog.h>
0039 
0040 // Keep in sync with qflatpakfiledialog from flatpak-platform-plugin
0041 Q_DECLARE_METATYPE(FileChooserPortal::Filter)
0042 Q_DECLARE_METATYPE(FileChooserPortal::Filters)
0043 Q_DECLARE_METATYPE(FileChooserPortal::FilterList)
0044 Q_DECLARE_METATYPE(FileChooserPortal::FilterListList)
0045 // used for options - choices
0046 Q_DECLARE_METATYPE(FileChooserPortal::Choice)
0047 Q_DECLARE_METATYPE(FileChooserPortal::Choices)
0048 Q_DECLARE_METATYPE(FileChooserPortal::Option)
0049 Q_DECLARE_METATYPE(FileChooserPortal::OptionList)
0050 
0051 QDBusArgument &operator<<(QDBusArgument &arg, const FileChooserPortal::Filter &filter)
0052 {
0053     arg.beginStructure();
0054     arg << filter.type << filter.filterString;
0055     arg.endStructure();
0056     return arg;
0057 }
0058 
0059 const QDBusArgument &operator>>(const QDBusArgument &arg, FileChooserPortal::Filter &filter)
0060 {
0061     uint type;
0062     QString filterString;
0063     arg.beginStructure();
0064     arg >> type >> filterString;
0065     filter.type = type;
0066     filter.filterString = filterString;
0067     arg.endStructure();
0068 
0069     return arg;
0070 }
0071 
0072 QDBusArgument &operator<<(QDBusArgument &arg, const FileChooserPortal::FilterList &filterList)
0073 {
0074     arg.beginStructure();
0075     arg << filterList.userVisibleName << filterList.filters;
0076     arg.endStructure();
0077     return arg;
0078 }
0079 
0080 const QDBusArgument &operator>>(const QDBusArgument &arg, FileChooserPortal::FilterList &filterList)
0081 {
0082     QString userVisibleName;
0083     FileChooserPortal::Filters filters;
0084     arg.beginStructure();
0085     arg >> userVisibleName >> filters;
0086     filterList.userVisibleName = userVisibleName;
0087     filterList.filters = filters;
0088     arg.endStructure();
0089 
0090     return arg;
0091 }
0092 
0093 QDBusArgument &operator<<(QDBusArgument &arg, const FileChooserPortal::Choice &choice)
0094 {
0095     arg.beginStructure();
0096     arg << choice.id << choice.value;
0097     arg.endStructure();
0098     return arg;
0099 }
0100 
0101 const QDBusArgument &operator>>(const QDBusArgument &arg, FileChooserPortal::Choice &choice)
0102 {
0103     QString id;
0104     QString value;
0105     arg.beginStructure();
0106     arg >> id >> value;
0107     choice.id = id;
0108     choice.value = value;
0109     arg.endStructure();
0110     return arg;
0111 }
0112 
0113 QDBusArgument &operator<<(QDBusArgument &arg, const FileChooserPortal::Option &option)
0114 {
0115     arg.beginStructure();
0116     arg << option.id << option.label << option.choices << option.initialChoiceId;
0117     arg.endStructure();
0118     return arg;
0119 }
0120 
0121 const QDBusArgument &operator>>(const QDBusArgument &arg, FileChooserPortal::Option &option)
0122 {
0123     QString id;
0124     QString label;
0125     FileChooserPortal::Choices choices;
0126     QString initialChoiceId;
0127     arg.beginStructure();
0128     arg >> id >> label >> choices >> initialChoiceId;
0129     option.id = id;
0130     option.label = label;
0131     option.choices = choices;
0132     option.initialChoiceId = initialChoiceId;
0133     arg.endStructure();
0134     return arg;
0135 }
0136 
0137 static bool isKIOFuseAvailable()
0138 {
0139     static bool available =
0140         QDBusConnection::sessionBus().interface() && QDBusConnection::sessionBus().interface()->activatableServiceNames().value().contains("org.kde.KIOFuse");
0141     return available;
0142 }
0143 
0144 FileDialog::FileDialog(QDialog *parent, Qt::WindowFlags flags)
0145     : QDialog(parent, flags)
0146     , m_fileWidget(new KFileWidget(QUrl(), this))
0147     , m_configGroup(KSharedConfig::openConfig()->group("FileDialogSize"))
0148 {
0149     setLayout(new QVBoxLayout);
0150     layout()->addWidget(m_fileWidget);
0151 
0152     m_buttons = new QDialogButtonBox(this);
0153     m_buttons->addButton(m_fileWidget->okButton(), QDialogButtonBox::AcceptRole);
0154     m_buttons->addButton(m_fileWidget->cancelButton(), QDialogButtonBox::RejectRole);
0155     layout()->addWidget(m_buttons);
0156 
0157     // accept
0158     connect(m_buttons, &QDialogButtonBox::accepted, m_fileWidget, &KFileWidget::slotOk);
0159     connect(m_fileWidget, &KFileWidget::accepted, m_fileWidget, &KFileWidget::accept);
0160     connect(m_fileWidget, &KFileWidget::accepted, this, &QDialog::accept);
0161 
0162     // reject
0163     connect(m_buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
0164     connect(this, &QDialog::rejected, m_fileWidget, &KFileWidget::slotCancel);
0165 
0166     // restore window size
0167     if (m_configGroup.exists()) {
0168         winId(); // ensure there's a window created
0169         KWindowConfig::restoreWindowSize(windowHandle(), m_configGroup);
0170         resize(windowHandle()->size());
0171     }
0172 }
0173 
0174 FileDialog::~FileDialog()
0175 {
0176     // save window size
0177     KWindowConfig::saveWindowSize(windowHandle(), m_configGroup);
0178 }
0179 
0180 FileChooserPortal::FileChooserPortal(QObject *parent)
0181     : QDBusAbstractAdaptor(parent)
0182 {
0183     qDBusRegisterMetaType<Filter>();
0184     qDBusRegisterMetaType<Filters>();
0185     qDBusRegisterMetaType<FilterList>();
0186     qDBusRegisterMetaType<FilterListList>();
0187     qDBusRegisterMetaType<Choice>();
0188     qDBusRegisterMetaType<Choices>();
0189     qDBusRegisterMetaType<Option>();
0190     qDBusRegisterMetaType<OptionList>();
0191 }
0192 
0193 FileChooserPortal::~FileChooserPortal()
0194 {
0195 }
0196 
0197 static QStringList fuseRedirect(QList<QUrl> urls)
0198 {
0199     qCDebug(XdgDesktopPortalKdeFileChooser) << "mounting urls with fuse" << urls;
0200 
0201     OrgKdeKIOFuseVFSInterface kiofuse_iface(QStringLiteral("org.kde.KIOFuse"), QStringLiteral("/org/kde/KIOFuse"), QDBusConnection::sessionBus());
0202     struct MountRequest {
0203         QDBusPendingReply<QString> reply;
0204         int urlIndex;
0205         QString basename;
0206     };
0207     QVector<MountRequest> requests;
0208     requests.reserve(urls.count());
0209     for (int i = 0; i < urls.count(); ++i) {
0210         QUrl url = urls.at(i);
0211         if (!url.isLocalFile()) {
0212             const QString path(url.path());
0213             const int slashes = path.count(QLatin1Char('/'));
0214             QString basename;
0215             if (slashes > 1) {
0216                 url.setPath(path.section(QLatin1Char('/'), 0, slashes - 1));
0217                 basename = path.section(QLatin1Char('/'), slashes, slashes);
0218             }
0219             requests.push_back({kiofuse_iface.mountUrl(url.toString()), i, basename});
0220         }
0221     }
0222 
0223     for (auto &request : requests) {
0224         request.reply.waitForFinished();
0225         if (request.reply.isError()) {
0226             qWarning() << "FUSE request failed:" << request.reply.error();
0227             continue;
0228         }
0229 
0230         urls[request.urlIndex] = QUrl::fromLocalFile(request.reply.value() + QLatin1Char('/') + request.basename);
0231     };
0232 
0233     qCDebug(XdgDesktopPortalKdeFileChooser) << "mounted urls with fuse, maybe" << urls;
0234 
0235     return QUrl::toStringList(urls, QUrl::FullyEncoded);
0236 }
0237 
0238 uint FileChooserPortal::OpenFile(const QDBusObjectPath &handle,
0239                                  const QString &app_id,
0240                                  const QString &parent_window,
0241                                  const QString &title,
0242                                  const QVariantMap &options,
0243                                  QVariantMap &results)
0244 {
0245     Q_UNUSED(app_id);
0246 
0247     qCDebug(XdgDesktopPortalKdeFileChooser) << "OpenFile called with parameters:";
0248     qCDebug(XdgDesktopPortalKdeFileChooser) << "    handle: " << handle.path();
0249     qCDebug(XdgDesktopPortalKdeFileChooser) << "    parent_window: " << parent_window;
0250     qCDebug(XdgDesktopPortalKdeFileChooser) << "    title: " << title;
0251     qCDebug(XdgDesktopPortalKdeFileChooser) << "    options: " << options;
0252 
0253     bool directory = false;
0254     bool modalDialog = true;
0255     bool multipleFiles = false;
0256     QStringList nameFilters;
0257     QStringList mimeTypeFilters;
0258     QString selectedMimeTypeFilter;
0259     // mapping between filter strings and actual filters
0260     QMap<QString, FilterList> allFilters;
0261 
0262     const QString acceptLabel = ExtractAcceptLabel(options);
0263 
0264     if (options.contains(QStringLiteral("modal"))) {
0265         modalDialog = options.value(QStringLiteral("modal")).toBool();
0266     }
0267 
0268     if (options.contains(QStringLiteral("multiple"))) {
0269         multipleFiles = options.value(QStringLiteral("multiple")).toBool();
0270     }
0271 
0272     if (options.contains(QStringLiteral("directory"))) {
0273         directory = options.value(QStringLiteral("directory")).toBool();
0274     }
0275 
0276     ExtractFilters(options, nameFilters, mimeTypeFilters, allFilters, selectedMimeTypeFilter);
0277 
0278     if (isMobile()) {
0279         if (!m_mobileFileDialog) {
0280             qCDebug(XdgDesktopPortalKdeFileChooser) << "Creating file dialog";
0281             m_mobileFileDialog = new MobileFileDialog(this);
0282         }
0283 
0284         m_mobileFileDialog->setTitle(title);
0285 
0286         // Always true when we are opening a file
0287         m_mobileFileDialog->setSelectExisting(true);
0288 
0289         m_mobileFileDialog->setSelectFolder(directory);
0290 
0291         m_mobileFileDialog->setSelectMultiple(multipleFiles);
0292 
0293         // currentName: not implemented
0294 
0295         if (!acceptLabel.isEmpty()) {
0296             m_mobileFileDialog->setAcceptLabel(acceptLabel);
0297         }
0298 
0299         if (!nameFilters.isEmpty()) {
0300             m_mobileFileDialog->setNameFilters(nameFilters);
0301         }
0302 
0303         if (!mimeTypeFilters.isEmpty()) {
0304             m_mobileFileDialog->setMimeTypeFilters(mimeTypeFilters);
0305         }
0306 
0307         uint retCode = m_mobileFileDialog->exec();
0308 
0309         results.insert(QStringLiteral("uris"), fuseRedirect(m_mobileFileDialog->results()));
0310 
0311         return retCode;
0312     }
0313 
0314     // Use QFileDialog for most directory requests to utilize
0315     // plasma-integration's KDirSelectDialog
0316     if (directory && !options.contains(QStringLiteral("choices"))) {
0317         QFileDialog dirDialog;
0318         dirDialog.setWindowTitle(title);
0319         dirDialog.setModal(modalDialog);
0320         dirDialog.setFileMode(QFileDialog::Directory);
0321         dirDialog.setOptions(QFileDialog::ShowDirsOnly);
0322         if (!isKIOFuseAvailable()) {
0323             dirDialog.setSupportedSchemes(QStringList{QStringLiteral("file")});
0324         }
0325         if (!acceptLabel.isEmpty()) {
0326             dirDialog.setLabelText(QFileDialog::Accept, acceptLabel);
0327         }
0328 
0329         dirDialog.winId(); // Trigger window creation
0330         Utils::setParentWindow(&dirDialog, parent_window);
0331         Request::makeClosableDialogRequest(handle, &dirDialog);
0332 
0333         if (dirDialog.exec() != QDialog::Accepted) {
0334             return 1;
0335         }
0336 
0337         const auto urls = dirDialog.selectedUrls();
0338         if (urls.empty()) {
0339             return 2;
0340         }
0341 
0342         results.insert(QStringLiteral("uris"), fuseRedirect(urls));
0343         results.insert(QStringLiteral("writable"), true);
0344 
0345         return 0;
0346     }
0347 
0348     // for handling of options - choices
0349     QScopedPointer<QWidget> optionsWidget;
0350     // to store IDs for choices along with corresponding comboboxes/checkboxes
0351     QMap<QString, QCheckBox *> checkboxes;
0352     QMap<QString, QComboBox *> comboboxes;
0353 
0354     if (options.contains(QStringLiteral("choices"))) {
0355         OptionList optionList = qdbus_cast<OptionList>(options.value(QStringLiteral("choices")));
0356         optionsWidget.reset(CreateChoiceControls(optionList, checkboxes, comboboxes));
0357     }
0358 
0359     QScopedPointer<FileDialog, QScopedPointerDeleteLater> fileDialog(new FileDialog());
0360     Utils::setParentWindow(fileDialog.data(), parent_window);
0361     Request::makeClosableDialogRequest(handle, fileDialog.get());
0362     fileDialog->setWindowTitle(title);
0363     fileDialog->setModal(modalDialog);
0364     KFile::Mode mode = directory ? KFile::Mode::Directory : multipleFiles ? KFile::Mode::Files : KFile::Mode::File;
0365     fileDialog->m_fileWidget->setMode(mode | KFile::Mode::ExistingOnly);
0366     if (!isKIOFuseAvailable()) {
0367         fileDialog->m_fileWidget->setSupportedSchemes(QStringList{QStringLiteral("file")});
0368     }
0369     fileDialog->m_fileWidget->okButton()->setText(!acceptLabel.isEmpty() ? acceptLabel : i18n("Open"));
0370 
0371     bool bMimeFilters = false;
0372     if (!mimeTypeFilters.isEmpty()) {
0373         fileDialog->m_fileWidget->setMimeFilter(mimeTypeFilters, selectedMimeTypeFilter);
0374         bMimeFilters = true;
0375     } else if (!nameFilters.isEmpty()) {
0376         fileDialog->m_fileWidget->setFilter(nameFilters.join(QLatin1Char('\n')));
0377     }
0378 
0379     if (optionsWidget) {
0380         fileDialog->m_fileWidget->setCustomWidget({}, optionsWidget.get());
0381     }
0382 
0383     if (fileDialog->exec() == QDialog::Accepted) {
0384         const auto urls = fileDialog->m_fileWidget->selectedUrls();
0385         if (urls.isEmpty()) {
0386             qCDebug(XdgDesktopPortalKdeFileChooser) << "Failed to open file: no local file selected";
0387             return 2;
0388         }
0389 
0390         results.insert(QStringLiteral("uris"), fuseRedirect(urls));
0391         results.insert(QStringLiteral("writable"), true);
0392 
0393         if (optionsWidget) {
0394             QVariant choices = EvaluateSelectedChoices(checkboxes, comboboxes);
0395             results.insert(QStringLiteral("choices"), choices);
0396         }
0397 
0398         // try to map current filter back to one of the predefined ones
0399         QString selectedFilter;
0400         if (bMimeFilters) {
0401             selectedFilter = fileDialog->m_fileWidget->currentMimeFilter();
0402         } else {
0403             selectedFilter = fileDialog->m_fileWidget->filterWidget()->currentText();
0404         }
0405         if (allFilters.contains(selectedFilter)) {
0406             results.insert(QStringLiteral("current_filter"), QVariant::fromValue<FilterList>(allFilters.value(selectedFilter)));
0407         }
0408 
0409         return 0;
0410     }
0411 
0412     return 1;
0413 }
0414 
0415 enum class Entity { File, Folder };
0416 static QUrl kioUrlFromSandboxPath(const QString &path, Entity entity)
0417 {
0418     if (path.isEmpty()) {
0419         return {};
0420     }
0421 
0422     static QString mountPoint;
0423     if (!mountPoint.isEmpty() && !path.startsWith(mountPoint)) {
0424         return QUrl::fromLocalFile(path);
0425     }
0426 
0427     OrgFreedesktopPortalDocumentsInterface documents_iface(QStringLiteral("org.freedesktop.portal.Documents"),
0428                                                            QStringLiteral("/org/freedesktop/portal/documents"),
0429                                                            QDBusConnection::sessionBus());
0430 
0431     if (mountPoint.isEmpty()) {
0432         mountPoint = QString::fromUtf8(documents_iface.GetMountPoint());
0433         if (!path.startsWith(mountPoint)) {
0434             return QUrl::fromLocalFile(path);
0435         }
0436     }
0437 
0438     QByteArray localFilePath;
0439     switch (entity) {
0440     case Entity::File: // basename of dirpath (= id of the shared document on the portal)
0441         localFilePath = documents_iface.Info(QFileInfo(QFileInfo(path).path()).fileName());
0442         break;
0443     case Entity::Folder: // basename of path
0444         localFilePath = documents_iface.Info(QFileInfo(path).fileName());
0445         break;
0446     }
0447     if (localFilePath.isEmpty()) {
0448         return QUrl::fromLocalFile(path);
0449     }
0450 
0451     if (!isKIOFuseAvailable()) {
0452         return QUrl::fromLocalFile(localFilePath);
0453     }
0454 
0455     OrgKdeKIOFuseVFSInterface fuse_iface(QStringLiteral("org.kde.KIOFuse"), QStringLiteral("/org/kde/KIOFuse"), QDBusConnection::sessionBus());
0456 
0457     QString remoteFilePath = fuse_iface.remoteUrl(localFilePath);
0458     if (remoteFilePath.isEmpty()) {
0459         return QUrl::fromLocalFile(path);
0460     }
0461 
0462     QUrl url(remoteFilePath);
0463     url.setPath(QFileInfo(url.path()).path());
0464     qCDebug(XdgDesktopPortalKdeFileChooser) << "Translated portal url to" << url;
0465     return url;
0466 };
0467 
0468 uint FileChooserPortal::SaveFile(const QDBusObjectPath &handle,
0469                                  const QString &app_id,
0470                                  const QString &parent_window,
0471                                  const QString &title,
0472                                  const QVariantMap &options,
0473                                  QVariantMap &results)
0474 {
0475     Q_UNUSED(app_id);
0476 
0477     qCDebug(XdgDesktopPortalKdeFileChooser) << "SaveFile called with parameters:";
0478     qCDebug(XdgDesktopPortalKdeFileChooser) << "    handle: " << handle.path();
0479     qCDebug(XdgDesktopPortalKdeFileChooser) << "    parent_window: " << parent_window;
0480     qCDebug(XdgDesktopPortalKdeFileChooser) << "    title: " << title;
0481     qCDebug(XdgDesktopPortalKdeFileChooser) << "    options: " << options;
0482 
0483     bool modalDialog = true;
0484     QString currentName;
0485     QString currentFolder;
0486     QString currentFile;
0487     QStringList nameFilters;
0488     QStringList mimeTypeFilters;
0489     QString selectedMimeTypeFilter;
0490     // mapping between filter strings and actual filters
0491     QMap<QString, FilterList> allFilters;
0492 
0493     if (options.contains(QStringLiteral("modal"))) {
0494         modalDialog = options.value(QStringLiteral("modal")).toBool();
0495     }
0496 
0497     const QString acceptLabel = ExtractAcceptLabel(options);
0498 
0499     if (options.contains(QStringLiteral("current_name"))) {
0500         currentName = options.value(QStringLiteral("current_name")).toString();
0501     }
0502 
0503     if (options.contains(QStringLiteral("current_folder"))) {
0504         currentFolder = QFile::decodeName(options.value(QStringLiteral("current_folder")).toByteArray());
0505     }
0506 
0507     if (options.contains(QStringLiteral("current_file"))) {
0508         currentFile = QFile::decodeName(options.value(QStringLiteral("current_file")).toByteArray());
0509     }
0510 
0511     ExtractFilters(options, nameFilters, mimeTypeFilters, allFilters, selectedMimeTypeFilter);
0512 
0513     if (isMobile()) {
0514         if (!m_mobileFileDialog) {
0515             qCDebug(XdgDesktopPortalKdeFileChooser) << "Creating file dialog";
0516             m_mobileFileDialog = new MobileFileDialog(this);
0517         }
0518 
0519         m_mobileFileDialog->setTitle(title);
0520 
0521         // Always false when we are saving a file
0522         m_mobileFileDialog->setSelectExisting(false);
0523 
0524         if (!currentFolder.isEmpty()) {
0525             // Set correct protocol
0526             m_mobileFileDialog->setFolder(QUrl::fromLocalFile(currentFolder));
0527         }
0528 
0529         if (!currentFile.isEmpty()) {
0530             m_mobileFileDialog->setCurrentFile(currentFile);
0531         }
0532 
0533         // currentName: not implemented
0534 
0535         if (!acceptLabel.isEmpty()) {
0536             m_mobileFileDialog->setAcceptLabel(acceptLabel);
0537         }
0538 
0539         if (!nameFilters.isEmpty()) {
0540             m_mobileFileDialog->setNameFilters(nameFilters);
0541         }
0542 
0543         if (!mimeTypeFilters.isEmpty()) {
0544             m_mobileFileDialog->setMimeTypeFilters(mimeTypeFilters);
0545         }
0546 
0547         uint retCode = m_mobileFileDialog->exec();
0548 
0549         results.insert(QStringLiteral("uris"), fuseRedirect(m_mobileFileDialog->results()));
0550 
0551         return retCode;
0552     }
0553 
0554     // for handling of options - choices
0555     QScopedPointer<QWidget> optionsWidget;
0556     // to store IDs for choices along with corresponding comboboxes/checkboxes
0557     QMap<QString, QCheckBox *> checkboxes;
0558     QMap<QString, QComboBox *> comboboxes;
0559 
0560     if (options.contains(QStringLiteral("choices"))) {
0561         OptionList optionList = qdbus_cast<OptionList>(options.value(QStringLiteral("choices")));
0562         optionsWidget.reset(CreateChoiceControls(optionList, checkboxes, comboboxes));
0563     }
0564 
0565     QScopedPointer<FileDialog, QScopedPointerDeleteLater> fileDialog(new FileDialog());
0566     Utils::setParentWindow(fileDialog.data(), parent_window);
0567     Request::makeClosableDialogRequest(handle, fileDialog.get());
0568     fileDialog->setWindowTitle(title);
0569     fileDialog->setModal(modalDialog);
0570     fileDialog->m_fileWidget->setOperationMode(KFileWidget::Saving);
0571     fileDialog->m_fileWidget->setConfirmOverwrite(true);
0572 
0573     const QUrl translatedCurrentFolderUrl = kioUrlFromSandboxPath(currentFolder, Entity::Folder);
0574     if (!translatedCurrentFolderUrl.isEmpty()) {
0575         fileDialog->m_fileWidget->setUrl(translatedCurrentFolderUrl);
0576     }
0577 
0578     if (!currentFile.isEmpty()) {
0579         // If we also had a currentfolder then recycle its URL, otherwise calculate from scratch.
0580         // In either case append the basename to get to a complete file URL again.
0581         QUrl kioUrl = translatedCurrentFolderUrl.isEmpty() ? kioUrlFromSandboxPath(currentFile, Entity::File) : translatedCurrentFolderUrl;
0582         if (!kioUrl.isEmpty()) {
0583             kioUrl.setPath(kioUrl.path() + QLatin1Char('/') + QFileInfo(currentFile).completeBaseName());
0584             fileDialog->m_fileWidget->setSelectedUrl(kioUrl);
0585         } else {
0586             fileDialog->m_fileWidget->setSelectedUrl(QUrl::fromLocalFile(currentFile));
0587         }
0588     }
0589 
0590     if (!currentName.isEmpty()) {
0591         QUrl url = fileDialog->m_fileWidget->baseUrl();
0592         QString path = url.path();
0593         if (path.back() == QLatin1Char('/')) {
0594             path = path + currentName;
0595         } else {
0596             path = path + QLatin1Char('/') + currentName;
0597         }
0598         url.setPath(path);
0599         fileDialog->m_fileWidget->setSelectedUrl(url);
0600     }
0601 
0602     if (!acceptLabel.isEmpty()) {
0603         fileDialog->m_fileWidget->okButton()->setText(acceptLabel);
0604     }
0605 
0606     bool bMimeFilters = false;
0607     if (!mimeTypeFilters.isEmpty()) {
0608         fileDialog->m_fileWidget->setMimeFilter(mimeTypeFilters, selectedMimeTypeFilter);
0609         bMimeFilters = true;
0610     } else if (!nameFilters.isEmpty()) {
0611         fileDialog->m_fileWidget->setFilter(nameFilters.join(QLatin1Char('\n')));
0612     }
0613 
0614     if (optionsWidget) {
0615         fileDialog->m_fileWidget->setCustomWidget(optionsWidget.get());
0616     }
0617 
0618     if (fileDialog->exec() == QDialog::Accepted) {
0619         const auto urls = fileDialog->m_fileWidget->selectedUrls();
0620         results.insert(QStringLiteral("uris"), fuseRedirect(urls));
0621 
0622         if (optionsWidget) {
0623             QVariant choices = EvaluateSelectedChoices(checkboxes, comboboxes);
0624             results.insert(QStringLiteral("choices"), choices);
0625         }
0626 
0627         // try to map current filter back to one of the predefined ones
0628         QString selectedFilter;
0629         if (bMimeFilters) {
0630             selectedFilter = fileDialog->m_fileWidget->currentMimeFilter();
0631         } else {
0632             selectedFilter = fileDialog->m_fileWidget->filterWidget()->currentText();
0633         }
0634         if (allFilters.contains(selectedFilter)) {
0635             results.insert(QStringLiteral("current_filter"), QVariant::fromValue<FilterList>(allFilters.value(selectedFilter)));
0636         }
0637 
0638         return 0;
0639     }
0640 
0641     return 1;
0642 }
0643 
0644 QWidget *FileChooserPortal::CreateChoiceControls(const FileChooserPortal::OptionList &optionList,
0645                                                  QMap<QString, QCheckBox *> &checkboxes,
0646                                                  QMap<QString, QComboBox *> &comboboxes)
0647 {
0648     if (optionList.empty()) {
0649         return nullptr;
0650     }
0651 
0652     QWidget *optionsWidget = new QWidget;
0653     QGridLayout *layout = new QGridLayout(optionsWidget);
0654     // set stretch for (unused) column 2 so controls only take the space they actually need
0655     layout->setColumnStretch(2, 1);
0656     optionsWidget->setLayout(layout);
0657 
0658     for (const Option &option : optionList) {
0659         const int nextRow = layout->rowCount();
0660         // empty list of choices -> boolean choice according to the spec
0661         if (option.choices.empty()) {
0662             QCheckBox *checkbox = new QCheckBox(option.label, optionsWidget);
0663             checkbox->setChecked(option.initialChoiceId == QStringLiteral("true"));
0664             layout->addWidget(checkbox, nextRow, 1);
0665             checkboxes.insert(option.id, checkbox);
0666         } else {
0667             QComboBox *combobox = new QComboBox(optionsWidget);
0668             for (const Choice &choice : option.choices) {
0669                 combobox->addItem(choice.value, choice.id);
0670                 // select this entry if initialChoiceId matches
0671                 if (choice.id == option.initialChoiceId) {
0672                     combobox->setCurrentIndex(combobox->count() - 1);
0673                 }
0674             }
0675             QString labelText = option.label;
0676             if (!labelText.endsWith(QChar::fromLatin1(':'))) {
0677                 labelText += QChar::fromLatin1(':');
0678             }
0679             QLabel *label = new QLabel(labelText, optionsWidget);
0680             label->setBuddy(combobox);
0681             layout->addWidget(label, nextRow, 0, Qt::AlignRight);
0682             layout->addWidget(combobox, nextRow, 1);
0683             comboboxes.insert(option.id, combobox);
0684         }
0685     }
0686 
0687     return optionsWidget;
0688 }
0689 
0690 QVariant FileChooserPortal::EvaluateSelectedChoices(const QMap<QString, QCheckBox *> &checkboxes, const QMap<QString, QComboBox *> &comboboxes)
0691 {
0692     Choices selectedChoices;
0693     const auto checkboxKeys = checkboxes.keys();
0694     for (const QString &id : checkboxKeys) {
0695         Choice choice;
0696         choice.id = id;
0697         choice.value = checkboxes.value(id)->isChecked() ? QStringLiteral("true") : QStringLiteral("false");
0698         selectedChoices << choice;
0699     }
0700     const auto comboboxKeys = comboboxes.keys();
0701     for (const QString &id : comboboxKeys) {
0702         Choice choice;
0703         choice.id = id;
0704         choice.value = comboboxes.value(id)->currentData().toString();
0705         selectedChoices << choice;
0706     }
0707 
0708     return QVariant::fromValue<Choices>(selectedChoices);
0709 }
0710 
0711 QString FileChooserPortal::ExtractAcceptLabel(const QVariantMap &options)
0712 {
0713     QString acceptLabel;
0714     if (options.contains(QStringLiteral("accept_label"))) {
0715         acceptLabel = options.value(QStringLiteral("accept_label")).toString();
0716         // 'accept_label' allows mnemonic underlines, but Qt uses '&' character, so replace/escape accordingly
0717         // to keep literal '&'s and transform mnemonic underlines to the Qt equivalent using '&' for mnemonic
0718         acceptLabel.replace(QChar::fromLatin1('&'), QStringLiteral("&&"));
0719         const int mnemonic_pos = acceptLabel.indexOf(QChar::fromLatin1('_'));
0720         if (mnemonic_pos != -1) {
0721             acceptLabel.replace(mnemonic_pos, 1, QChar::fromLatin1('&'));
0722         }
0723     }
0724     return acceptLabel;
0725 }
0726 
0727 void FileChooserPortal::ExtractFilters(const QVariantMap &options,
0728                                        QStringList &nameFilters,
0729                                        QStringList &mimeTypeFilters,
0730                                        QMap<QString, FilterList> &allFilters,
0731                                        QString &selectedMimeTypeFilter)
0732 {
0733     if (options.contains(QStringLiteral("filters"))) {
0734         const FilterListList filterListList = qdbus_cast<FilterListList>(options.value(QStringLiteral("filters")));
0735         for (const FilterList &filterList : filterListList) {
0736             QStringList filterStrings;
0737             for (const Filter &filterStruct : filterList.filters) {
0738                 if (filterStruct.type == 0) {
0739                     filterStrings << filterStruct.filterString;
0740                 } else {
0741                     mimeTypeFilters << filterStruct.filterString;
0742                     allFilters[filterStruct.filterString] = filterList;
0743                 }
0744             }
0745 
0746             if (!filterStrings.isEmpty()) {
0747                 QString userVisibleName = filterList.userVisibleName;
0748                 if (!isMobile()) {
0749                     userVisibleName.replace(QLatin1Char('/'), QStringLiteral("\\/"));
0750                 }
0751                 const QString filterString = filterStrings.join(QLatin1Char(' '));
0752                 const QString nameFilter = QStringLiteral("%1|%2").arg(filterString, userVisibleName);
0753                 nameFilters << nameFilter;
0754                 allFilters[filterList.userVisibleName] = filterList;
0755             }
0756         }
0757     }
0758 
0759     if (options.contains(QStringLiteral("current_filter"))) {
0760         FilterList filterList = qdbus_cast<FilterList>(options.value(QStringLiteral("current_filter")));
0761         if (filterList.filters.size() == 1) {
0762             Filter filterStruct = filterList.filters.at(0);
0763             if (filterStruct.type == 0) {
0764                 // make the relevant entry the first one in the list of filters,
0765                 // since that is the one that gets preselected by KFileWidget::setFilter
0766                 QString userVisibleName = filterList.userVisibleName;
0767                 if (!isMobile()) {
0768                     userVisibleName.replace(QLatin1Char('/'), QStringLiteral("\\/"));
0769                 }
0770                 QString nameFilter = QStringLiteral("%1|%2").arg(filterStruct.filterString, userVisibleName);
0771                 nameFilters.removeAll(nameFilter);
0772                 nameFilters.push_front(nameFilter);
0773             } else {
0774                 selectedMimeTypeFilter = filterStruct.filterString;
0775             }
0776         } else {
0777             qCDebug(XdgDesktopPortalKdeFileChooser) << "Ignoring 'current_filter' parameter with 0 or multiple filters specified.";
0778         }
0779     }
0780 }
0781 
0782 bool FileChooserPortal::isMobile()
0783 {
0784     QByteArray mobile = qgetenv("QT_QUICK_CONTROLS_MOBILE");
0785     return mobile == "true" || mobile == "1";
0786 }