File indexing completed on 2025-02-09 06:48:21
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 }