File indexing completed on 2024-05-19 04:29:54
0001 /* This file is part of the KDE project 0002 SPDX-FileCopyrightText: 2013-2014 Yue Liu <yue.liu@mail.com> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "KoFileDialog.h" 0008 #include <QDebug> 0009 #include <QFileDialog> 0010 #include <KisPreviewFileDialog.h> 0011 #include <QApplication> 0012 #include <QImageReader> 0013 #include <QClipboard> 0014 #include <QInputDialog> 0015 #include <QMessageBox> 0016 0017 #include <kconfiggroup.h> 0018 #include <ksharedconfig.h> 0019 #include <klocalizedstring.h> 0020 #include <kstandardguiitem.h> 0021 0022 #include <KisMimeDatabase.h> 0023 #include <KoJsonTrader.h> 0024 #include "WidgetUtilsDebug.h" 0025 0026 #include <kis_assert.h> 0027 0028 #ifdef Q_OS_MACOS 0029 #include "KisMacosSecurityBookmarkManager.h" 0030 #endif 0031 0032 class Q_DECL_HIDDEN KoFileDialog::Private 0033 { 0034 public: 0035 Private(QWidget *parent_, 0036 KoFileDialog::DialogType dialogType_, 0037 const QString caption_, 0038 const QString defaultDir_, 0039 const QString dialogName_) 0040 : parent(parent_) 0041 , type(dialogType_) 0042 , dialogName(dialogName_) 0043 , caption(caption_) 0044 , defaultDirectory(defaultDir_) 0045 , filterList(QStringList()) 0046 , defaultFilter(QString()) 0047 { 0048 } 0049 0050 ~Private() 0051 { 0052 } 0053 0054 QWidget *parent; 0055 KoFileDialog::DialogType type; 0056 QString dialogName; 0057 QString caption; 0058 QString defaultDirectory; 0059 QString proposedFileName; 0060 QUrl defaultUri; 0061 QStringList filterList; 0062 QMap<QString, QString> suffixes; // Filter description to extension, may lack some entries 0063 QString defaultFilter; 0064 QScopedPointer<KisPreviewFileDialog> fileDialog; 0065 QString mimeType; 0066 }; 0067 0068 KoFileDialog::KoFileDialog(QWidget *parent, 0069 KoFileDialog::DialogType type, 0070 const QString &dialogName) 0071 : d(new Private(parent, type, "", getUsedDir(dialogName), dialogName)) 0072 { 0073 } 0074 0075 KoFileDialog::~KoFileDialog() 0076 { 0077 delete d; 0078 } 0079 0080 void KoFileDialog::setCaption(const QString &caption) 0081 { 0082 d->caption = caption; 0083 } 0084 0085 void KoFileDialog::setDefaultDir(const QString &defaultDir, bool force) 0086 { 0087 if (!defaultDir.isEmpty()) { 0088 if (d->defaultDirectory.isEmpty() || force) { 0089 QFileInfo f(defaultDir); 0090 if (f.isDir()) { 0091 d->defaultDirectory = defaultDir; 0092 } 0093 else { 0094 d->defaultDirectory = f.absolutePath(); 0095 } 0096 } 0097 if (!QFileInfo(defaultDir).isDir()) { 0098 d->proposedFileName = QFileInfo(defaultDir).fileName(); 0099 } 0100 } 0101 } 0102 0103 void KoFileDialog::setDirectoryUrl(const QUrl &defaultUri) 0104 { 0105 d->defaultUri = defaultUri; 0106 } 0107 0108 void KoFileDialog::setImageFilters() 0109 { 0110 QStringList imageFilters; 0111 // add filters for all formats supported by QImage 0112 Q_FOREACH (const QByteArray &format, QImageReader::supportedImageFormats()) { 0113 imageFilters << QLatin1String("image/") + format; 0114 } 0115 setMimeTypeFilters(imageFilters); 0116 } 0117 0118 QString KoFileDialog::selectedNameFilter() const 0119 { 0120 return d->fileDialog->selectedNameFilter(); 0121 } 0122 0123 QString KoFileDialog::selectedMimeType() const 0124 { 0125 return d->mimeType; 0126 } 0127 0128 void KoFileDialog::createFileDialog() 0129 { 0130 d->fileDialog.reset(new KisPreviewFileDialog(d->parent, d->caption, d->defaultDirectory + "/" + d->proposedFileName)); 0131 if (!d->defaultUri.isEmpty()) { 0132 d->fileDialog->setDirectoryUrl(d->defaultUri); 0133 } 0134 connect(d->fileDialog.get(), SIGNAL(filterSelected(const QString&)), this, SLOT(onFilterSelected(const QString&))); 0135 0136 #ifdef Q_OS_MACOS 0137 KisMacosSecurityBookmarkManager *bookmarkmngr = KisMacosSecurityBookmarkManager::instance(); 0138 if(bookmarkmngr->isSandboxed()) { 0139 connect(d->fileDialog.get(), SIGNAL(urlSelected (const QUrl&)), bookmarkmngr, SLOT(addBookmarkAndCheckParentDir(const QUrl&))); 0140 } 0141 #endif 0142 0143 KConfigGroup group = KSharedConfig::openConfig()->group("File Dialogs"); 0144 0145 bool dontUseNative = true; 0146 #ifdef Q_OS_ANDROID 0147 dontUseNative = false; 0148 #endif 0149 #ifdef Q_OS_UNIX 0150 if (qgetenv("XDG_CURRENT_DESKTOP") == "KDE") { 0151 dontUseNative = false; 0152 } 0153 #endif 0154 #ifdef Q_OS_MACOS 0155 dontUseNative = false; 0156 #endif 0157 #ifdef Q_OS_WIN 0158 dontUseNative = false; 0159 #endif 0160 0161 bool optionDontUseNative; 0162 if (!qEnvironmentVariable("APPIMAGE").isEmpty()) { 0163 // AppImages don't have access to platform plugins. BUG: 447805 0164 optionDontUseNative = false; 0165 } else { 0166 optionDontUseNative = group.readEntry("DontUseNativeFileDialog", dontUseNative); 0167 } 0168 0169 d->fileDialog->setOption(QFileDialog::DontUseNativeDialog, optionDontUseNative); 0170 d->fileDialog->setOption(QFileDialog::DontConfirmOverwrite, false); 0171 d->fileDialog->setOption(QFileDialog::HideNameFilterDetails, dontUseNative ? true : false); 0172 0173 0174 #ifdef Q_OS_MACOS 0175 QList<QUrl> urls = d->fileDialog->sidebarUrls(); 0176 QUrl volumes = QUrl::fromLocalFile("/Volumes"); 0177 if (!urls.contains(volumes)) { 0178 urls.append(volumes); 0179 } 0180 0181 d->fileDialog->setSidebarUrls(urls); 0182 #endif 0183 0184 if (d->type == SaveFile) { 0185 d->fileDialog->setAcceptMode(QFileDialog::AcceptSave); 0186 d->fileDialog->setFileMode(QFileDialog::AnyFile); 0187 } 0188 else { // open / import 0189 0190 d->fileDialog->setAcceptMode(QFileDialog::AcceptOpen); 0191 0192 if (d->type == ImportDirectory || d->type == OpenDirectory) { 0193 d->fileDialog->setFileMode(QFileDialog::Directory); 0194 d->fileDialog->setOption(QFileDialog::ShowDirsOnly, true); 0195 } 0196 else { // open / import file(s) 0197 if (d->type == OpenFile || d->type == ImportFile) 0198 { 0199 d->fileDialog->setFileMode(QFileDialog::ExistingFile); 0200 } 0201 else { // files 0202 d->fileDialog->setFileMode(QFileDialog::ExistingFiles); 0203 } 0204 } 0205 } 0206 0207 #ifndef Q_OS_ANDROID 0208 d->fileDialog->setNameFilters(d->filterList); 0209 0210 if (!d->proposedFileName.isEmpty()) { 0211 QString mime = KisMimeDatabase::mimeTypeForFile(d->proposedFileName, d->type == KoFileDialog::SaveFile ? false : true); 0212 0213 QString description = KisMimeDatabase::descriptionForMimeType(mime); 0214 Q_FOREACH(const QString &filter, d->filterList) { 0215 if (filter.startsWith(description)) { 0216 d->fileDialog->selectNameFilter(filter); 0217 break; 0218 } 0219 } 0220 } 0221 else if (!d->defaultFilter.isEmpty()) { 0222 d->fileDialog->selectNameFilter(d->defaultFilter); 0223 } 0224 #endif 0225 0226 if (d->type == ImportDirectory || 0227 d->type == ImportFile || d->type == ImportFiles || 0228 d->type == SaveFile) { 0229 0230 bool allowModal = true; 0231 // MacOS do not declare native file dialog as modal BUG:413241. 0232 #ifdef Q_OS_MACOS 0233 allowModal = optionDontUseNative; 0234 // if ( d->proposedFileName.isEmpty() ) { 0235 // d->fileDialog->selectFile("untitled.kra"); 0236 // } else { 0237 // d->fileDialog->selectFile(d->proposedFileName); 0238 // } 0239 // qDebug() << d->proposedFileName.isEmpty() << d->proposedFileName << d->defaultDirectory; 0240 #endif 0241 if (allowModal) { 0242 d->fileDialog->setWindowModality(Qt::WindowModal); 0243 } 0244 } 0245 d->fileDialog->resetIconProvider(); 0246 0247 // QFileDialog::filterSelected is not emitted with the initial value 0248 onFilterSelected(d->fileDialog->selectedNameFilter()); 0249 } 0250 0251 QString KoFileDialog::filename() 0252 { 0253 QString url; 0254 createFileDialog(); 0255 0256 #ifdef Q_OS_ANDROID 0257 if (d->type == SaveFile) { 0258 QString extension = ".kra"; 0259 QInputDialog mimeSelector; 0260 mimeSelector.setLabelText(i18n("Save As:")); 0261 mimeSelector.setComboBoxItems(d->filterList); 0262 mimeSelector.setOkButtonText(KStandardGuiItem::ok().text()); 0263 mimeSelector.setCancelButtonText(KStandardGuiItem::cancel().text()); 0264 // combobox as they stand, are very hard to scroll on a touch device 0265 mimeSelector.setOption(QInputDialog::UseListViewForComboBoxItems); 0266 0267 if (mimeSelector.exec() == QDialog::Accepted) { 0268 const QString selectedFilter = mimeSelector.textValue(); 0269 int start = selectedFilter.indexOf("*.") + 1; 0270 int end = selectedFilter.indexOf(" ", start); 0271 int n = end - start; 0272 extension = selectedFilter.mid(start, n); 0273 if (!extension.startsWith(".")) { 0274 extension = "." + extension; 0275 } 0276 d->fileDialog->selectNameFilter(selectedFilter); 0277 0278 const QString proposedFileBaseName = QFileInfo(d->proposedFileName).baseName(); 0279 // HACK: discovered by looking into the code 0280 d->fileDialog->setWindowTitle(proposedFileBaseName.isEmpty() ? QString("Untitled" + extension) 0281 : proposedFileBaseName + extension); 0282 } else { 0283 return url; 0284 } 0285 } 0286 #endif 0287 0288 bool retryNeeded; 0289 do { 0290 retryNeeded = false; 0291 if (d->fileDialog->exec() == QDialog::Accepted) { 0292 url = d->fileDialog->selectedFiles().first(); 0293 } else { 0294 url = QString(); 0295 break; 0296 } 0297 0298 // The Android native file selector does not know to add the .kra 0299 // extension (MIME type not registered), so just skip the whole file 0300 // suffix check for Android. 0301 #ifndef Q_OS_ANDROID 0302 const QString suffix = QFileInfo(url).suffix(); 0303 bool isValidSuffix = true; 0304 if (KisMimeDatabase::mimeTypeForSuffix(suffix).isEmpty()) { 0305 warnWidgetUtils << "Selected file name suffix" << suffix << "does not match known MIME types"; 0306 isValidSuffix = false; 0307 } 0308 0309 if (d->type == SaveFile && (suffix.isEmpty() || !isValidSuffix)) { 0310 QString extension; 0311 if (d->suffixes.contains(d->fileDialog->selectedNameFilter())) { 0312 extension = d->suffixes[d->fileDialog->selectedNameFilter()]; 0313 if (!extension.isEmpty()) { 0314 // Append the default file extension to the file name before 0315 // relaunching the file selector. We do _not_ just append 0316 // the extension and return the new file name because: 0317 // * it bypasses the file overwrite prompt provided by the 0318 // file selector. 0319 // * doing so will break sandboxed macOS and Android, 0320 // because access to user files is restricted and must 0321 // be done through the native file selector. 0322 url.append('.').append(extension); 0323 d->fileDialog->selectFile(url); 0324 } 0325 } 0326 if (extension.isEmpty()) { 0327 // Use the first extension of the selected filter just as a suggestion 0328 QString selectedFilter; 0329 // skip index 0 which is "All supported formats" 0330 for (int i = 1; i < d->filterList.size(); ++i) { 0331 if (d->filterList[i].startsWith(d->fileDialog->selectedNameFilter())) { 0332 selectedFilter = d->filterList[i]; 0333 break; 0334 } 0335 } 0336 int start = selectedFilter.indexOf("*.") + 2; 0337 int end = selectedFilter.indexOf(" ", start); 0338 if (start != -1 + 2 && end != -1) { 0339 extension = selectedFilter.mid(start, end - start); 0340 } 0341 } 0342 QMessageBox::warning(d->parent, d->caption, 0343 i18n("The selected file name does not have a file extension that Krita understands.\n" 0344 "Make sure the file name ends in '.%1' for example.", extension)); 0345 retryNeeded = true; 0346 0347 // We can only write to the Uri that was returned, we don't have permission to change the Uri. 0348 #if !(defined(Q_OS_MACOS) || defined(Q_OS_ANDROID)) 0349 url = url + extension; 0350 #endif 0351 } 0352 #endif 0353 } while (retryNeeded); 0354 0355 if (!url.isEmpty()) { 0356 d->mimeType = KisMimeDatabase::mimeTypeForFile(url, d->type == KoFileDialog::SaveFile ? false : true); 0357 saveUsedDir(url, d->dialogName); 0358 } 0359 return url; 0360 } 0361 0362 QStringList KoFileDialog::filenames() 0363 { 0364 QStringList urls; 0365 0366 createFileDialog(); 0367 if (d->fileDialog->exec() == QDialog::Accepted) { 0368 urls = d->fileDialog->selectedFiles(); 0369 } 0370 if (urls.size() > 0) { 0371 saveUsedDir(urls.first(), d->dialogName); 0372 } 0373 return urls; 0374 } 0375 0376 QStringList KoFileDialog::splitNameFilter(const QString &nameFilter, QStringList *mimeList) 0377 { 0378 Q_ASSERT(mimeList); 0379 0380 QStringList filters; 0381 QString description; 0382 0383 if (nameFilter.contains("(")) { 0384 description = nameFilter.left(nameFilter.indexOf("(") -1).trimmed(); 0385 } 0386 0387 #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) 0388 QStringList entries = nameFilter.mid(nameFilter.indexOf("(") + 1).split(" ", Qt::SkipEmptyParts); 0389 #else 0390 QStringList entries = nameFilter.mid(nameFilter.indexOf("(") + 1).split(" ", QString::SkipEmptyParts); 0391 #endif 0392 0393 entries.sort(); 0394 Q_FOREACH (QString entry, entries) { 0395 0396 entry = entry.remove("*"); 0397 entry = entry.remove(")"); 0398 0399 QString mimeType = KisMimeDatabase::mimeTypeForSuffix(entry); 0400 if (mimeType != "application/octet-stream") { 0401 if (!mimeList->contains(mimeType)) { 0402 mimeList->append(mimeType); 0403 filters.append(KisMimeDatabase::descriptionForMimeType(mimeType) + " ( *" + entry + " )"); 0404 } 0405 } 0406 else { 0407 filters.append(entry.remove(".").toUpper() + " " + description + " ( *." + entry + " )"); 0408 } 0409 } 0410 return filters; 0411 } 0412 0413 void KoFileDialog::setMimeTypeFilters(const QStringList &mimeTypeList, QString defaultMimeType) 0414 { 0415 constexpr bool withAllSupportedEntry = true; 0416 QStringList mimeSeen; 0417 0418 struct FilterData 0419 { 0420 QString descriptionOnly; 0421 QString fullLine; 0422 QString defaultSuffix; 0423 }; 0424 0425 FilterData defaultFilter {}; 0426 // 1 0427 QString allSupported; 0428 // 2 0429 FilterData kritaNative {}; 0430 // 3 0431 FilterData ora {}; 0432 // remaining 0433 QVector<FilterData> otherFileTypes; 0434 // All files 0435 bool hasAllFilesFilter = false; 0436 0437 QStringList mimeList = mimeTypeList; 0438 mimeList.sort(); 0439 0440 Q_FOREACH(const QString &mimeType, mimeList) { 0441 if (!mimeSeen.contains(mimeType)) { 0442 if (mimeType == QLatin1String("application/octet-stream")) { 0443 // QFileDialog uses application/octet-stream for the 0444 // "All files (*)" filter. We can do the same here. 0445 hasAllFilesFilter = true; 0446 mimeSeen << mimeType; 0447 continue; 0448 } 0449 QString description = KisMimeDatabase::descriptionForMimeType(mimeType); 0450 if (description.isEmpty() && !mimeType.isEmpty()) { 0451 description = mimeType.split("/")[1]; 0452 if (description.startsWith("x-")) { 0453 description = description.remove(0, 2); 0454 } 0455 } 0456 0457 0458 QString oneFilter; 0459 const QStringList suffixes = KisMimeDatabase::suffixesForMimeType(mimeType); 0460 KIS_SAFE_ASSERT_RECOVER(!suffixes.isEmpty()) { 0461 warnWidgetUtils << "KoFileDialog: Found no suffixes for mime type" << mimeType; 0462 continue; 0463 } 0464 0465 Q_FOREACH(const QString &suffix, suffixes) { 0466 const QString glob = QStringLiteral("*.") + suffix; 0467 oneFilter.append(glob + " "); 0468 if (withAllSupportedEntry) { 0469 allSupported.append(glob + " "); 0470 } 0471 #ifdef Q_OS_LINUX 0472 if (qgetenv("XDG_CURRENT_DESKTOP") == "GNOME") { 0473 oneFilter.append(glob.toUpper() + " "); 0474 if (withAllSupportedEntry) { 0475 allSupported.append(glob.toUpper() + " "); 0476 } 0477 } 0478 #endif 0479 } 0480 0481 Q_ASSERT(!description.isEmpty()); 0482 0483 FilterData filterData {}; 0484 filterData.descriptionOnly = description; 0485 filterData.fullLine = description + " ( " + oneFilter + ")"; 0486 filterData.defaultSuffix = suffixes.first(); 0487 0488 if (mimeType == QLatin1String("application/x-krita")) { 0489 kritaNative = filterData; 0490 } else if (mimeType == QLatin1String("image/openraster")) { 0491 ora = filterData; 0492 } else { 0493 otherFileTypes.append(filterData); 0494 } 0495 if (defaultMimeType == mimeType) { 0496 debugWidgetUtils << "KoFileDialog: Matched default MIME type to filter" << filterData.fullLine; 0497 defaultFilter = filterData; 0498 } 0499 mimeSeen << mimeType; 0500 } 0501 } 0502 0503 QStringList retFilterList; 0504 QMap<QString, QString> retFilterToSuffixMap; 0505 auto addFilterItem = [&](const FilterData &filterData) { 0506 if (retFilterList.contains(filterData.fullLine)) { 0507 debugWidgetUtils << "KoFileDialog: Duplicated filter" << filterData.fullLine; 0508 return; 0509 } 0510 retFilterList.append(filterData.fullLine); 0511 // the "simplified" version that comes to "onFilterSelect" when details are disabled 0512 retFilterToSuffixMap.insert(filterData.descriptionOnly, filterData.defaultSuffix); 0513 // "full version" that comes when details are enabled 0514 retFilterToSuffixMap.insert(filterData.fullLine, filterData.defaultSuffix); 0515 }; 0516 0517 if (!allSupported.isEmpty()) { 0518 FilterData allFilter {}; 0519 if (allSupported.contains("*.kra")) { 0520 allSupported.remove("*.kra "); 0521 allSupported.prepend("*.kra "); 0522 allFilter.defaultSuffix = QStringLiteral("kra"); 0523 } else if (!defaultFilter.fullLine.isEmpty()) { 0524 const QString suffixToMove = QString("*.") + defaultFilter.defaultSuffix + " "; 0525 allSupported.remove(suffixToMove); 0526 allSupported.prepend(suffixToMove); 0527 allFilter.defaultSuffix = defaultFilter.defaultSuffix; 0528 } else { 0529 // XXX: we don't have a meaningful default suffix 0530 warnWidgetUtils << "KoFileDialog: No default suffix for 'All supported formats'"; 0531 allFilter.defaultSuffix = QStringLiteral(""); 0532 } 0533 allFilter.descriptionOnly = i18n("All supported formats"); 0534 allFilter.fullLine = allFilter.descriptionOnly + " ( " + allSupported + ")"; 0535 addFilterItem(allFilter); 0536 } 0537 if (!kritaNative.fullLine.isEmpty()) { 0538 addFilterItem(kritaNative); 0539 } 0540 if (!ora.fullLine.isEmpty()) { 0541 addFilterItem(ora); 0542 } 0543 0544 std::sort(otherFileTypes.begin(), otherFileTypes.end(), [](const FilterData &a, const FilterData &b) { 0545 return a.descriptionOnly < b.descriptionOnly; 0546 }); 0547 Q_FOREACH(const FilterData &filterData, otherFileTypes) { 0548 addFilterItem(filterData); 0549 } 0550 0551 if (hasAllFilesFilter) { 0552 // Reusing Qt's existing "All files" translation 0553 retFilterList.append(QFileDialog::tr("All files (*)")); 0554 } 0555 0556 d->filterList = retFilterList; 0557 d->suffixes = retFilterToSuffixMap; 0558 d->defaultFilter = defaultFilter.fullLine; // this can be empty 0559 } 0560 0561 QString KoFileDialog::getUsedDir(const QString &dialogName) 0562 { 0563 if (dialogName.isEmpty()) return ""; 0564 0565 KConfigGroup group = KSharedConfig::openConfig()->group("File Dialogs"); 0566 QString dir = group.readEntry(dialogName, ""); 0567 return dir; 0568 } 0569 0570 void KoFileDialog::saveUsedDir(const QString &fileName, 0571 const QString &dialogName) 0572 { 0573 0574 if (dialogName.isEmpty()) return; 0575 0576 QFileInfo fileInfo(fileName); 0577 KConfigGroup group = KSharedConfig::openConfig()->group("File Dialogs"); 0578 group.writeEntry(dialogName, fileInfo.absolutePath()); 0579 0580 } 0581 0582 void KoFileDialog::onFilterSelected(const QString &filter) 0583 { 0584 debugWidgetUtils << "KoFileDialog::onFilterSelected" << filter; 0585 0586 // Setting default suffix for Android is broken as of Qt 5.12.0, returning the file 0587 // with extension added but no write permissions granted. 0588 #ifndef Q_OS_ANDROID 0589 QFileDialog::FileMode mode = d->fileDialog->fileMode(); 0590 if (mode != QFileDialog::FileMode::Directory && mode != QFileDialog::FileMode::DirectoryOnly) { 0591 // we do not need suffixes for directories 0592 if (d->suffixes.contains(filter)) { 0593 QString suffix = d->suffixes[filter]; 0594 debugWidgetUtils << " Setting default suffix to" << suffix; 0595 d->fileDialog->setDefaultSuffix(suffix); 0596 } else { 0597 warnWidgetUtils << "KoFileDialog::onFilterSelected: Cannot find suffix for filter" << filter; 0598 d->fileDialog->setDefaultSuffix(""); 0599 } 0600 } 0601 #endif 0602 }