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 "qquickfolderdialogimpl_p.h" |
5 | #include "qquickfolderdialogimpl_p_p.h" |
6 | |
7 | #include <QtCore/qloggingcategory.h> |
8 | #include <QtQuickTemplates2/private/qquickdialogbuttonbox_p_p.h> |
9 | |
10 | #include "qquickfiledialogdelegate_p.h" |
11 | #include "qquickfolderbreadcrumbbar_p.h" |
12 | |
13 | QT_BEGIN_NAMESPACE |
14 | |
15 | Q_LOGGING_CATEGORY(lcFolderDialogCurrentFolder, "qt.quick.dialogs.quickfolderdialogimpl.currentFolder" ) |
16 | Q_LOGGING_CATEGORY(lcFolderDialogSelectedFolder, "qt.quick.dialogs.quickfolderdialogimpl.selectedFolder" ) |
17 | Q_LOGGING_CATEGORY(lcFolderDialogOptions, "qt.quick.dialogs.quickfolderdialogimpl.options" ) |
18 | |
19 | QQuickFolderDialogImplPrivate::QQuickFolderDialogImplPrivate() |
20 | { |
21 | } |
22 | |
23 | void QQuickFolderDialogImplPrivate::updateEnabled() |
24 | { |
25 | Q_Q(QQuickFolderDialogImpl); |
26 | if (!buttonBox) |
27 | return; |
28 | |
29 | QQuickFolderDialogImplAttached *attached = attachedOrWarn(); |
30 | if (!attached) |
31 | return; |
32 | |
33 | auto openButton = buttonBox->standardButton(button: QPlatformDialogHelper::Open); |
34 | if (!openButton) { |
35 | qmlWarning(me: q).nospace() << "Can't update Open button's enabled state because it wasn't found" ; |
36 | return; |
37 | } |
38 | |
39 | openButton->setEnabled(!selectedFolder.isEmpty() && attached->breadcrumbBar() |
40 | && !attached->breadcrumbBar()->textField()->isVisible()); |
41 | } |
42 | |
43 | /*! |
44 | \internal |
45 | |
46 | Ensures that a folder is always selected after a change in \c currentFolder. |
47 | |
48 | \a oldFolderPath is the previous value of \c currentFolder. |
49 | */ |
50 | void QQuickFolderDialogImplPrivate::updateSelectedFolder(const QString &oldFolderPath) |
51 | { |
52 | Q_Q(QQuickFolderDialogImpl); |
53 | QQuickFolderDialogImplAttached *attached = attachedOrWarn(); |
54 | if (!attached || !attached->folderDialogListView()) |
55 | return; |
56 | |
57 | QString newSelectedFolderPath; |
58 | int newSelectedFolderIndex = 0; |
59 | const QString newFolderPath = QQmlFile::urlToLocalFileOrQrc(currentFolder); |
60 | if (!oldFolderPath.isEmpty() && !newFolderPath.isEmpty()) { |
61 | // If the user went up a directory (or several), we should set |
62 | // selectedFolder to be the directory that we were in (or |
63 | // its closest ancestor that is a child of the new directory). |
64 | // E.g. if oldFolderPath is /foo/bar/baz/abc/xyz, and newFolderPath is /foo/bar, |
65 | // then we want to set selectedFolder to be /foo/bar/baz. |
66 | const int indexOfFolder = oldFolderPath.indexOf(s: newFolderPath); |
67 | if (indexOfFolder != -1) { |
68 | // [folder] |
69 | // [ oldFolderPath ] |
70 | // /foo/bar/baz/abc/xyz |
71 | // [rel...Paths] |
72 | QStringList relativePaths = oldFolderPath.mid(position: indexOfFolder + newFolderPath.size()).split(sep: QLatin1Char('/'), behavior: Qt::SkipEmptyParts); |
73 | newSelectedFolderPath = newFolderPath + QLatin1Char('/') + relativePaths.first(); |
74 | |
75 | // Now find the index of that directory so that we can set the ListView's currentIndex to it. |
76 | const QDir newFolderDir(newFolderPath); |
77 | // Just to be safe... |
78 | if (!newFolderDir.exists()) { |
79 | qmlWarning(me: q) << "Directory" << newSelectedFolderPath << "doesn't exist; can't get a file entry list for it" ; |
80 | return; |
81 | } |
82 | |
83 | const QFileInfoList dirs = newFolderDir.entryInfoList(filters: QDir::Dirs | QDir::NoDotAndDotDot, sort: QDir::DirsFirst); |
84 | const QFileInfo newSelectedFileInfo(newSelectedFolderPath); |
85 | // The directory can contain files, but since we put dirs first, that should never affect the indices. |
86 | newSelectedFolderIndex = dirs.indexOf(t: newSelectedFileInfo); |
87 | } |
88 | } |
89 | |
90 | if (newSelectedFolderPath.isEmpty()) { |
91 | // When entering into a directory that isn't a parent of the old one, the first |
92 | // file delegate should be selected. |
93 | // TODO: is there a cheaper way to do this? QDirIterator doesn't support sorting, |
94 | // so we can't use that. QQuickFolderListModel uses threads to fetch its data, |
95 | // so should be considered asynchronous. We might be able to use it, but it would |
96 | // complicate the code even more... |
97 | QDir newFolderDir(newFolderPath); |
98 | if (newFolderDir.exists()) { |
99 | const QFileInfoList files = newFolderDir.entryInfoList(filters: QDir::Dirs | QDir::NoDotAndDotDot, sort: QDir::DirsFirst); |
100 | if (!files.isEmpty()) |
101 | newSelectedFolderPath = files.first().absoluteFilePath(); |
102 | } |
103 | } |
104 | |
105 | const bool folderSelected = !newSelectedFolderPath.isEmpty(); |
106 | q->setSelectedFolder(folderSelected ? QUrl::fromLocalFile(localfile: newSelectedFolderPath) : QUrl()); |
107 | { |
108 | // Set the appropriate currentIndex for the selected folder. We block signals from ListView |
109 | // because we don't want folderDialogListViewCurrentIndexChanged to be called, as the file |
110 | // it gets from the delegate will not be up-to-date (but most importantly because we already |
111 | // just set the selected folder). |
112 | QSignalBlocker blocker(attached->folderDialogListView()); |
113 | attached->folderDialogListView()->setCurrentIndex(folderSelected ? newSelectedFolderIndex : -1); |
114 | } |
115 | if (folderSelected) { |
116 | if (QQuickItem *currentItem = attached->folderDialogListView()->currentItem()) |
117 | currentItem->forceActiveFocus(); |
118 | } |
119 | } |
120 | |
121 | void QQuickFolderDialogImplPrivate::handleAccept() |
122 | { |
123 | // Let handleClick take care of calling accept(). |
124 | } |
125 | |
126 | void QQuickFolderDialogImplPrivate::handleClick(QQuickAbstractButton *button) |
127 | { |
128 | Q_Q(QQuickFolderDialogImpl); |
129 | if (buttonRole(button) == QPlatformDialogHelper::AcceptRole && selectedFolder.isValid()) { |
130 | q->setSelectedFolder(selectedFolder); |
131 | q->accept(); |
132 | } |
133 | } |
134 | |
135 | /*! |
136 | \class QQuickFolderDialogImpl |
137 | \internal |
138 | |
139 | An interface that QQuickFolderDialog can use to access the non-native Qt Quick FolderDialog. |
140 | |
141 | Both this and the native implementations are created in QQuickAbstractDialog::create(). |
142 | */ |
143 | |
144 | QQuickFolderDialogImpl::QQuickFolderDialogImpl(QObject *parent) |
145 | : QQuickDialog(*(new QQuickFolderDialogImplPrivate), parent) |
146 | { |
147 | } |
148 | |
149 | QQuickFolderDialogImplAttached *QQuickFolderDialogImpl::qmlAttachedProperties(QObject *object) |
150 | { |
151 | return new QQuickFolderDialogImplAttached(object); |
152 | } |
153 | |
154 | QUrl QQuickFolderDialogImpl::currentFolder() const |
155 | { |
156 | Q_D(const QQuickFolderDialogImpl); |
157 | return d->currentFolder; |
158 | } |
159 | |
160 | void QQuickFolderDialogImpl::setCurrentFolder(const QUrl ¤tFolder) |
161 | { |
162 | qCDebug(lcFolderDialogCurrentFolder) << "setCurrentFolder called with" << currentFolder; |
163 | Q_D(QQuickFolderDialogImpl); |
164 | if (currentFolder == d->currentFolder) |
165 | return; |
166 | |
167 | const QString oldFolderPath = QQmlFile::urlToLocalFileOrQrc(d->currentFolder); |
168 | |
169 | d->currentFolder = currentFolder; |
170 | d->updateSelectedFolder(oldFolderPath); |
171 | emit currentFolderChanged(folderUrl: d->currentFolder); |
172 | } |
173 | |
174 | QUrl QQuickFolderDialogImpl::selectedFolder() const |
175 | { |
176 | Q_D(const QQuickFolderDialogImpl); |
177 | return d->selectedFolder; |
178 | } |
179 | |
180 | void QQuickFolderDialogImpl::setSelectedFolder(const QUrl &selectedFolder) |
181 | { |
182 | Q_D(QQuickFolderDialogImpl); |
183 | qCDebug(lcFolderDialogSelectedFolder).nospace() << "setSelectedFolder called with selectedFolder " |
184 | << selectedFolder << " (d->selectedFolder is " << d->selectedFolder << ")" ; |
185 | if (selectedFolder == d->selectedFolder) |
186 | return; |
187 | |
188 | d->selectedFolder = selectedFolder; |
189 | d->updateEnabled(); |
190 | emit selectedFolderChanged(folderUrl: selectedFolder); |
191 | } |
192 | |
193 | QSharedPointer<QFileDialogOptions> QQuickFolderDialogImpl::options() const |
194 | { |
195 | Q_D(const QQuickFolderDialogImpl); |
196 | return d->options; |
197 | } |
198 | |
199 | void QQuickFolderDialogImpl::setOptions(const QSharedPointer<QFileDialogOptions> &options) |
200 | { |
201 | qCDebug(lcFolderDialogOptions).nospace() << "setOptions called with:" |
202 | << " acceptMode=" << options->acceptMode() |
203 | << " fileMode=" << options->fileMode() |
204 | << " initialDirectory=" << options->initialDirectory(); |
205 | |
206 | Q_D(QQuickFolderDialogImpl); |
207 | d->options = options; |
208 | } |
209 | |
210 | /*! |
211 | \internal |
212 | |
213 | These allow QQuickPlatformFileDialog::show() to set custom labels on the |
214 | dialog buttons without having to know about/go through QQuickFolderDialogImplAttached |
215 | and QQuickDialogButtonBox. |
216 | */ |
217 | void QQuickFolderDialogImpl::setAcceptLabel(const QString &label) |
218 | { |
219 | Q_D(QQuickFolderDialogImpl); |
220 | d->acceptLabel = label; |
221 | QQuickFolderDialogImplAttached *attached = d->attachedOrWarn(); |
222 | if (!attached) |
223 | return; |
224 | |
225 | auto acceptButton = d->buttonBox->standardButton(button: QPlatformDialogHelper::Open); |
226 | if (!acceptButton) { |
227 | qmlWarning(me: this).nospace() << "Can't set accept label to " << label |
228 | << "; failed to find Open button in DialogButtonBox of " << this; |
229 | return; |
230 | } |
231 | |
232 | acceptButton->setText(!label.isEmpty() |
233 | ? label : QQuickDialogButtonBoxPrivate::buttonText(standardButton: QPlatformDialogHelper::Open)); |
234 | } |
235 | |
236 | void QQuickFolderDialogImpl::setRejectLabel(const QString &label) |
237 | { |
238 | Q_D(QQuickFolderDialogImpl); |
239 | d->rejectLabel = label; |
240 | if (!d->buttonBox) |
241 | return; |
242 | |
243 | auto rejectButton = d->buttonBox->standardButton(button: QPlatformDialogHelper::Cancel); |
244 | if (!rejectButton) { |
245 | qmlWarning(me: this).nospace() << "Can't set reject label to " << label |
246 | << "; failed to find Open button in DialogButtonBox of " << this; |
247 | return; |
248 | } |
249 | |
250 | rejectButton->setText(!label.isEmpty() |
251 | ? label : QQuickDialogButtonBoxPrivate::buttonText(standardButton: QPlatformDialogHelper::Cancel)); |
252 | } |
253 | |
254 | void QQuickFolderDialogImpl::componentComplete() |
255 | { |
256 | Q_D(QQuickFolderDialogImpl); |
257 | QQuickDialog::componentComplete(); |
258 | |
259 | // Find the right-most button and set its key navigation so that |
260 | // tab moves focus to the breadcrumb bar's up button. I tried |
261 | // doing this via KeyNavigation on the DialogButtonBox in QML, |
262 | // but it didn't work (probably because it's not the right item). |
263 | QQuickFolderDialogImplAttached *attached = d->attachedOrWarn(); |
264 | if (!attached) |
265 | return; |
266 | |
267 | Q_ASSERT(d->buttonBox); |
268 | const int buttonCount = d->buttonBox->count(); |
269 | if (buttonCount == 0) |
270 | return; |
271 | |
272 | QQuickAbstractButton *rightMostButton = qobject_cast<QQuickAbstractButton *>( |
273 | object: d->buttonBox->itemAt(index: buttonCount - 1)); |
274 | if (!rightMostButton) { |
275 | qmlWarning(me: this) << "Can't find right-most button in DialogButtonBox" ; |
276 | return; |
277 | } |
278 | |
279 | auto keyNavigationAttached = QQuickKeyNavigationAttached::qmlAttachedProperties(rightMostButton); |
280 | if (!keyNavigationAttached) { |
281 | qmlWarning(me: this) << "Can't create attached KeyNavigation object on" << QDebug::toString(object&: rightMostButton); |
282 | return; |
283 | } |
284 | |
285 | keyNavigationAttached->setTab(attached->breadcrumbBar()->upButton()); |
286 | } |
287 | |
288 | void QQuickFolderDialogImpl::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &data) |
289 | { |
290 | Q_D(QQuickFolderDialogImpl); |
291 | QQuickDialog::itemChange(change, data); |
292 | |
293 | if (change != QQuickItem::ItemVisibleHasChanged || !isComponentComplete() || !data.boolValue) |
294 | return; |
295 | |
296 | QQuickFolderDialogImplAttached *attached = d->attachedOrWarn(); |
297 | if (!attached) |
298 | return; |
299 | |
300 | attached->folderDialogListView()->forceActiveFocus(); |
301 | d->updateEnabled(); |
302 | } |
303 | |
304 | QQuickFolderDialogImplAttached *QQuickFolderDialogImplPrivate::attachedOrWarn() |
305 | { |
306 | Q_Q(QQuickFolderDialogImpl); |
307 | QQuickFolderDialogImplAttached *attached = static_cast<QQuickFolderDialogImplAttached*>( |
308 | qmlAttachedPropertiesObject<QQuickFolderDialogImpl>(obj: q)); |
309 | if (!attached) |
310 | qmlWarning(me: q) << "Expected FileDialogImpl attached object to be present on" << this; |
311 | return attached; |
312 | } |
313 | |
314 | void QQuickFolderDialogImplAttachedPrivate::folderDialogListViewCurrentIndexChanged() |
315 | { |
316 | auto folderDialogImpl = qobject_cast<QQuickFolderDialogImpl*>(object: parent); |
317 | if (!folderDialogImpl) |
318 | return; |
319 | |
320 | auto folderDialogDelegate = qobject_cast<QQuickFileDialogDelegate*>(object: folderDialogListView->currentItem()); |
321 | if (!folderDialogDelegate) |
322 | return; |
323 | |
324 | folderDialogImpl->setSelectedFolder(folderDialogDelegate->file()); |
325 | } |
326 | |
327 | QQuickFolderDialogImplAttached::QQuickFolderDialogImplAttached(QObject *parent) |
328 | : QObject(*(new QQuickFolderDialogImplAttachedPrivate), parent) |
329 | { |
330 | if (!qobject_cast<QQuickFolderDialogImpl*>(object: parent)) { |
331 | qmlWarning(me: this) << "FolderDialogImpl attached properties should only be " |
332 | << "accessed through the root FileDialogImpl instance" ; |
333 | } |
334 | } |
335 | |
336 | QQuickListView *QQuickFolderDialogImplAttached::folderDialogListView() const |
337 | { |
338 | Q_D(const QQuickFolderDialogImplAttached); |
339 | return d->folderDialogListView; |
340 | } |
341 | |
342 | void QQuickFolderDialogImplAttached::setFolderDialogListView(QQuickListView *folderDialogListView) |
343 | { |
344 | Q_D(QQuickFolderDialogImplAttached); |
345 | if (folderDialogListView == d->folderDialogListView) |
346 | return; |
347 | |
348 | d->folderDialogListView = folderDialogListView; |
349 | |
350 | QObjectPrivate::connect(sender: d->folderDialogListView, signal: &QQuickListView::currentIndexChanged, |
351 | receiverPrivate: d, slot: &QQuickFolderDialogImplAttachedPrivate::folderDialogListViewCurrentIndexChanged); |
352 | |
353 | emit folderDialogListViewChanged(); |
354 | } |
355 | |
356 | QQuickFolderBreadcrumbBar *QQuickFolderDialogImplAttached::breadcrumbBar() const |
357 | { |
358 | Q_D(const QQuickFolderDialogImplAttached); |
359 | return d->breadcrumbBar; |
360 | } |
361 | |
362 | void QQuickFolderDialogImplAttached::setBreadcrumbBar(QQuickFolderBreadcrumbBar *breadcrumbBar) |
363 | { |
364 | Q_D(QQuickFolderDialogImplAttached); |
365 | if (breadcrumbBar == d->breadcrumbBar) |
366 | return; |
367 | |
368 | d->breadcrumbBar = breadcrumbBar; |
369 | emit breadcrumbBarChanged(); |
370 | } |
371 | |
372 | QT_END_NAMESPACE |
373 | |
374 | #include "moc_qquickfolderdialogimpl_p.cpp" |
375 | |