1 | // Copyright (C) 2021 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 "qquickfiledialogimpl_p.h" |
5 | #include "qquickfiledialogimpl_p_p.h" |
6 | |
7 | #include <QtCore/qloggingcategory.h> |
8 | #include <QtGui/private/qguiapplication_p.h> |
9 | #include <QtGui/qpa/qplatformtheme.h> |
10 | #include <QtQml/qqmlinfo.h> |
11 | #include <QtQml/qqmlfile.h> |
12 | #include <QtQuick/private/qquickitemview_p_p.h> |
13 | #include <QtQuickTemplates2/private/qquickdialogbuttonbox_p_p.h> |
14 | #include <QtQuickTemplates2/private/qquickpopupitem_p_p.h> |
15 | #include <QtQuickControls2Impl/private/qquickplatformtheme_p.h> |
16 | #include <QtQuickDialogs2Utils/private/qquickfilenamefilter_p.h> |
17 | |
18 | #include "qquickfiledialogdelegate_p.h" |
19 | #include "qquickfolderbreadcrumbbar_p.h" |
20 | |
21 | QT_BEGIN_NAMESPACE |
22 | |
23 | Q_LOGGING_CATEGORY(lcCurrentFolder, "qt.quick.dialogs.quickfiledialogimpl.currentFolder" ) |
24 | Q_LOGGING_CATEGORY(lcSelectedFile, "qt.quick.dialogs.quickfiledialogimpl.selectedFile" ) |
25 | Q_LOGGING_CATEGORY(lcUpdateSelectedFile, "qt.quick.dialogs.quickfiledialogimpl.updateSelectedFile" ) |
26 | Q_LOGGING_CATEGORY(lcOptions, "qt.quick.dialogs.quickfiledialogimpl.options" ) |
27 | Q_LOGGING_CATEGORY(lcNameFilters, "qt.quick.dialogs.quickfiledialogimpl.namefilters" ) |
28 | Q_LOGGING_CATEGORY(lcAttachedNameFilters, "qt.quick.dialogs.quickfiledialogimplattached.namefilters" ) |
29 | Q_LOGGING_CATEGORY(lcAttachedCurrentIndex, "qt.quick.dialogs.quickfiledialogimplattached.currentIndex" ) |
30 | |
31 | QQuickFileDialogImplPrivate::QQuickFileDialogImplPrivate() |
32 | { |
33 | } |
34 | |
35 | void QQuickFileDialogImplPrivate::setNameFilters(const QStringList &filters) |
36 | { |
37 | Q_Q(QQuickFileDialogImpl); |
38 | if (filters == nameFilters) |
39 | return; |
40 | |
41 | nameFilters = filters; |
42 | emit q->nameFiltersChanged(); |
43 | } |
44 | |
45 | void QQuickFileDialogImplPrivate::updateEnabled() |
46 | { |
47 | Q_Q(QQuickFileDialogImpl); |
48 | QQuickFileDialogImplAttached *attached = attachedOrWarn(); |
49 | if (!attached) |
50 | return; |
51 | |
52 | auto openButton = attached->buttonBox()->standardButton(button: QPlatformDialogHelper::Open); |
53 | if (!openButton) { |
54 | qmlWarning(me: q).nospace() << "Can't update Open button's enabled state because it wasn't found" ; |
55 | return; |
56 | } |
57 | |
58 | openButton->setEnabled(!selectedFile.isEmpty() && attached->breadcrumbBar() |
59 | && !attached->breadcrumbBar()->textField()->isVisible()); |
60 | } |
61 | |
62 | /*! |
63 | \internal |
64 | |
65 | Ensures that a file is always selected after a change in \c folder. |
66 | |
67 | \a oldFolderPath is the previous value of \c folder. |
68 | */ |
69 | void QQuickFileDialogImplPrivate::updateSelectedFile(const QString &oldFolderPath) |
70 | { |
71 | Q_Q(QQuickFileDialogImpl); |
72 | QQuickFileDialogImplAttached *attached = attachedOrWarn(); |
73 | if (!attached || !attached->fileDialogListView()) |
74 | return; |
75 | |
76 | qCDebug(lcUpdateSelectedFile) << "updateSelectedFile called with oldFolderPath" << oldFolderPath; |
77 | |
78 | QString newSelectedFilePath; |
79 | int newSelectedFileIndex = -1; |
80 | const QString newFolderPath = QQmlFile::urlToLocalFileOrQrc(currentFolder); |
81 | if (!oldFolderPath.isEmpty() && !newFolderPath.isEmpty()) { |
82 | // TODO: Add another platform theme hint for this behavior too, as e.g. macOS |
83 | // doesn't do it this way. |
84 | // If the user went up a directory (or several), we should set |
85 | // selectedFile to be the directory that we were in (or |
86 | // its closest ancestor that is a child of the new directory). |
87 | // E.g. if oldFolderPath is /foo/bar/baz/abc/xyz, and newFolderPath is /foo/bar, |
88 | // then we want to set selectedFile to be /foo/bar/baz. |
89 | const int indexOfFolder = oldFolderPath.indexOf(s: newFolderPath); |
90 | if (indexOfFolder != -1) { |
91 | // [folder] |
92 | // [ oldFolderPath ] |
93 | // /foo/bar/baz/abc/xyz |
94 | // [rel...Paths] |
95 | QStringList relativePaths = oldFolderPath.mid(position: indexOfFolder + newFolderPath.size()).split(sep: QLatin1Char('/'), behavior: Qt::SkipEmptyParts); |
96 | newSelectedFilePath = newFolderPath + QLatin1Char('/') + relativePaths.first(); |
97 | |
98 | // Now find the index of that directory so that we can set the ListView's currentIndex to it. |
99 | const QDir newFolderDir(newFolderPath); |
100 | // Just to be safe... |
101 | if (!newFolderDir.exists()) { |
102 | qmlWarning(me: q) << "Directory" << newSelectedFilePath << "doesn't exist; can't get a file entry list for it" ; |
103 | return; |
104 | } |
105 | |
106 | const QFileInfoList filesInNewDir = fileList(dir: newFolderDir); |
107 | const QFileInfo newSelectedFileInfo(newSelectedFilePath); |
108 | newSelectedFileIndex = filesInNewDir.indexOf(t: newSelectedFileInfo); |
109 | } |
110 | } |
111 | |
112 | static const bool preselectFirstFile = []() { |
113 | const QVariant envVar = qEnvironmentVariable(varName: "QT_QUICK_DIALOGS_PRESELECT_FIRST_FILE" ); |
114 | if (envVar.isValid() && envVar.canConvert<bool>()) |
115 | return envVar.toBool(); |
116 | return QGuiApplicationPrivate::platformTheme()->themeHint( |
117 | hint: QPlatformTheme::PreselectFirstFileInDirectory).toBool(); |
118 | }(); |
119 | |
120 | if (preselectFirstFile && newSelectedFilePath.isEmpty()) { |
121 | // When entering into a directory that isn't a parent of the old one, the first |
122 | // file delegate should be selected. |
123 | // TODO: is there a cheaper way to do this? QDirIterator doesn't support sorting, |
124 | // so we can't use that. QQuickFolderListModel uses threads to fetch its data, |
125 | // so should be considered asynchronous. We might be able to use it, but it would |
126 | // complicate the code even more... |
127 | const QDir newFolderDir(newFolderPath); |
128 | if (newFolderDir.exists()) { |
129 | if (!cachedFileList.isEmpty()) { |
130 | newSelectedFilePath = cachedFileList.first().absoluteFilePath(); |
131 | newSelectedFileIndex = 0; |
132 | } |
133 | } |
134 | } |
135 | |
136 | const QUrl newSelectedFileUrl = QUrl::fromLocalFile(localfile: newSelectedFilePath); |
137 | qCDebug(lcUpdateSelectedFile).nospace() << "updateSelectedFile is setting selectedFile to " << newSelectedFileUrl |
138 | << ", newSelectedFileIndex is " << newSelectedFileIndex; |
139 | q->setSelectedFile(newSelectedFileUrl); |
140 | // If the index is -1, there are no files in the directory, and so fileDialogListView's |
141 | // currentIndex will already be -1. |
142 | if (newSelectedFileIndex != -1) |
143 | tryUpdateFileDialogListViewCurrentIndex(newCurrentIndex: newSelectedFileIndex); |
144 | } |
145 | |
146 | QDir::SortFlags QQuickFileDialogImplPrivate::fileListSortFlags() |
147 | { |
148 | QDir::SortFlags sortFlags = QDir::IgnoreCase; |
149 | if (QQuickPlatformTheme::getThemeHint(themeHint: QPlatformTheme::ShowDirectoriesFirst).toBool()) |
150 | sortFlags.setFlag(flag: QDir::DirsFirst); |
151 | return sortFlags; |
152 | } |
153 | |
154 | QFileInfoList QQuickFileDialogImplPrivate::fileList(const QDir &dir) |
155 | { |
156 | return dir.entryInfoList(filters: QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot, sort: fileListSortFlags()); |
157 | } |
158 | |
159 | void QQuickFileDialogImplPrivate::setFileDialogListViewCurrentIndex(int newCurrentIndex) |
160 | { |
161 | qCDebug(lcSelectedFile) << "setting fileDialogListView's currentIndex to" << newCurrentIndex; |
162 | |
163 | // We block signals from ListView because we don't want fileDialogListViewCurrentIndexChanged |
164 | // to be called, as the file it gets from the delegate will not be up-to-date (but most |
165 | // importantly because we already just set the selected file). |
166 | QQuickFileDialogImplAttached *attached = attachedOrWarn(); |
167 | const QSignalBlocker blocker(attached->fileDialogListView()); |
168 | attached->fileDialogListView()->setCurrentIndex(newCurrentIndex); |
169 | attached->fileDialogListView()->positionViewAtIndex(index: newCurrentIndex, mode: QQuickListView::Center); |
170 | if (QQuickItem *currentItem = attached->fileDialogListView()->currentItem()) |
171 | currentItem->forceActiveFocus(); |
172 | } |
173 | |
174 | /*! |
175 | \internal |
176 | |
177 | Tries to set the currentIndex of fileDialogListView to \a newCurrentIndex and gives |
178 | focus to the current item. |
179 | */ |
180 | void QQuickFileDialogImplPrivate::tryUpdateFileDialogListViewCurrentIndex(int newCurrentIndex) |
181 | { |
182 | qCDebug(lcSelectedFile) << "tryUpdateFileDialogListViewCurrentIndex called with newCurrentIndex" << newCurrentIndex; |
183 | QQuickFileDialogImplAttached *attached = attachedOrWarn(); |
184 | Q_ASSERT(attached); |
185 | Q_ASSERT(attached->fileDialogListView()); |
186 | |
187 | // We were likely trying to set an index for a file that the ListView hadn't loaded yet. |
188 | // We need to wait until the ListView has loaded all expected items, but since we have no |
189 | // efficient way of verifying that, we just check that the count is as expected. |
190 | if (newCurrentIndex != -1 && newCurrentIndex >= attached->fileDialogListView()->count()) { |
191 | qCDebug(lcSelectedFile) << "- trying to set currentIndex to" << newCurrentIndex |
192 | << "but fileDialogListView only has" << attached->fileDialogListView()->count() |
193 | << "items; setting pendingCurrentIndexToSet to" << newCurrentIndex; |
194 | pendingCurrentIndexToSet = newCurrentIndex; |
195 | QObjectPrivate::connect(sender: attached->fileDialogListView(), signal: &QQuickItemView::countChanged, |
196 | receiverPrivate: this, slot: &QQuickFileDialogImplPrivate::fileDialogListViewCountChanged, type: Qt::ConnectionType(Qt::DirectConnection | Qt::UniqueConnection)); |
197 | return; |
198 | } |
199 | |
200 | setFileDialogListViewCurrentIndex(newCurrentIndex); |
201 | } |
202 | |
203 | void QQuickFileDialogImplPrivate::fileDialogListViewCountChanged() |
204 | { |
205 | QQuickFileDialogImplAttached *attached = attachedOrWarn(); |
206 | qCDebug(lcSelectedFile) << "fileDialogListView count changed to" << attached->fileDialogListView()->count(); |
207 | |
208 | if (pendingCurrentIndexToSet != -1 && pendingCurrentIndexToSet < attached->fileDialogListView()->count()) { |
209 | // The view now has all of the items we expect it to, so we can set |
210 | // its currentIndex back to the selected file. |
211 | qCDebug(lcSelectedFile) << "- ListView has expected count;" |
212 | << "applying pending fileDialogListView currentIndex" << pendingCurrentIndexToSet; |
213 | |
214 | QObjectPrivate::disconnect(sender: attached->fileDialogListView(), signal: &QQuickItemView::countChanged, |
215 | receiverPrivate: this, slot: &QQuickFileDialogImplPrivate::fileDialogListViewCountChanged); |
216 | setFileDialogListViewCurrentIndex(pendingCurrentIndexToSet); |
217 | pendingCurrentIndexToSet = -1; |
218 | qCDebug(lcSelectedFile) << "- reset pendingCurrentIndexToSet to -1" ; |
219 | } else { |
220 | qCDebug(lcSelectedFile) << "- ListView doesn't yet have expected count of" << cachedFileList.size(); |
221 | } |
222 | } |
223 | |
224 | void QQuickFileDialogImplPrivate::handleAccept() |
225 | { |
226 | // Let handleClick take care of calling accept(). |
227 | } |
228 | |
229 | void QQuickFileDialogImplPrivate::handleClick(QQuickAbstractButton *button) |
230 | { |
231 | Q_Q(QQuickFileDialogImpl); |
232 | if (buttonRole(button) == QPlatformDialogHelper::AcceptRole && selectedFile.isValid()) { |
233 | // The "Open" button was clicked, so we need to set the file to the current file, if any. |
234 | const QFileInfo fileInfo(selectedFile.toLocalFile()); |
235 | if (fileInfo.isDir()) { |
236 | // If it's a directory, navigate to it. |
237 | q->setCurrentFolder(currentFolder: selectedFile); |
238 | // Don't call accept(), because selecting a folder != accepting the dialog. |
239 | } else { |
240 | // Otherwise it's a file, so select it and close the dialog. |
241 | q->setSelectedFile(selectedFile); |
242 | q->accept(); |
243 | QQuickDialogPrivate::handleClick(button); |
244 | emit q->fileSelected(fileUrl: selectedFile); |
245 | } |
246 | } |
247 | } |
248 | |
249 | QQuickFileDialogImpl::QQuickFileDialogImpl(QObject *parent) |
250 | : QQuickDialog(*(new QQuickFileDialogImplPrivate), parent) |
251 | { |
252 | } |
253 | |
254 | QQuickFileDialogImplAttached *QQuickFileDialogImpl::qmlAttachedProperties(QObject *object) |
255 | { |
256 | return new QQuickFileDialogImplAttached(object); |
257 | } |
258 | |
259 | QUrl QQuickFileDialogImpl::currentFolder() const |
260 | { |
261 | Q_D(const QQuickFileDialogImpl); |
262 | return d->currentFolder; |
263 | } |
264 | |
265 | void QQuickFileDialogImpl::setCurrentFolder(const QUrl ¤tFolder, SetReason setReason) |
266 | { |
267 | Q_D(QQuickFileDialogImpl); |
268 | qCDebug(lcCurrentFolder).nospace() << "setCurrentFolder called with " << currentFolder |
269 | << " (old currentFolder is " << d->currentFolder << ")" ; |
270 | |
271 | // As we would otherwise get the file list from scratch in a couple of places, |
272 | // just get it once and cache it. |
273 | // We need to cache it before the equality check, otherwise opening the dialog |
274 | // several times in the same directory wouldn't update the cache. |
275 | if (!currentFolder.isEmpty()) |
276 | d->cachedFileList = d->fileList(dir: QQmlFile::urlToLocalFileOrQrc(currentFolder)); |
277 | else |
278 | d->cachedFileList.clear(); |
279 | qCDebug(lcCurrentFolder) << "- cachedFileList size is now " << d->cachedFileList.size(); |
280 | |
281 | if (currentFolder == d->currentFolder) |
282 | return; |
283 | |
284 | const QString oldFolderPath = QQmlFile::urlToLocalFileOrQrc(d->currentFolder); |
285 | |
286 | d->currentFolder = currentFolder; |
287 | // Don't update the selectedFile if it's an Internal set, as that |
288 | // means that the user just set selectedFile, and we're being called as a result of that. |
289 | if (setReason == SetReason::External) { |
290 | // Since the directory changed, the old file can no longer be selected. |
291 | d->updateSelectedFile(oldFolderPath); |
292 | } |
293 | emit currentFolderChanged(folderUrl: d->currentFolder); |
294 | } |
295 | |
296 | QUrl QQuickFileDialogImpl::selectedFile() const |
297 | { |
298 | Q_D(const QQuickFileDialogImpl); |
299 | return d->selectedFile; |
300 | } |
301 | |
302 | /*! |
303 | \internal |
304 | |
305 | This is mostly called as a result of user interaction, but is also |
306 | called (indirectly) by QQuickFileDialog::onShow when the user set an initial |
307 | selectedFile. |
308 | */ |
309 | void QQuickFileDialogImpl::setSelectedFile(const QUrl &selectedFile) |
310 | { |
311 | qCDebug(lcSelectedFile) << "setSelectedFile called with" << selectedFile; |
312 | Q_D(QQuickFileDialogImpl); |
313 | if (selectedFile == d->selectedFile) |
314 | return; |
315 | |
316 | d->selectedFile = selectedFile; |
317 | d->updateEnabled(); |
318 | emit selectedFileChanged(selectedFileUrl: d->selectedFile); |
319 | } |
320 | |
321 | /*! |
322 | \internal |
323 | |
324 | Called when showing the FileDialog each time, so long as |
325 | QFileDialogOptions::initiallySelectedFiles is not empty. |
326 | */ |
327 | void QQuickFileDialogImpl::setInitialCurrentFolderAndSelectedFile(const QUrl &file) |
328 | { |
329 | Q_D(QQuickFileDialogImpl); |
330 | const QUrl fileDirUrl = QUrl::fromLocalFile(localfile: QFileInfo(file.toLocalFile()).dir().absolutePath()); |
331 | const bool currentFolderChanged = d->currentFolder != fileDirUrl; |
332 | qCDebug(lcSelectedFile) << "setting initial currentFolder to" << fileDirUrl << "and selectedFile to" << file; |
333 | setCurrentFolder(currentFolder: fileDirUrl, setReason: QQuickFileDialogImpl::SetReason::Internal); |
334 | setSelectedFile(file); |
335 | d->setCurrentIndexToInitiallySelectedFile = true; |
336 | |
337 | // If the currentFolder didn't change, the FolderListModel won't change and |
338 | // neither will the ListView. This means that setFileDialogListViewCurrentIndex |
339 | // will never get called and the currentIndex will not reflect selectedFile. |
340 | // We need to account for that here. |
341 | if (!currentFolderChanged) { |
342 | const QFileInfo newSelectedFileInfo(d->selectedFile.toLocalFile()); |
343 | const int indexOfSelectedFileInFileDialogListView = d->cachedFileList.indexOf(t: newSelectedFileInfo); |
344 | d->tryUpdateFileDialogListViewCurrentIndex(newCurrentIndex: indexOfSelectedFileInFileDialogListView); |
345 | } |
346 | } |
347 | |
348 | QSharedPointer<QFileDialogOptions> QQuickFileDialogImpl::options() const |
349 | { |
350 | Q_D(const QQuickFileDialogImpl); |
351 | return d->options; |
352 | } |
353 | |
354 | void QQuickFileDialogImpl::setOptions(const QSharedPointer<QFileDialogOptions> &options) |
355 | { |
356 | qCDebug(lcOptions).nospace() << "setOptions called with:" |
357 | << " acceptMode=" << options->acceptMode() |
358 | << " fileMode=" << options->fileMode() |
359 | << " initialDirectory=" << options->initialDirectory() |
360 | << " nameFilters=" << options->nameFilters() |
361 | << " initiallySelectedNameFilter=" << options->initiallySelectedNameFilter(); |
362 | |
363 | Q_D(QQuickFileDialogImpl); |
364 | d->options = options; |
365 | |
366 | if (d->options) { |
367 | d->selectedNameFilter->setOptions(options); |
368 | d->setNameFilters(options->nameFilters()); |
369 | |
370 | if (auto attached = d->attachedOrWarn()) { |
371 | const bool isSaveMode = d->options->fileMode() == QFileDialogOptions::AnyFile; |
372 | attached->fileNameLabel()->setVisible(isSaveMode); |
373 | attached->fileNameTextField()->setVisible(isSaveMode); |
374 | } |
375 | } |
376 | } |
377 | |
378 | /*! |
379 | \internal |
380 | |
381 | The list of user-facing strings describing the available file filters. |
382 | */ |
383 | QStringList QQuickFileDialogImpl::nameFilters() const |
384 | { |
385 | Q_D(const QQuickFileDialogImpl); |
386 | return d->options ? d->options->nameFilters() : QStringList(); |
387 | } |
388 | |
389 | void QQuickFileDialogImpl::resetNameFilters() |
390 | { |
391 | Q_D(QQuickFileDialogImpl); |
392 | d->setNameFilters(QStringList()); |
393 | } |
394 | |
395 | QQuickFileNameFilter *QQuickFileDialogImpl::selectedNameFilter() const |
396 | { |
397 | Q_D(const QQuickFileDialogImpl); |
398 | if (!d->selectedNameFilter) { |
399 | QQuickFileDialogImpl *that = const_cast<QQuickFileDialogImpl *>(this); |
400 | d->selectedNameFilter = new QQuickFileNameFilter(that); |
401 | if (d->options) |
402 | d->selectedNameFilter->setOptions(d->options); |
403 | } |
404 | return d->selectedNameFilter; |
405 | } |
406 | |
407 | /*! |
408 | \internal |
409 | |
410 | These allow QQuickPlatformFileDialog::show() to set custom labels on the |
411 | dialog buttons without having to know about/go through QQuickFileDialogImplAttached |
412 | and QQuickDialogButtonBox. |
413 | */ |
414 | void QQuickFileDialogImpl::setAcceptLabel(const QString &label) |
415 | { |
416 | Q_D(QQuickFileDialogImpl); |
417 | d->acceptLabel = label; |
418 | QQuickFileDialogImplAttached *attached = d->attachedOrWarn(); |
419 | if (!attached) |
420 | return; |
421 | |
422 | auto acceptButton = attached->buttonBox()->standardButton(button: QPlatformDialogHelper::Open); |
423 | if (!acceptButton) { |
424 | qmlWarning(me: this).nospace() << "Can't set accept label to " << label |
425 | << "; failed to find Open button in DialogButtonBox of " << this; |
426 | return; |
427 | } |
428 | |
429 | acceptButton->setText(!label.isEmpty() |
430 | ? label : QQuickDialogButtonBoxPrivate::buttonText(standardButton: QPlatformDialogHelper::Open)); |
431 | } |
432 | |
433 | void QQuickFileDialogImpl::setRejectLabel(const QString &label) |
434 | { |
435 | Q_D(QQuickFileDialogImpl); |
436 | d->rejectLabel = label; |
437 | QQuickFileDialogImplAttached *attached = d->attachedOrWarn(); |
438 | if (!attached) |
439 | return; |
440 | |
441 | auto rejectButton = attached->buttonBox()->standardButton(button: QPlatformDialogHelper::Cancel); |
442 | if (!rejectButton) { |
443 | qmlWarning(me: this).nospace() << "Can't set reject label to " << label |
444 | << "; failed to find Open button in DialogButtonBox of " << this; |
445 | return; |
446 | } |
447 | |
448 | rejectButton->setText(!label.isEmpty() |
449 | ? label : QQuickDialogButtonBoxPrivate::buttonText(standardButton: QPlatformDialogHelper::Cancel)); |
450 | } |
451 | |
452 | void QQuickFileDialogImpl::selectNameFilter(const QString &filter) |
453 | { |
454 | qCDebug(lcNameFilters) << "selectNameFilter called with" << filter; |
455 | Q_D(QQuickFileDialogImpl); |
456 | d->selectedNameFilter->update(filter); |
457 | emit filterSelected(filter); |
458 | } |
459 | |
460 | QString QQuickFileDialogImpl::fileName() const |
461 | { |
462 | return selectedFile().fileName(); |
463 | } |
464 | void QQuickFileDialogImpl::setFileName(const QString &fileName) |
465 | { |
466 | const QString previous = selectedFile().fileName(); |
467 | if (previous == fileName) |
468 | return; |
469 | |
470 | setSelectedFile(QUrl(currentFolder().path() + u'/' + fileName)); |
471 | } |
472 | |
473 | void QQuickFileDialogImpl::componentComplete() |
474 | { |
475 | Q_D(QQuickFileDialogImpl); |
476 | QQuickDialog::componentComplete(); |
477 | |
478 | // Find the right-most button and set its key navigation so that |
479 | // tab moves focus to the breadcrumb bar's up button. I tried |
480 | // doing this via KeyNavigation on the DialogButtonBox in QML, |
481 | // but it didn't work (probably because it's not the right item). |
482 | QQuickFileDialogImplAttached *attached = d->attachedOrWarn(); |
483 | if (!attached) |
484 | return; |
485 | |
486 | const int buttonCount = attached->buttonBox()->count(); |
487 | if (buttonCount == 0) |
488 | return; |
489 | |
490 | QQuickAbstractButton *rightMostButton = qobject_cast<QQuickAbstractButton *>( |
491 | object: attached->buttonBox()->itemAt(index: buttonCount - 1)); |
492 | if (!rightMostButton) { |
493 | qmlWarning(me: this) << "Can't find right-most button in DialogButtonBox" ; |
494 | return; |
495 | } |
496 | |
497 | auto keyNavigationAttached = QQuickKeyNavigationAttached::qmlAttachedProperties(rightMostButton); |
498 | if (!keyNavigationAttached) { |
499 | qmlWarning(me: this) << "Can't create attached KeyNavigation object on" << QDebug::toString(object&: rightMostButton); |
500 | return; |
501 | } |
502 | |
503 | keyNavigationAttached->setTab(attached->breadcrumbBar()->upButton()); |
504 | } |
505 | |
506 | void QQuickFileDialogImpl::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &data) |
507 | { |
508 | Q_D(QQuickFileDialogImpl); |
509 | QQuickDialog::itemChange(change, data); |
510 | |
511 | if (change != QQuickItem::ItemVisibleHasChanged || !isComponentComplete() || !data.boolValue) |
512 | return; |
513 | |
514 | QQuickFileDialogImplAttached *attached = d->attachedOrWarn(); |
515 | if (!attached) |
516 | return; |
517 | |
518 | attached->fileDialogListView()->forceActiveFocus(); |
519 | d->updateEnabled(); |
520 | } |
521 | |
522 | QQuickFileDialogImplAttached *QQuickFileDialogImplPrivate::attachedOrWarn() |
523 | { |
524 | Q_Q(QQuickFileDialogImpl); |
525 | QQuickFileDialogImplAttached *attached = static_cast<QQuickFileDialogImplAttached*>( |
526 | qmlAttachedPropertiesObject<QQuickFileDialogImpl>(obj: q, create: false)); |
527 | if (!attached) |
528 | qmlWarning(me: q) << "Expected FileDialogImpl attached object to be present on" << this; |
529 | return attached; |
530 | } |
531 | |
532 | void QQuickFileDialogImplAttachedPrivate::nameFiltersComboBoxItemActivated(int index) |
533 | { |
534 | qCDebug(lcAttachedNameFilters) << "nameFiltersComboBoxItemActivated called with" << index; |
535 | auto fileDialogImpl = qobject_cast<QQuickFileDialogImpl*>(object: parent); |
536 | if (!fileDialogImpl) |
537 | return; |
538 | |
539 | fileDialogImpl->selectNameFilter(filter: nameFiltersComboBox->textAt(index)); |
540 | } |
541 | |
542 | void QQuickFileDialogImplAttachedPrivate::fileDialogListViewCurrentIndexChanged() |
543 | { |
544 | auto fileDialogImpl = qobject_cast<QQuickFileDialogImpl*>(object: parent); |
545 | if (!fileDialogImpl) |
546 | return; |
547 | |
548 | auto fileDialogDelegate = qobject_cast<QQuickFileDialogDelegate*>(object: fileDialogListView->currentItem()); |
549 | if (!fileDialogDelegate) |
550 | return; |
551 | |
552 | const QQuickItemViewPrivate::MovementReason moveReason = QQuickItemViewPrivate::get(o: fileDialogListView)->moveReason; |
553 | qCDebug(lcAttachedCurrentIndex).nospace() << "fileDialogListView currentIndex changed to " << fileDialogListView->currentIndex() |
554 | << " with moveReason " << moveReason |
555 | << "; the file at that index is " << fileDialogDelegate->file(); |
556 | |
557 | // Only update selectedFile if the currentIndex changed as a result of user interaction; |
558 | // things like model changes (i.e. QQuickItemViewPrivate::applyModelChanges() calling |
559 | // QQuickItemViewPrivate::updateCurrent as a result of us changing the directory on the FolderListModel) |
560 | // shouldn't cause the selectedFile to change. |
561 | auto fileDialogImplPrivate = QQuickFileDialogImplPrivate::get(dialog: fileDialogImpl); |
562 | if (moveReason != QQuickItemViewPrivate::Other) { |
563 | fileDialogImpl->setSelectedFile(fileDialogDelegate->file()); |
564 | } else if (fileDialogImplPrivate->setCurrentIndexToInitiallySelectedFile) { |
565 | // When setting selectedFile before opening the FileDialog, |
566 | // we need to ensure that the currentIndex is correct, because the initial change |
567 | // in directory will cause the underyling FolderListModel to change its folder property, |
568 | // which in turn resets the fileDialogListView's currentIndex to 0. |
569 | const QFileInfo newSelectedFileInfo(fileDialogImplPrivate->selectedFile.toLocalFile()); |
570 | const int indexOfSelectedFileInFileDialogListView = fileDialogImplPrivate->cachedFileList.indexOf(t: newSelectedFileInfo); |
571 | fileDialogImplPrivate->tryUpdateFileDialogListViewCurrentIndex(newCurrentIndex: indexOfSelectedFileInFileDialogListView); |
572 | fileDialogImplPrivate->setCurrentIndexToInitiallySelectedFile = false; |
573 | } |
574 | } |
575 | |
576 | void QQuickFileDialogImplAttachedPrivate::fileNameChangedByUser() |
577 | { |
578 | auto fileDialogImpl = qobject_cast<QQuickFileDialogImpl *>(object: parent); |
579 | if (!fileDialogImpl) |
580 | return; |
581 | |
582 | fileDialogImpl->setFileName(fileNameTextField->text()); |
583 | } |
584 | |
585 | QQuickFileDialogImplAttached::QQuickFileDialogImplAttached(QObject *parent) |
586 | : QObject(*(new QQuickFileDialogImplAttachedPrivate), parent) |
587 | { |
588 | if (!qobject_cast<QQuickFileDialogImpl*>(object: parent)) { |
589 | qmlWarning(me: this) << "FileDialogImpl attached properties should only be " |
590 | << "accessed through the root FileDialogImpl instance" ; |
591 | } |
592 | } |
593 | |
594 | QQuickDialogButtonBox *QQuickFileDialogImplAttached::buttonBox() const |
595 | { |
596 | Q_D(const QQuickFileDialogImplAttached); |
597 | return d->buttonBox; |
598 | } |
599 | |
600 | void QQuickFileDialogImplAttached::setButtonBox(QQuickDialogButtonBox *buttonBox) |
601 | { |
602 | Q_D(QQuickFileDialogImplAttached); |
603 | if (buttonBox == d->buttonBox) |
604 | return; |
605 | |
606 | if (d->buttonBox) { |
607 | QQuickFileDialogImpl *fileDialogImpl = qobject_cast<QQuickFileDialogImpl*>(object: parent()); |
608 | if (fileDialogImpl) { |
609 | auto dialogPrivate = QQuickDialogPrivate::get(dialog: fileDialogImpl); |
610 | QObjectPrivate::disconnect(sender: d->buttonBox, signal: &QQuickDialogButtonBox::accepted, |
611 | receiverPrivate: dialogPrivate, slot: &QQuickDialogPrivate::handleAccept); |
612 | QObjectPrivate::disconnect(sender: d->buttonBox, signal: &QQuickDialogButtonBox::rejected, |
613 | receiverPrivate: dialogPrivate, slot: &QQuickDialogPrivate::handleReject); |
614 | QObjectPrivate::disconnect(sender: d->buttonBox, signal: &QQuickDialogButtonBox::clicked, |
615 | receiverPrivate: dialogPrivate, slot: &QQuickDialogPrivate::handleClick); |
616 | } |
617 | } |
618 | |
619 | d->buttonBox = buttonBox; |
620 | |
621 | if (buttonBox) { |
622 | QQuickFileDialogImpl *fileDialogImpl = qobject_cast<QQuickFileDialogImpl*>(object: parent()); |
623 | if (fileDialogImpl) { |
624 | auto dialogPrivate = QQuickDialogPrivate::get(dialog: fileDialogImpl); |
625 | QObjectPrivate::connect(sender: d->buttonBox, signal: &QQuickDialogButtonBox::accepted, |
626 | receiverPrivate: dialogPrivate, slot: &QQuickDialogPrivate::handleAccept); |
627 | QObjectPrivate::connect(sender: d->buttonBox, signal: &QQuickDialogButtonBox::rejected, |
628 | receiverPrivate: dialogPrivate, slot: &QQuickDialogPrivate::handleReject); |
629 | QObjectPrivate::connect(sender: d->buttonBox, signal: &QQuickDialogButtonBox::clicked, |
630 | receiverPrivate: dialogPrivate, slot: &QQuickDialogPrivate::handleClick); |
631 | } |
632 | } |
633 | |
634 | emit buttonBoxChanged(); |
635 | } |
636 | |
637 | QQuickComboBox *QQuickFileDialogImplAttached::nameFiltersComboBox() const |
638 | { |
639 | Q_D(const QQuickFileDialogImplAttached); |
640 | return d->nameFiltersComboBox; |
641 | } |
642 | |
643 | void QQuickFileDialogImplAttached::setNameFiltersComboBox(QQuickComboBox *nameFiltersComboBox) |
644 | { |
645 | Q_D(QQuickFileDialogImplAttached); |
646 | if (nameFiltersComboBox == d->nameFiltersComboBox) |
647 | return; |
648 | |
649 | d->nameFiltersComboBox = nameFiltersComboBox; |
650 | |
651 | QObjectPrivate::connect(sender: d->nameFiltersComboBox, signal: &QQuickComboBox::activated, |
652 | receiverPrivate: d, slot: &QQuickFileDialogImplAttachedPrivate::nameFiltersComboBoxItemActivated); |
653 | |
654 | emit nameFiltersComboBoxChanged(); |
655 | } |
656 | |
657 | QString QQuickFileDialogImplAttached::selectedNameFilter() const |
658 | { |
659 | Q_D(const QQuickFileDialogImplAttached); |
660 | return d->nameFiltersComboBox ? d->nameFiltersComboBox->currentText() : QString(); |
661 | } |
662 | |
663 | void QQuickFileDialogImplAttached::selectNameFilter(const QString &filter) |
664 | { |
665 | Q_D(QQuickFileDialogImplAttached); |
666 | qCDebug(lcAttachedNameFilters) << "selectNameFilter called with" << filter; |
667 | if (!d->nameFiltersComboBox) |
668 | return; |
669 | |
670 | const int indexInComboBox = d->nameFiltersComboBox->find(text: filter); |
671 | if (indexInComboBox == -1) |
672 | return; |
673 | |
674 | qCDebug(lcAttachedNameFilters) << "setting ComboBox's currentIndex to" << indexInComboBox; |
675 | d->nameFiltersComboBox->setCurrentIndex(indexInComboBox); |
676 | } |
677 | |
678 | QQuickListView *QQuickFileDialogImplAttached::fileDialogListView() const |
679 | { |
680 | Q_D(const QQuickFileDialogImplAttached); |
681 | return d->fileDialogListView; |
682 | } |
683 | |
684 | void QQuickFileDialogImplAttached::setFileDialogListView(QQuickListView *fileDialogListView) |
685 | { |
686 | Q_D(QQuickFileDialogImplAttached); |
687 | if (fileDialogListView == d->fileDialogListView) |
688 | return; |
689 | |
690 | d->fileDialogListView = fileDialogListView; |
691 | |
692 | QObjectPrivate::connect(sender: d->fileDialogListView, signal: &QQuickListView::currentIndexChanged, |
693 | receiverPrivate: d, slot: &QQuickFileDialogImplAttachedPrivate::fileDialogListViewCurrentIndexChanged); |
694 | |
695 | emit fileDialogListViewChanged(); |
696 | } |
697 | |
698 | QQuickFolderBreadcrumbBar *QQuickFileDialogImplAttached::breadcrumbBar() const |
699 | { |
700 | Q_D(const QQuickFileDialogImplAttached); |
701 | return d->breadcrumbBar; |
702 | } |
703 | |
704 | void QQuickFileDialogImplAttached::setBreadcrumbBar(QQuickFolderBreadcrumbBar *breadcrumbBar) |
705 | { |
706 | Q_D(QQuickFileDialogImplAttached); |
707 | if (breadcrumbBar == d->breadcrumbBar) |
708 | return; |
709 | |
710 | d->breadcrumbBar = breadcrumbBar; |
711 | emit breadcrumbBarChanged(); |
712 | } |
713 | |
714 | QQuickLabel *QQuickFileDialogImplAttached::fileNameLabel() const |
715 | { |
716 | Q_D(const QQuickFileDialogImplAttached); |
717 | return d->fileNameLabel; |
718 | } |
719 | |
720 | void QQuickFileDialogImplAttached::setFileNameLabel(QQuickLabel *fileNameLabel) |
721 | { |
722 | Q_D(QQuickFileDialogImplAttached); |
723 | if (fileNameLabel == d->fileNameLabel) |
724 | return; |
725 | |
726 | d->fileNameLabel = fileNameLabel; |
727 | |
728 | emit fileNameLabelChanged(); |
729 | } |
730 | |
731 | QQuickTextField *QQuickFileDialogImplAttached::fileNameTextField() const |
732 | { |
733 | Q_D(const QQuickFileDialogImplAttached); |
734 | return d->fileNameTextField; |
735 | } |
736 | |
737 | void QQuickFileDialogImplAttached::setFileNameTextField(QQuickTextField *fileNameTextField) |
738 | { |
739 | Q_D(QQuickFileDialogImplAttached); |
740 | if (fileNameTextField == d->fileNameTextField) |
741 | return; |
742 | |
743 | if (d->fileNameTextField) |
744 | QObjectPrivate::disconnect(sender: d->fileNameTextField, signal: &QQuickTextField::editingFinished, |
745 | receiverPrivate: d, slot: &QQuickFileDialogImplAttachedPrivate::fileNameChangedByUser); |
746 | |
747 | d->fileNameTextField = fileNameTextField; |
748 | |
749 | if (d->fileNameTextField) |
750 | QObjectPrivate::connect(sender: d->fileNameTextField, signal: &QQuickTextField::editingFinished, |
751 | receiverPrivate: d, slot: &QQuickFileDialogImplAttachedPrivate::fileNameChangedByUser); |
752 | |
753 | emit fileNameTextFieldChanged(); |
754 | } |
755 | |
756 | QT_END_NAMESPACE |
757 | |
758 | #include "moc_qquickfiledialogimpl_p.cpp" |
759 | |