Krita Source Code Documentation
Loading...
Searching...
No Matches
KoFileDialog.cpp
Go to the documentation of this file.
1/* This file is part of the KDE project
2 SPDX-FileCopyrightText: 2013-2014 Yue Liu <yue.liu@mail.com>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "KoFileDialog.h"
8#include <QDebug>
9#include <QFileDialog>
11#include <QApplication>
12#include <QImageReader>
13#include <QClipboard>
14#include <QInputDialog>
15#include <QMessageBox>
16
17#include <kconfiggroup.h>
18#include <ksharedconfig.h>
19#include <klocalizedstring.h>
20#include <kstandardguiitem.h>
21
22#include <KisMimeDatabase.h>
23#include <KoJsonTrader.h>
24#include "WidgetUtilsDebug.h"
25
26#include <kis_assert.h>
27
28#ifdef Q_OS_MACOS
30#endif
31
32class Q_DECL_HIDDEN KoFileDialog::Private
33{
34public:
35 Private(QWidget *parent_,
36 KoFileDialog::DialogType dialogType_,
37 const QString caption_,
38 const QString defaultDir_,
39 const QString dialogName_)
40 : parent(parent_)
41 , type(dialogType_)
42 , dialogName(dialogName_)
43 , caption(caption_)
44 , defaultDirectory(defaultDir_)
45 , filterList(QStringList())
46 , defaultFilter(QString())
47 {
48 }
49
51 {
52 }
53
54 QWidget *parent;
56 QString dialogName;
57 QString caption;
62 QMap<QString, QString> suffixes; // Filter description to extension, may lack some entries
64 QScopedPointer<KisPreviewFileDialog> fileDialog;
65 QString mimeType;
66};
67
70 const QString &dialogName)
71 : d(new Private(parent, type, "", getUsedDir(dialogName), dialogName))
72{
73}
74
76{
77 delete d;
78}
79
80void KoFileDialog::setCaption(const QString &caption)
81{
82 d->caption = caption;
83}
84
85void KoFileDialog::setDefaultDir(const QString &defaultDir, bool force)
86{
87 if (!defaultDir.isEmpty()) {
88 if (d->defaultDirectory.isEmpty() || force) {
89 QFileInfo f(defaultDir);
90 if (f.isDir()) {
91 d->defaultDirectory = defaultDir;
92 }
93 else {
94 d->defaultDirectory = f.absolutePath();
95 }
96 }
97 if (!QFileInfo(defaultDir).isDir()) {
98 d->proposedFileName = QFileInfo(defaultDir).fileName();
99 }
100 }
101}
102
103void KoFileDialog::setDirectoryUrl(const QUrl &defaultUri)
104{
105 d->defaultUri = defaultUri;
106}
107
109{
110 QStringList imageFilters;
111 // add filters for all formats supported by QImage
112 Q_FOREACH (const QByteArray &format, QImageReader::supportedImageFormats()) {
113 imageFilters << QLatin1String("image/") + format;
114 }
115 setMimeTypeFilters(imageFilters);
116}
117
118void KoFileDialog::setNameFilter(const QString &filter)
119{
120 d->filterList = filter.split(";;");
121}
122
123void KoFileDialog::selectNameFilter(const QString &filter)
124{
125 d->defaultFilter = filter;
126}
127
129{
130 return d->fileDialog->selectedNameFilter();
131}
132
134{
135 return d->mimeType;
136}
137
139{
140 d->fileDialog.reset(new KisPreviewFileDialog(d->parent, d->caption, d->defaultDirectory + "/" + d->proposedFileName));
141 if (!d->defaultUri.isEmpty()) {
142 d->fileDialog->setDirectoryUrl(d->defaultUri);
143 }
144 connect(d->fileDialog.get(), SIGNAL(filterSelected(const QString&)), this, SLOT(onFilterSelected(const QString&)));
145
146#ifdef Q_OS_MACOS
148 if(bookmarkmngr->isSandboxed()) {
149 connect(d->fileDialog.get(), SIGNAL(urlSelected (const QUrl&)), bookmarkmngr, SLOT(addBookmarkAndCheckParentDir(const QUrl&)));
150 }
151#endif
152
153 KConfigGroup group = KSharedConfig::openConfig()->group("File Dialogs");
154
155 bool dontUseNative = true;
156#ifdef Q_OS_ANDROID
157 dontUseNative = false;
158#endif
159#ifdef Q_OS_UNIX
160 if (qgetenv("XDG_CURRENT_DESKTOP") == "KDE") {
161 dontUseNative = false;
162 }
163#endif
164#ifdef Q_OS_MACOS
165 dontUseNative = false;
166#endif
167#ifdef Q_OS_WIN
168 dontUseNative = false;
169#endif
170
171 bool optionDontUseNative;
172 if (!qEnvironmentVariable("APPIMAGE").isEmpty()) {
173 // AppImages don't have access to platform plugins. BUG: 447805
174 optionDontUseNative = false;
175 } else {
176 optionDontUseNative = group.readEntry("DontUseNativeFileDialog", dontUseNative);
177 }
178
179 d->fileDialog->setOption(QFileDialog::DontUseNativeDialog, optionDontUseNative);
180 d->fileDialog->setOption(QFileDialog::DontConfirmOverwrite, false);
181 d->fileDialog->setOption(QFileDialog::HideNameFilterDetails, dontUseNative ? true : false);
182
183
184#ifdef Q_OS_MACOS
185 QList<QUrl> urls = d->fileDialog->sidebarUrls();
186 QUrl volumes = QUrl::fromLocalFile("/Volumes");
187 if (!urls.contains(volumes)) {
188 urls.append(volumes);
189 }
190
191 d->fileDialog->setSidebarUrls(urls);
192#endif
193
194 if (d->type == SaveFile) {
195 d->fileDialog->setAcceptMode(QFileDialog::AcceptSave);
196 d->fileDialog->setFileMode(QFileDialog::AnyFile);
197 }
198 else { // open / import
199
200 d->fileDialog->setAcceptMode(QFileDialog::AcceptOpen);
201
202 if (d->type == ImportDirectory || d->type == OpenDirectory) {
203 d->fileDialog->setFileMode(QFileDialog::Directory);
204 d->fileDialog->setOption(QFileDialog::ShowDirsOnly, true);
205 }
206 else { // open / import file(s)
207 if (d->type == OpenFile || d->type == ImportFile)
208 {
209 d->fileDialog->setFileMode(QFileDialog::ExistingFile);
210 }
211 else { // files
212 d->fileDialog->setFileMode(QFileDialog::ExistingFiles);
213 }
214 }
215 }
216
217#ifndef Q_OS_ANDROID
218 d->fileDialog->setNameFilters(d->filterList);
219
220 if (!d->proposedFileName.isEmpty()) {
221 QString mime = KisMimeDatabase::mimeTypeForFile(d->proposedFileName, d->type == KoFileDialog::SaveFile ? false : true);
222
223 QString description = KisMimeDatabase::descriptionForMimeType(mime);
224 Q_FOREACH(const QString &filter, d->filterList) {
225 if (filter.startsWith(description)) {
226 d->fileDialog->selectNameFilter(filter);
227 break;
228 }
229 }
230 }
231 else if (!d->defaultFilter.isEmpty()) {
232 d->fileDialog->selectNameFilter(d->defaultFilter);
233 }
234#endif
235
236 if (d->type == ImportDirectory ||
237 d->type == ImportFile || d->type == ImportFiles ||
238 d->type == SaveFile) {
239
240 bool allowModal = true;
241// MacOS do not declare native file dialog as modal BUG:413241.
242#ifdef Q_OS_MACOS
243 allowModal = optionDontUseNative;
244// if ( d->proposedFileName.isEmpty() ) {
245// d->fileDialog->selectFile("untitled.kra");
246// } else {
247// d->fileDialog->selectFile(d->proposedFileName);
248// }
249// qDebug() << d->proposedFileName.isEmpty() << d->proposedFileName << d->defaultDirectory;
250#endif
251 if (allowModal) {
252 d->fileDialog->setWindowModality(Qt::WindowModal);
253 }
254 }
255 d->fileDialog->resetIconProvider();
256
257 // QFileDialog::filterSelected is not emitted with the initial value
258 onFilterSelected(d->fileDialog->selectedNameFilter());
259}
260
262{
263 QString url;
265
266#ifdef Q_OS_ANDROID
267 if (d->type == SaveFile) {
268 QString extension = ".kra";
269 QInputDialog mimeSelector;
270 mimeSelector.setLabelText(i18n("Save As:"));
271 mimeSelector.setComboBoxItems(d->filterList);
272 mimeSelector.setOkButtonText(KStandardGuiItem::ok().text());
273 mimeSelector.setCancelButtonText(KStandardGuiItem::cancel().text());
274 // combobox as they stand, are very hard to scroll on a touch device
275 mimeSelector.setOption(QInputDialog::UseListViewForComboBoxItems);
276
277 if (mimeSelector.exec() == QDialog::Accepted) {
278 const QString selectedFilter = mimeSelector.textValue();
279 int start = selectedFilter.indexOf("*.") + 1;
280 int end = selectedFilter.indexOf(" ", start);
281 int n = end - start;
282 extension = selectedFilter.mid(start, n);
283 if (!extension.startsWith(".")) {
284 extension = "." + extension;
285 }
286 d->fileDialog->selectNameFilter(selectedFilter);
287
288 const QString proposedFileBaseName = QFileInfo(d->proposedFileName).baseName();
289 // HACK: discovered by looking into the code
290 d->fileDialog->setWindowTitle(proposedFileBaseName.isEmpty() ? QString("Untitled" + extension)
291 : proposedFileBaseName + extension);
292 } else {
293 return url;
294 }
295 }
296#endif
297
298 bool retryNeeded;
299 do {
300 retryNeeded = false;
301 if (d->fileDialog->exec() == QDialog::Accepted) {
302 url = d->fileDialog->selectedFiles().first();
303 } else {
304 url = QString();
305 break;
306 }
307
308 // The Android native file selector does not know to add the .kra
309 // extension (MIME type not registered), so just skip the whole file
310 // suffix check for Android.
311#ifndef Q_OS_ANDROID
312 const QString suffix = QFileInfo(url).suffix();
313 bool isValidSuffix = true;
314 if (KisMimeDatabase::mimeTypeForSuffix(suffix).isEmpty()) {
315 warnWidgetUtils << "Selected file name suffix" << suffix << "does not match known MIME types";
316 isValidSuffix = false;
317 }
318
319 if (d->type == SaveFile && (suffix.isEmpty() || !isValidSuffix)) {
320 QString extension;
321 if (d->suffixes.contains(d->fileDialog->selectedNameFilter())) {
322 extension = d->suffixes[d->fileDialog->selectedNameFilter()];
323 if (!extension.isEmpty()) {
324 // Append the default file extension to the file name before
325 // relaunching the file selector. We do _not_ just append
326 // the extension and return the new file name because:
327 // * it bypasses the file overwrite prompt provided by the
328 // file selector.
329 // * doing so will break sandboxed macOS and Android,
330 // because access to user files is restricted and must
331 // be done through the native file selector.
332 url.append('.').append(extension);
333 d->fileDialog->selectFile(url);
334 }
335 }
336 if (extension.isEmpty()) {
337 // Use the first extension of the selected filter just as a suggestion
338 QString selectedFilter;
339 // skip index 0 which is "All supported formats"
340 for (int i = 1; i < d->filterList.size(); ++i) {
341 if (d->filterList[i].startsWith(d->fileDialog->selectedNameFilter())) {
342 selectedFilter = d->filterList[i];
343 break;
344 }
345 }
346 int start = selectedFilter.indexOf("*.") + 2;
347 int end = selectedFilter.indexOf(" ", start);
348 if (start != -1 + 2 && end != -1) {
349 extension = selectedFilter.mid(start, end - start);
350 }
351 }
352 QMessageBox::warning(d->parent, d->caption,
353 i18n("The selected file name does not have a file extension that Krita understands.\n"
354 "Make sure the file name ends in '.%1' for example.", extension));
355 retryNeeded = true;
356
357// We can only write to the Uri that was returned, we don't have permission to change the Uri.
358#if !(defined(Q_OS_MACOS) || defined(Q_OS_ANDROID))
359 url = url + extension;
360#endif
361 }
362#endif
363 } while (retryNeeded);
364
365 if (!url.isEmpty()) {
366 d->mimeType = KisMimeDatabase::mimeTypeForFile(url, d->type == KoFileDialog::SaveFile ? false : true);
367 saveUsedDir(url, d->dialogName);
368 }
369 return url;
370}
371
373{
374 QStringList urls;
375
377 if (d->fileDialog->exec() == QDialog::Accepted) {
378 urls = d->fileDialog->selectedFiles();
379 }
380 if (urls.size() > 0) {
381 saveUsedDir(urls.first(), d->dialogName);
382 }
383 return urls;
384}
385
386QStringList KoFileDialog::splitNameFilter(const QString &nameFilter, QStringList *mimeList)
387{
388 Q_ASSERT(mimeList);
389
390 QStringList filters;
391 QString description;
392
393 if (nameFilter.contains("(")) {
394 description = nameFilter.left(nameFilter.indexOf("(") -1).trimmed();
395 }
396
397 QStringList entries = nameFilter.mid(nameFilter.indexOf("(") + 1).split(" ", Qt::SkipEmptyParts);
398
399 entries.sort();
400 Q_FOREACH (QString entry, entries) {
401
402 entry = entry.remove("*");
403 entry = entry.remove(")");
404
406 if (mimeType != "application/octet-stream") {
407 if (!mimeList->contains(mimeType)) {
408 mimeList->append(mimeType);
409 filters.append(KisMimeDatabase::descriptionForMimeType(mimeType) + " ( *" + entry + " )");
410 }
411 }
412 else {
413 filters.append(entry.remove(".").toUpper() + " " + description + " ( *." + entry + " )");
414 }
415 }
416 return filters;
417}
418
419void KoFileDialog::setMimeTypeFilters(const QStringList &mimeTypeList, QString defaultMimeType)
420{
421 constexpr bool withAllSupportedEntry = true;
422 QStringList mimeSeen;
423
424 struct FilterData
425 {
426 QString descriptionOnly;
427 QString fullLine;
428 QString defaultSuffix;
429 };
430
431 FilterData defaultFilter {};
432 // 1
433 QString allSupported;
434 // 2
435 FilterData kritaNative {};
436 // 3
437 FilterData ora {};
438 // remaining
439 QVector<FilterData> otherFileTypes;
440 // All files
441 bool hasAllFilesFilter = false;
442
443 QStringList mimeList = mimeTypeList;
444 mimeList.sort();
445
446 Q_FOREACH(const QString &mimeType, mimeList) {
447 if (!mimeSeen.contains(mimeType)) {
448 if (mimeType == QLatin1String("application/octet-stream")) {
449 // QFileDialog uses application/octet-stream for the
450 // "All files (*)" filter. We can do the same here.
451 hasAllFilesFilter = true;
452 mimeSeen << mimeType;
453 continue;
454 }
456 if (description.isEmpty() && !mimeType.isEmpty()) {
457 description = mimeType.split("/")[1];
458 if (description.startsWith("x-")) {
459 description = description.remove(0, 2);
460 }
461 }
462
463
464 QString oneFilter;
467 warnWidgetUtils << "KoFileDialog: Found no suffixes for mime type" << mimeType;
468 continue;
469 }
470
471 Q_FOREACH(const QString &suffix, suffixes) {
472 const QString glob = QStringLiteral("*.") + suffix;
473 oneFilter.append(glob + " ");
474 if (withAllSupportedEntry) {
475 allSupported.append(glob + " ");
476 }
477#ifdef Q_OS_LINUX
478 if (qgetenv("XDG_CURRENT_DESKTOP") == "GNOME") {
479 oneFilter.append(glob.toUpper() + " ");
480 if (withAllSupportedEntry) {
481 allSupported.append(glob.toUpper() + " ");
482 }
483 }
484#endif
485 }
486
487 Q_ASSERT(!description.isEmpty());
488
489 FilterData filterData {};
490 filterData.descriptionOnly = description;
491 filterData.fullLine = description + " ( " + oneFilter + ")";
492 filterData.defaultSuffix = suffixes.first();
493
494 if (mimeType == QLatin1String("application/x-krita")) {
495 kritaNative = filterData;
496 } else if (mimeType == QLatin1String("image/openraster")) {
497 ora = filterData;
498 } else {
499 otherFileTypes.append(filterData);
500 }
501 if (defaultMimeType == mimeType) {
502 debugWidgetUtils << "KoFileDialog: Matched default MIME type to filter" << filterData.fullLine;
503 defaultFilter = filterData;
504 }
505 mimeSeen << mimeType;
506 }
507 }
508
509 QStringList retFilterList;
510 QMap<QString, QString> retFilterToSuffixMap;
511 auto addFilterItem = [&](const FilterData &filterData) {
512 if (retFilterList.contains(filterData.fullLine)) {
513 debugWidgetUtils << "KoFileDialog: Duplicated filter" << filterData.fullLine;
514 return;
515 }
516 retFilterList.append(filterData.fullLine);
517 // the "simplified" version that comes to "onFilterSelect" when details are disabled
518 retFilterToSuffixMap.insert(filterData.descriptionOnly, filterData.defaultSuffix);
519 // "full version" that comes when details are enabled
520 retFilterToSuffixMap.insert(filterData.fullLine, filterData.defaultSuffix);
521 };
522
523 if (!allSupported.isEmpty()) {
524 FilterData allFilter {};
525 if (allSupported.contains("*.kra")) {
526 allSupported.remove("*.kra ");
527 allSupported.prepend("*.kra ");
528 allFilter.defaultSuffix = QStringLiteral("kra");
529 } else if (!defaultFilter.fullLine.isEmpty()) {
530 const QString suffixToMove = QString("*.") + defaultFilter.defaultSuffix + " ";
531 allSupported.remove(suffixToMove);
532 allSupported.prepend(suffixToMove);
533 allFilter.defaultSuffix = defaultFilter.defaultSuffix;
534 } else {
535 // XXX: we don't have a meaningful default suffix
536 warnWidgetUtils << "KoFileDialog: No default suffix for 'All supported formats'";
537 allFilter.defaultSuffix = QStringLiteral("");
538 }
539 allFilter.descriptionOnly = i18n("All supported formats");
540 allFilter.fullLine = allFilter.descriptionOnly + " ( " + allSupported + ")";
541 addFilterItem(allFilter);
542 }
543 if (!kritaNative.fullLine.isEmpty()) {
544 addFilterItem(kritaNative);
545 }
546 if (!ora.fullLine.isEmpty()) {
547 addFilterItem(ora);
548 }
549
550 std::sort(otherFileTypes.begin(), otherFileTypes.end(), [](const FilterData &a, const FilterData &b) {
551 return a.descriptionOnly < b.descriptionOnly;
552 });
553 Q_FOREACH(const FilterData &filterData, otherFileTypes) {
554 addFilterItem(filterData);
555 }
556
557 if (hasAllFilesFilter) {
558 // Reusing Qt's existing "All files" translation
559 retFilterList.append(QFileDialog::tr("All files (*)"));
560 }
561
562 d->filterList = retFilterList;
563 d->suffixes = retFilterToSuffixMap;
564 d->defaultFilter = defaultFilter.fullLine; // this can be empty
565}
566
567QString KoFileDialog::getUsedDir(const QString &dialogName)
568{
569 if (dialogName.isEmpty()) return "";
570
571 KConfigGroup group = KSharedConfig::openConfig()->group("File Dialogs");
572 QString dir = group.readEntry(dialogName, "");
573 return dir;
574}
575
576void KoFileDialog::saveUsedDir(const QString &fileName,
577 const QString &dialogName)
578{
579
580 if (dialogName.isEmpty()) return;
581
582 QFileInfo fileInfo(fileName);
583 KConfigGroup group = KSharedConfig::openConfig()->group("File Dialogs");
584 group.writeEntry(dialogName, fileInfo.absolutePath());
585
586}
587
588void KoFileDialog::onFilterSelected(const QString &filter)
589{
590 debugWidgetUtils << "KoFileDialog::onFilterSelected" << filter;
591
592 // Setting default suffix for Android is broken as of Qt 5.12.0, returning the file
593 // with extension added but no write permissions granted.
594#ifndef Q_OS_ANDROID
595 QFileDialog::FileMode mode = d->fileDialog->fileMode();
596 if (mode != QFileDialog::Directory && !d->fileDialog->testOption(QFileDialog::ShowDirsOnly)) {
597 // we do not need suffixes for directories
598 if (d->suffixes.contains(filter)) {
599 QString suffix = d->suffixes[filter];
600 debugWidgetUtils << " Setting default suffix to" << suffix;
601 d->fileDialog->setDefaultSuffix(suffix);
602 } else {
603 warnWidgetUtils << "KoFileDialog::onFilterSelected: Cannot find suffix for filter" << filter;
604 d->fileDialog->setDefaultSuffix("");
605 }
606 }
607#endif
608}
connect(this, SIGNAL(optionsChanged()), this, SLOT(saveOptions()))
#define warnWidgetUtils
#define debugWidgetUtils
static KisMacosSecurityBookmarkManager * instance()
static QStringList suffixesForMimeType(const QString &mimeType)
static QString mimeTypeForFile(const QString &file, bool checkExistingFiles=true)
Find the mimetype for the given filename. The filename must include a suffix.
static QString mimeTypeForSuffix(const QString &suffix)
Find the mimetype for a given extension. The extension may have the form "*.xxx" or "xxx".
static QString descriptionForMimeType(const QString &mimeType)
Find the user-readable description for the given mimetype.
QString selectedNameFilter() const
selectedNameFilter returns the name filter the user selected, either directory or by clicking on it.
Private *const d
void saveUsedDir(const QString &fileName, const QString &dialogName)
~KoFileDialog() override
QStringList filterList
QScopedPointer< KisPreviewFileDialog > fileDialog
static QStringList splitNameFilter(const QString &nameFilter, QStringList *mimeList)
splitNameFilter take a single line of a QDialog name filter and split it into several lines....
Private(QWidget *parent_, KoFileDialog::DialogType dialogType_, const QString caption_, const QString defaultDir_, const QString dialogName_)
QString defaultFilter
void setDirectoryUrl(const QUrl &defaultUri)
setDirectoryUrl set the default URI to defaultUri.
QWidget * parent
QMap< QString, QString > suffixes
void setImageFilters()
setImageFilters sets the name filters for the file dialog to all image formats Qt's QImageReader supp...
QString selectedMimeType() const
QString proposedFileName
void createFileDialog()
QString getUsedDir(const QString &dialogName)
KoFileDialog(QWidget *parent, KoFileDialog::DialogType type, const QString &dialogName)
constructor
QString defaultDirectory
QString filename()
Get the file name the user selected in the file dialog.
void setDefaultDir(const QString &defaultDir, bool force=false)
setDefaultDir set the default directory to defaultDir.
void onFilterSelected(const QString &filter)
QStringList filenames()
Get the file names the user selected in the file dialog.
void setCaption(const QString &caption)
void selectNameFilter(const QString &filter)
void setNameFilter(const QString &filter)
KoFileDialog::DialogType type
void setMimeTypeFilters(const QStringList &mimeTypeList, QString defaultMimeType=QString())
setMimeTypeFilters Update the list of file filters from mime types.
QString dialogName
#define KIS_SAFE_ASSERT_RECOVER(cond)
Definition kis_assert.h:126