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
13QT_BEGIN_NAMESPACE
14
15Q_LOGGING_CATEGORY(lcFolderDialogCurrentFolder, "qt.quick.dialogs.quickfolderdialogimpl.currentFolder")
16Q_LOGGING_CATEGORY(lcFolderDialogSelectedFolder, "qt.quick.dialogs.quickfolderdialogimpl.selectedFolder")
17Q_LOGGING_CATEGORY(lcFolderDialogOptions, "qt.quick.dialogs.quickfolderdialogimpl.options")
18
19QQuickFolderDialogImplPrivate::QQuickFolderDialogImplPrivate()
20{
21}
22
23void 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*/
50void 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
121void QQuickFolderDialogImplPrivate::handleAccept()
122{
123 // Let handleClick take care of calling accept().
124}
125
126void 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
144QQuickFolderDialogImpl::QQuickFolderDialogImpl(QObject *parent)
145 : QQuickDialog(*(new QQuickFolderDialogImplPrivate), parent)
146{
147}
148
149QQuickFolderDialogImplAttached *QQuickFolderDialogImpl::qmlAttachedProperties(QObject *object)
150{
151 return new QQuickFolderDialogImplAttached(object);
152}
153
154QUrl QQuickFolderDialogImpl::currentFolder() const
155{
156 Q_D(const QQuickFolderDialogImpl);
157 return d->currentFolder;
158}
159
160void QQuickFolderDialogImpl::setCurrentFolder(const QUrl &currentFolder)
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
174QUrl QQuickFolderDialogImpl::selectedFolder() const
175{
176 Q_D(const QQuickFolderDialogImpl);
177 return d->selectedFolder;
178}
179
180void 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
193QSharedPointer<QFileDialogOptions> QQuickFolderDialogImpl::options() const
194{
195 Q_D(const QQuickFolderDialogImpl);
196 return d->options;
197}
198
199void 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*/
217void 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
236void 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
254void 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
288void 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
304QQuickFolderDialogImplAttached *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
314void 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
327QQuickFolderDialogImplAttached::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
336QQuickListView *QQuickFolderDialogImplAttached::folderDialogListView() const
337{
338 Q_D(const QQuickFolderDialogImplAttached);
339 return d->folderDialogListView;
340}
341
342void 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
356QQuickFolderBreadcrumbBar *QQuickFolderDialogImplAttached::breadcrumbBar() const
357{
358 Q_D(const QQuickFolderDialogImplAttached);
359 return d->breadcrumbBar;
360}
361
362void 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
372QT_END_NAMESPACE
373
374#include "moc_qquickfolderdialogimpl_p.cpp"
375

source code of qtdeclarative/src/quickdialogs/quickdialogsquickimpl/qquickfolderdialogimpl.cpp