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 }