1// Copyright (C) 2017 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qquicklabsplatformfiledialog_p.h"
5
6#include <QtCore/qlist.h>
7
8QT_BEGIN_NAMESPACE
9
10using namespace Qt::StringLiterals;
11
12/*!
13 \qmltype FileDialog
14 \inherits Dialog
15//! \instantiates QQuickLabsPlatformFileDialog
16 \inqmlmodule Qt.labs.platform
17 \since 5.8
18 \brief A native file dialog.
19
20 The FileDialog type provides a QML API for native platform file dialogs.
21
22 \image qtlabsplatform-filedialog-gtk.png
23
24 To show a file dialog, construct an instance of FileDialog, set the
25 desired properties, and call \l {Dialog::}{open()}. The \l currentFile
26 or \l currentFiles properties can be used to determine the currently
27 selected file(s) in the dialog. The \l file and \l files properties
28 are updated only after the final selection has been made by accepting
29 the dialog.
30
31 \code
32 MenuItem {
33 text: "Open..."
34 onTriggered: fileDialog.open()
35 }
36
37 FileDialog {
38 id: fileDialog
39 currentFile: document.source
40 folder: StandardPaths.writableLocation(StandardPaths.DocumentsLocation)
41 }
42
43 MyDocument {
44 id: document
45 source: fileDialog.file
46 }
47 \endcode
48
49 \section2 Availability
50
51 A native platform file dialog is currently available on the following platforms:
52
53 \list
54 \li Android
55 \li iOS
56 \li Linux (when running with the GTK+ platform theme)
57 \li macOS
58 \li Windows
59 \endlist
60
61 \input includes/widgets.qdocinc 1
62
63 \labs
64
65 \sa FolderDialog, StandardPaths
66*/
67
68QQuickLabsPlatformFileDialog::QQuickLabsPlatformFileDialog(QObject *parent)
69 : QQuickLabsPlatformDialog(QPlatformTheme::FileDialog, parent),
70 m_fileMode(OpenFile),
71 m_options(QFileDialogOptions::create()),
72 m_selectedNameFilter(nullptr)
73{
74 m_options->setFileMode(QFileDialogOptions::ExistingFile);
75 m_options->setAcceptMode(QFileDialogOptions::AcceptOpen);
76}
77
78/*!
79 \qmlproperty enumeration Qt.labs.platform::FileDialog::fileMode
80
81 This property holds the mode of the dialog.
82
83 Available values:
84 \value FileDialog.OpenFile The dialog is used to select an existing file (default).
85 \value FileDialog.OpenFiles The dialog is used to select multiple existing files.
86 \value FileDialog.SaveFile The dialog is used to select any file. The file does not have to exist.
87*/
88QQuickLabsPlatformFileDialog::FileMode QQuickLabsPlatformFileDialog::fileMode() const
89{
90 return m_fileMode;
91}
92
93void QQuickLabsPlatformFileDialog::setFileMode(FileMode mode)
94{
95 if (mode == m_fileMode)
96 return;
97
98 switch (mode) {
99 case OpenFile:
100 m_options->setFileMode(QFileDialogOptions::ExistingFile);
101 m_options->setAcceptMode(QFileDialogOptions::AcceptOpen);
102 break;
103 case OpenFiles:
104 m_options->setFileMode(QFileDialogOptions::ExistingFiles);
105 m_options->setAcceptMode(QFileDialogOptions::AcceptOpen);
106 break;
107 case SaveFile:
108 m_options->setFileMode(QFileDialogOptions::AnyFile);
109 m_options->setAcceptMode(QFileDialogOptions::AcceptSave);
110 break;
111 default:
112 break;
113 }
114
115 m_fileMode = mode;
116 emit fileModeChanged();
117}
118
119/*!
120 \qmlproperty url Qt.labs.platform::FileDialog::file
121
122 This property holds the final accepted file.
123
124 Unlike the \l currentFile property, the \c file property is not updated
125 while the user is selecting files in the dialog, but only after the final
126 selection has been made. That is, when the user has clicked \uicontrol OK
127 to accept a file. Alternatively, the \l {Dialog::}{accepted()} signal
128 can be handled to get the final selection.
129
130 \sa currentFile, {Dialog::}{accepted()}
131*/
132QUrl QQuickLabsPlatformFileDialog::file() const
133{
134 return addDefaultSuffix(file: m_files.value(i: 0));
135}
136
137void QQuickLabsPlatformFileDialog::setFile(const QUrl &file)
138{
139 setFiles(QList<QUrl>() << file);
140}
141
142/*!
143 \qmlproperty list<url> Qt.labs.platform::FileDialog::files
144
145 This property holds the final accepted files.
146
147 Unlike the \l currentFiles property, the \c files property is not updated
148 while the user is selecting files in the dialog, but only after the final
149 selection has been made. That is, when the user has clicked \uicontrol OK
150 to accept files. Alternatively, the \l {Dialog::}{accepted()} signal
151 can be handled to get the final selection.
152
153 \sa currentFiles, {Dialog::}{accepted()}
154*/
155QList<QUrl> QQuickLabsPlatformFileDialog::files() const
156{
157 return addDefaultSuffixes(files: m_files);
158}
159
160void QQuickLabsPlatformFileDialog::setFiles(const QList<QUrl> &files)
161{
162 if (m_files == files)
163 return;
164
165 bool firstChanged = m_files.value(i: 0) != files.value(i: 0);
166 m_files = files;
167 if (firstChanged)
168 emit fileChanged();
169 emit filesChanged();
170}
171
172/*!
173 \qmlproperty url Qt.labs.platform::FileDialog::currentFile
174
175 This property holds the currently selected file in the dialog.
176
177 Unlike the \l file property, the \c currentFile property is updated
178 while the user is selecting files in the dialog, even before the final
179 selection has been made.
180
181 \sa file, currentFiles
182*/
183QUrl QQuickLabsPlatformFileDialog::currentFile() const
184{
185 return currentFiles().value(i: 0);
186}
187
188void QQuickLabsPlatformFileDialog::setCurrentFile(const QUrl &file)
189{
190 setCurrentFiles(QList<QUrl>() << file);
191}
192
193/*!
194 \qmlproperty list<url> Qt.labs.platform::FileDialog::currentFiles
195
196 This property holds the currently selected files in the dialog.
197
198 Unlike the \l files property, the \c currentFiles property is updated
199 while the user is selecting files in the dialog, even before the final
200 selection has been made.
201
202 \sa files, currentFile
203*/
204QList<QUrl> QQuickLabsPlatformFileDialog::currentFiles() const
205{
206 if (QPlatformFileDialogHelper *fileDialog = qobject_cast<QPlatformFileDialogHelper *>(object: handle()))
207 return fileDialog->selectedFiles();
208 return m_options->initiallySelectedFiles();
209}
210
211void QQuickLabsPlatformFileDialog::setCurrentFiles(const QList<QUrl> &files)
212{
213 if (QPlatformFileDialogHelper *fileDialog = qobject_cast<QPlatformFileDialogHelper *>(object: handle())) {
214 for (const QUrl &file : files)
215 fileDialog->selectFile(filename: file);
216 }
217 m_options->setInitiallySelectedFiles(files);
218}
219
220/*!
221 \qmlproperty url Qt.labs.platform::FileDialog::folder
222
223 This property holds the folder where files are selected.
224 For selecting a folder, use FolderDialog instead.
225
226 \sa FolderDialog
227*/
228QUrl QQuickLabsPlatformFileDialog::folder() const
229{
230 if (QPlatformFileDialogHelper *fileDialog = qobject_cast<QPlatformFileDialogHelper *>(object: handle()))
231 return fileDialog->directory();
232 return m_options->initialDirectory();
233}
234
235void QQuickLabsPlatformFileDialog::setFolder(const QUrl &folder)
236{
237 if (QPlatformFileDialogHelper *fileDialog = qobject_cast<QPlatformFileDialogHelper *>(object: handle()))
238 fileDialog->setDirectory(folder);
239 m_options->setInitialDirectory(folder);
240}
241
242/*!
243 \qmlproperty flags Qt.labs.platform::FileDialog::options
244
245 This property holds the various options that affect the look and feel of the dialog.
246
247 By default, all options are disabled.
248
249 Options should be set before showing the dialog. Setting them while the dialog is
250 visible is not guaranteed to have an immediate effect on the dialog (depending on
251 the option and on the platform).
252
253 Available options:
254 \value FileDialog.DontResolveSymlinks Don't resolve symlinks in the file dialog. By default symlinks are resolved.
255 \value FileDialog.DontConfirmOverwrite Don't ask for confirmation if an existing file is selected. By default confirmation is requested.
256 \value FileDialog.ReadOnly Indicates that the dialog doesn't allow creating directories.
257 \value FileDialog.HideNameFilterDetails Indicates if the file name filter details are hidden or not.
258*/
259QFileDialogOptions::FileDialogOptions QQuickLabsPlatformFileDialog::options() const
260{
261 return m_options->options();
262}
263
264void QQuickLabsPlatformFileDialog::setOptions(QFileDialogOptions::FileDialogOptions options)
265{
266 if (options == m_options->options())
267 return;
268
269 m_options->setOptions(options);
270 emit optionsChanged();
271}
272
273void QQuickLabsPlatformFileDialog::resetOptions()
274{
275 setOptions({});
276}
277
278/*!
279 \qmlproperty list<string> Qt.labs.platform::FileDialog::nameFilters
280
281 This property holds the filters that restrict the types of files that
282 can be selected.
283
284 \code
285 FileDialog {
286 nameFilters: ["Text files (*.txt)", "HTML files (*.html *.htm)"]
287 }
288 \endcode
289
290 \note \b{*.*} is not a portable filter, because the historical assumption
291 that the file extension determines the file type is not consistent on every
292 operating system. It is possible to have a file with no dot in its name (for
293 example, \c Makefile). In a native Windows file dialog, \b{*.*} will match
294 such files, while in other types of file dialogs it may not. So it is better
295 to use \b{*} if you mean to select any file.
296
297 \sa selectedNameFilter
298*/
299QStringList QQuickLabsPlatformFileDialog::nameFilters() const
300{
301 return m_options->nameFilters();
302}
303
304void QQuickLabsPlatformFileDialog::setNameFilters(const QStringList &filters)
305{
306 if (filters == m_options->nameFilters())
307 return;
308
309 m_options->setNameFilters(filters);
310 if (m_selectedNameFilter) {
311 int index = m_selectedNameFilter->index();
312 if (index < 0 || index >= filters.size())
313 index = 0;
314 m_selectedNameFilter->update(filter: filters.value(i: index));
315 }
316 emit nameFiltersChanged();
317}
318
319void QQuickLabsPlatformFileDialog::resetNameFilters()
320{
321 setNameFilters(QStringList());
322}
323
324/*!
325 \qmlproperty int Qt.labs.platform::FileDialog::selectedNameFilter.index
326 \qmlproperty string Qt.labs.platform::FileDialog::selectedNameFilter.name
327 \qmlproperty list<string> Qt.labs.platform::FileDialog::selectedNameFilter.extensions
328
329 These properties hold the currently selected name filter.
330
331 \table
332 \header
333 \li Name
334 \li Description
335 \row
336 \li \b index : int
337 \li This property determines which \l {nameFilters}{name filter} is selected.
338 The specified filter is selected when the dialog is opened. The value is
339 updated when the user selects another filter.
340 \row
341 \li [read-only] \b name : string
342 \li This property holds the name of the selected filter. In the
343 example below, the name of the first filter is \c {"Text files"}
344 and the second is \c {"HTML files"}.
345 \row
346 \li [read-only] \b extensions : list<string>
347 \li This property holds the list of extensions of the selected filter.
348 In the example below, the list of extensions of the first filter is
349 \c {["txt"]} and the second is \c {["html", "htm"]}.
350 \endtable
351
352 \code
353 FileDialog {
354 id: fileDialog
355 selectedNameFilter.index: 1
356 nameFilters: ["Text files (*.txt)", "HTML files (*.html *.htm)"]
357 }
358
359 MyDocument {
360 id: document
361 fileType: fileDialog.selectedNameFilter.extensions[0]
362 }
363 \endcode
364
365 \sa nameFilters
366*/
367QQuickLabsPlatformFileNameFilter *QQuickLabsPlatformFileDialog::selectedNameFilter() const
368{
369 if (!m_selectedNameFilter) {
370 QQuickLabsPlatformFileDialog *that = const_cast<QQuickLabsPlatformFileDialog *>(this);
371 m_selectedNameFilter = new QQuickLabsPlatformFileNameFilter(that);
372 m_selectedNameFilter->setOptions(m_options);
373 }
374 return m_selectedNameFilter;
375}
376
377/*!
378 \qmlproperty string Qt.labs.platform::FileDialog::defaultSuffix
379
380 This property holds a suffix that is added to selected files that have
381 no suffix specified. The suffix is typically used to indicate the file
382 type (e.g. "txt" indicates a text file).
383
384 If the first character is a dot ('.'), it is removed.
385*/
386QString QQuickLabsPlatformFileDialog::defaultSuffix() const
387{
388 return m_options->defaultSuffix();
389}
390
391void QQuickLabsPlatformFileDialog::setDefaultSuffix(const QString &suffix)
392{
393 if (suffix == m_options->defaultSuffix())
394 return;
395
396 m_options->setDefaultSuffix(suffix);
397 emit defaultSuffixChanged();
398}
399
400void QQuickLabsPlatformFileDialog::resetDefaultSuffix()
401{
402 setDefaultSuffix(QString());
403}
404
405/*!
406 \qmlproperty string Qt.labs.platform::FileDialog::acceptLabel
407
408 This property holds the label text shown on the button that accepts the dialog.
409
410 When set to an empty string, the default label of the underlying platform is used.
411 The default label is typically \uicontrol Open or \uicontrol Save depending on which
412 \l fileMode the dialog is used in.
413
414 The default value is an empty string.
415
416 \sa rejectLabel
417*/
418QString QQuickLabsPlatformFileDialog::acceptLabel() const
419{
420 return m_options->labelText(label: QFileDialogOptions::Accept);
421}
422
423void QQuickLabsPlatformFileDialog::setAcceptLabel(const QString &label)
424{
425 if (label == m_options->labelText(label: QFileDialogOptions::Accept))
426 return;
427
428 m_options->setLabelText(label: QFileDialogOptions::Accept, text: label);
429 emit acceptLabelChanged();
430}
431
432void QQuickLabsPlatformFileDialog::resetAcceptLabel()
433{
434 setAcceptLabel(QString());
435}
436
437/*!
438 \qmlproperty string Qt.labs.platform::FileDialog::rejectLabel
439
440 This property holds the label text shown on the button that rejects the dialog.
441
442 When set to an empty string, the default label of the underlying platform is used.
443 The default label is typically \uicontrol Cancel.
444
445 The default value is an empty string.
446
447 \sa acceptLabel
448*/
449QString QQuickLabsPlatformFileDialog::rejectLabel() const
450{
451 return m_options->labelText(label: QFileDialogOptions::Reject);
452}
453
454void QQuickLabsPlatformFileDialog::setRejectLabel(const QString &label)
455{
456 if (label == m_options->labelText(label: QFileDialogOptions::Reject))
457 return;
458
459 m_options->setLabelText(label: QFileDialogOptions::Reject, text: label);
460 emit rejectLabelChanged();
461}
462
463void QQuickLabsPlatformFileDialog::resetRejectLabel()
464{
465 setRejectLabel(QString());
466}
467
468bool QQuickLabsPlatformFileDialog::useNativeDialog() const
469{
470 return QQuickLabsPlatformDialog::useNativeDialog()
471 && !m_options->testOption(option: QFileDialogOptions::DontUseNativeDialog);
472}
473
474void QQuickLabsPlatformFileDialog::onCreate(QPlatformDialogHelper *dialog)
475{
476 if (QPlatformFileDialogHelper *fileDialog = qobject_cast<QPlatformFileDialogHelper *>(object: dialog)) {
477 // TODO: emit currentFileChanged only when the first entry in currentFiles changes
478 connect(sender: fileDialog, signal: &QPlatformFileDialogHelper::currentChanged, context: this, slot: &QQuickLabsPlatformFileDialog::currentFileChanged);
479 connect(sender: fileDialog, signal: &QPlatformFileDialogHelper::currentChanged, context: this, slot: &QQuickLabsPlatformFileDialog::currentFilesChanged);
480 connect(sender: fileDialog, signal: &QPlatformFileDialogHelper::directoryEntered, context: this, slot: &QQuickLabsPlatformFileDialog::folderChanged);
481 fileDialog->setOptions(m_options);
482 }
483}
484
485void QQuickLabsPlatformFileDialog::onShow(QPlatformDialogHelper *dialog)
486{
487 m_options->setWindowTitle(title());
488 if (QPlatformFileDialogHelper *fileDialog = qobject_cast<QPlatformFileDialogHelper *>(object: dialog)) {
489 fileDialog->setOptions(m_options); // setOptions only assigns a member and isn't virtual
490 if (m_firstShow && m_options->initialDirectory().isValid())
491 fileDialog->setDirectory(m_options->initialDirectory());
492 if (m_selectedNameFilter) {
493 const int index = m_selectedNameFilter->index();
494 const QString filter = m_options->nameFilters().value(i: index);
495 m_options->setInitiallySelectedNameFilter(filter);
496 fileDialog->selectNameFilter(filter);
497 connect(sender: fileDialog, signal: &QPlatformFileDialogHelper::filterSelected, context: m_selectedNameFilter, slot: &QQuickLabsPlatformFileNameFilter::update);
498 }
499 }
500 if (m_firstShow)
501 m_firstShow = false;
502}
503
504void QQuickLabsPlatformFileDialog::onHide(QPlatformDialogHelper *dialog)
505{
506 if (QPlatformFileDialogHelper *fileDialog = qobject_cast<QPlatformFileDialogHelper *>(object: dialog)) {
507 if (m_selectedNameFilter)
508 disconnect(sender: fileDialog, signal: &QPlatformFileDialogHelper::filterSelected, receiver: m_selectedNameFilter, slot: &QQuickLabsPlatformFileNameFilter::update);
509 }
510}
511
512void QQuickLabsPlatformFileDialog::accept()
513{
514 if (QPlatformFileDialogHelper *fileDialog = qobject_cast<QPlatformFileDialogHelper *>(object: handle()))
515 setFiles(fileDialog->selectedFiles());
516 QQuickLabsPlatformDialog::accept();
517}
518
519QUrl QQuickLabsPlatformFileDialog::addDefaultSuffix(const QUrl &file) const
520{
521 QUrl url = file;
522 const QString path = url.path();
523 const QString suffix = m_options->defaultSuffix();
524 // Urls with "content" scheme do not require suffixes. Such schemes are
525 // used on Android.
526 const bool isContentScheme = url.scheme() == u"content"_s;
527 if (!isContentScheme && !suffix.isEmpty() && !path.endsWith(c: QLatin1Char('/'))
528 && path.lastIndexOf(c: QLatin1Char('.')) == -1) {
529 url.setPath(path: path + QLatin1Char('.') + suffix);
530 }
531 return url;
532}
533
534QList<QUrl> QQuickLabsPlatformFileDialog::addDefaultSuffixes(const QList<QUrl> &files) const
535{
536 QList<QUrl> urls;
537 urls.reserve(asize: files.size());
538 for (const QUrl &file : files)
539 urls += addDefaultSuffix(file);
540 return urls;
541}
542
543QQuickLabsPlatformFileNameFilter::QQuickLabsPlatformFileNameFilter(QObject *parent)
544 : QObject(parent), m_index(-1)
545{
546}
547
548int QQuickLabsPlatformFileNameFilter::index() const
549{
550 return m_index;
551}
552
553void QQuickLabsPlatformFileNameFilter::setIndex(int index)
554{
555 if (m_index == index)
556 return;
557
558 m_index = index;
559 emit indexChanged(index);
560}
561
562QString QQuickLabsPlatformFileNameFilter::name() const
563{
564 return m_name;
565}
566
567QStringList QQuickLabsPlatformFileNameFilter::extensions() const
568{
569 return m_extensions;
570}
571
572QSharedPointer<QFileDialogOptions> QQuickLabsPlatformFileNameFilter::options() const
573{
574 return m_options;
575}
576
577void QQuickLabsPlatformFileNameFilter::setOptions(const QSharedPointer<QFileDialogOptions> &options)
578{
579 m_options = options;
580}
581
582static QString extractName(const QString &filter)
583{
584 return filter.left(n: filter.indexOf(c: QLatin1Char('(')) - 1);
585}
586
587static QString extractExtension(QStringView filter)
588{
589 return filter.mid(pos: filter.indexOf(c: QLatin1Char('.')) + 1).toString();
590}
591
592static QStringList extractExtensions(QStringView filter)
593{
594 QStringList extensions;
595 const int from = filter.indexOf(c: QLatin1Char('('));
596 const int to = filter.lastIndexOf(c: QLatin1Char(')')) - 1;
597 if (from >= 0 && from < to) {
598 const QStringView ref = filter.mid(pos: from + 1, n: to - from);
599 const QList<QStringView> exts = ref.split(sep: QLatin1Char(' '), behavior: Qt::SkipEmptyParts);
600 for (const QStringView &ref : exts)
601 extensions += extractExtension(filter: ref);
602 }
603
604 return extensions;
605}
606
607void QQuickLabsPlatformFileNameFilter::update(const QString &filter)
608{
609 const QStringList filters = nameFilters();
610
611 const int oldIndex = m_index;
612 const QString oldName = m_name;
613 const QStringList oldExtensions = m_extensions;
614
615 m_index = filters.indexOf(str: filter);
616 m_name = extractName(filter);
617 m_extensions = extractExtensions(filter);
618
619 if (oldIndex != m_index)
620 emit indexChanged(index: m_index);
621 if (oldName != m_name)
622 emit nameChanged(name: m_name);
623 if (oldExtensions != m_extensions)
624 emit extensionsChanged(extensions: m_extensions);
625}
626
627QStringList QQuickLabsPlatformFileNameFilter::nameFilters() const
628{
629 return m_options ? m_options->nameFilters() : QStringList();
630}
631
632QString QQuickLabsPlatformFileNameFilter::nameFilter(int index) const
633{
634 return m_options ? m_options->nameFilters().value(i: index) : QString();
635}
636
637QT_END_NAMESPACE
638
639#include "moc_qquicklabsplatformfiledialog_p.cpp"
640

source code of qtdeclarative/src/labs/platform/qquicklabsplatformfiledialog.cpp