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