File indexing completed on 2024-04-07 09:03:35

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