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 }