1// Copyright (C) 2024 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 "qquicksidebar_p.h"
5#include "qquicksidebar_p_p.h"
6#include "qquickfiledialogimpl_p_p.h"
7#include <QtQml/qqmllist.h>
8#if QT_CONFIG(settings)
9#include <QtCore/qsettings.h>
10#endif
11
12#include <QtQuickTemplates2/private/qquickaction_p.h>
13#include <QtQuickTemplates2/private/qquickcontextmenu_p.h>
14
15/*!
16 \internal
17
18 Private class for the sidebar in a file dialog.
19
20 Given a FileDialog, SideBar creates a ListView that appears on the left hand side of the
21 of the FileDialog's content item. The ListView has two halves. The first half contains
22 standard paths and the second half contains favorites. Favorites can be added by dragging
23 and dropping a directory from the main FileDialog ListView into the SideBar. Favorites are
24 removed by right clicking and selecting 'Remove' from the context menu.
25*/
26
27using namespace Qt::Literals::StringLiterals;
28
29static std::initializer_list<QStandardPaths::StandardLocation> s_defaultPaths = {
30 QStandardPaths::HomeLocation, QStandardPaths::DesktopLocation,
31 QStandardPaths::DownloadLocation, QStandardPaths::DocumentsLocation,
32 QStandardPaths::MusicLocation, QStandardPaths::PicturesLocation,
33 QStandardPaths::MoviesLocation,
34};
35
36QQuickSideBar::QQuickSideBar(QQuickItem *parent)
37 : QQuickContainer(*(new QQuickSideBarPrivate), parent)
38{
39 Q_D(QQuickSideBar);
40 d->folderPaths = s_defaultPaths;
41
42 QObject::connect(sender: this, signal: &QQuickContainer::currentIndexChanged, slot: [d](){
43 d->currentButtonClickedUrl.clear();
44 });
45
46 // read in the favorites
47#if QT_CONFIG(settings)
48 d->readSettings();
49#endif
50}
51
52QQuickSideBar::~QQuickSideBar()
53{
54 Q_D(QQuickSideBar);
55
56#if QT_CONFIG(settings)
57 d->writeSettings();
58#endif
59}
60
61QQuickDialog *QQuickSideBar::dialog() const
62{
63 Q_D(const QQuickSideBar);
64 return d->dialog;
65}
66
67void QQuickSideBar::setDialog(QQuickDialog *dialog)
68{
69 Q_D(QQuickSideBar);
70 if (dialog == d->dialog)
71 return;
72
73 if (auto fileDialog = qobject_cast<QQuickFileDialogImpl *>(object: d->dialog))
74 QObjectPrivate::disconnect(sender: fileDialog, signal: &QQuickFileDialogImpl::currentFolderChanged, receiverPrivate: d,
75 slot: &QQuickSideBarPrivate::folderChanged);
76
77 d->dialog = dialog;
78
79 if (auto fileDialog = qobject_cast<QQuickFileDialogImpl *>(object: d->dialog))
80 QObjectPrivate::connect(sender: fileDialog, signal: &QQuickFileDialogImpl::currentFolderChanged, receiverPrivate: d,
81 slot: &QQuickSideBarPrivate::folderChanged);
82
83 emit dialogChanged();
84}
85
86QList<QStandardPaths::StandardLocation> QQuickSideBar::folderPaths() const
87{
88 Q_D(const QQuickSideBar);
89 return d->folderPaths;
90}
91
92void QQuickSideBar::setFolderPaths(const QList<QStandardPaths::StandardLocation> &folderPaths)
93{
94 Q_D(QQuickSideBar);
95 if (folderPaths == d->folderPaths)
96 return;
97
98 const auto oldEffective = effectiveFolderPaths();
99
100 d->folderPaths = folderPaths;
101 emit folderPathsChanged();
102
103 if (oldEffective != effectiveFolderPaths())
104 emit effectiveFolderPathsChanged();
105
106 d->repopulate();
107}
108
109QList<QStandardPaths::StandardLocation> QQuickSideBar::effectiveFolderPaths() const
110{
111 QList<QStandardPaths::StandardLocation> effectivePaths;
112
113 // The home location is never returned as empty
114 const QString homeLocation = QStandardPaths::writableLocation(type: QStandardPaths::HomeLocation);
115 bool homeFound = false;
116 for (auto &path : folderPaths()) {
117 if (!homeFound && path == QStandardPaths::HomeLocation) {
118 effectivePaths.append(t: path);
119 homeFound = true;
120 } else if (QStandardPaths::writableLocation(type: path) != homeLocation) {
121 // if a standard path is not found, it will be resolved to home location
122 effectivePaths.append(t: path);
123 }
124 }
125
126 return effectivePaths;
127}
128
129QList<QUrl> QQuickSideBar::favoritePaths() const
130{
131 Q_D(const QQuickSideBar);
132 return d->favoritePaths;
133}
134
135void QQuickSideBar::setFavoritePaths(const QList<QUrl> &favoritePaths)
136{
137 Q_D(QQuickSideBar);
138 if (favoritePaths == d->favoritePaths)
139 return;
140
141 d->favoritePaths = favoritePaths;
142 emit favoritePathsChanged();
143
144#if QT_CONFIG(settings)
145 d->writeSettings();
146#endif
147 d->repopulate();
148}
149
150QQmlComponent *QQuickSideBar::buttonDelegate() const
151{
152 Q_D(const QQuickSideBar);
153 return d->buttonDelegate;
154}
155
156void QQuickSideBar::setButtonDelegate(QQmlComponent *delegate)
157{
158 Q_D(QQuickSideBar);
159 if (d->componentComplete || delegate == d->buttonDelegate)
160 return;
161
162 d->buttonDelegate = delegate;
163 emit buttonDelegateChanged();
164}
165
166QQmlComponent *QQuickSideBar::separatorDelegate() const
167{
168 Q_D(const QQuickSideBar);
169 return d->separatorDelegate;
170}
171
172void QQuickSideBar::setSeparatorDelegate(QQmlComponent *delegate)
173{
174 Q_D(QQuickSideBar);
175 if (d->componentComplete || delegate == d->separatorDelegate)
176 return;
177
178 d->separatorDelegate = delegate;
179 emit separatorDelegateChanged();
180}
181
182QQmlComponent *QQuickSideBar::addFavoriteDelegate() const
183{
184 Q_D(const QQuickSideBar);
185 return d->addFavoriteDelegate;
186}
187
188void QQuickSideBar::setAddFavoriteDelegate(QQmlComponent *delegate)
189{
190 Q_D(QQuickSideBar);
191 if (d->componentComplete || delegate == d->addFavoriteDelegate)
192 return;
193
194 d->addFavoriteDelegate = delegate;
195 emit addFavoriteDelegateChanged();
196
197 if (d->showAddFavoriteDelegate())
198 d->repopulate();
199}
200
201QQuickItem *QQuickSideBarPrivate::createDelegateItem(QQmlComponent *component,
202 const QVariantMap &initialProperties)
203{
204 Q_Q(QQuickSideBar);
205 // If we don't use the correct context, it won't be possible to refer to
206 // the control's id from within the delegates.
207 QQmlContext *context = component->creationContext();
208 // The component might not have been created in QML, in which case
209 // the creation context will be null and we have to create it ourselves.
210 if (!context)
211 context = qmlContext(q);
212
213 // If we have initial properties we assume that all necessary information is passed via
214 // initial properties.
215 if (!component->isBound() && initialProperties.isEmpty()) {
216 context = new QQmlContext(context, q);
217 context->setContextObject(q);
218 }
219
220 QQuickItem *item = qobject_cast<QQuickItem *>(
221 o: component->createWithInitialProperties(initialProperties, context));
222 if (item)
223 QQml_setParent_noEvent(object: item, parent: q);
224 return item;
225}
226
227void QQuickSideBarPrivate::repopulate()
228{
229 Q_Q(QQuickSideBar);
230
231 if (repopulating || !buttonDelegate || !separatorDelegate || !addFavoriteDelegate || !q->contentItem())
232 return;
233
234 QScopedValueRollback repopulateGuard(repopulating, true);
235
236 auto updateIconSourceAndSize = [this](QQuickAbstractButton *button, const QUrl &iconUrl) {
237 // we need to preserve the default binding on icon.color, so
238 // we just take the default-created icon, and update its source
239 // and size
240 QQuickIcon icon = button->icon();
241 icon.setSource(iconUrl);
242 const QSize iconSize = dialogIconSize();
243 icon.setWidth(iconSize.width());
244 icon.setHeight(iconSize.height());
245 button->setIcon(icon);
246 };
247
248 auto createButtonDelegate = [this, q, &updateIconSourceAndSize](int index, const QString &folderPath, const QUrl &iconUrl) {
249 const QString displayName = displayNameFromFolderPath(filePath: folderPath);
250 QVariantMap initialProperties = {
251 { "index"_L1, QVariant::fromValue(value: index) },
252 { "folderName"_L1, QVariant::fromValue(value: displayName) },
253 };
254
255 if (QQuickItem *buttonItem = createDelegateItem(component: buttonDelegate, initialProperties)) {
256 if (QQuickAbstractButton *button = qobject_cast<QQuickAbstractButton *>(object: buttonItem)) {
257 QObjectPrivate::connect(sender: button, signal: &QQuickAbstractButton::clicked, receiverPrivate: this,
258 slot: &QQuickSideBarPrivate::buttonClicked);
259 updateIconSourceAndSize(button, iconUrl);
260 }
261 insertItem(index: q->count(), item: buttonItem);
262 }
263 };
264
265 // clean up previous state
266 while (q->count() > 0)
267 q->removeItem(item: q->itemAt(index: 0));
268
269 // repopulate
270 const auto folders = q->effectiveFolderPaths();
271 const auto favorites = q->favoritePaths();
272 showSeparator = !folders.isEmpty() && (!favorites.isEmpty() || showAddFavoriteDelegate());
273 int insertIndex = 0;
274
275 for (auto &folder : folders)
276 createButtonDelegate(insertIndex++, QStandardPaths::displayName(type: folder), folderIconSource(stdLocation: folder));
277
278
279 if (QQuickItem *separatorItem = createDelegateItem(component: separatorDelegate, initialProperties: {{"visible"_L1, false}})) {
280 separatorImplicitSize = separatorItem->implicitHeight();
281 if (showSeparator) {
282 separatorItem->setVisible(true);
283 insertItem(index: insertIndex++, item: separatorItem);
284 } else {
285 separatorItem->deleteLater();
286 }
287 }
288
289 // The variant needs to be QString, not a QLatin1StringView
290 const QString labelText = QCoreApplication::translate(context: "FileDialog", key: "Add Favorite");
291 const QVariantMap initialProperties = {
292 { "labelText"_L1, QVariant::fromValue(value: labelText) },
293 { "dragHovering"_L1, QVariant::fromValue(value: addFavoriteDelegateHovered()) },
294 { "visible"_L1, false}
295 };
296 if (auto *addFavoriteDelegateItem = createDelegateItem(component: addFavoriteDelegate, initialProperties)) {
297 addFavoriteButtonImplicitSize = addFavoriteDelegateItem->implicitHeight();
298 if (showAddFavoriteDelegate()) {
299 addFavoriteDelegateItem->setVisible(true);
300 if (QQuickAbstractButton *button = qobject_cast<QQuickAbstractButton *>(object: addFavoriteDelegateItem))
301 updateIconSourceAndSize(button, addFavoriteIconUrl());
302 insertItem(index: insertIndex++, item: addFavoriteDelegateItem);
303 } else {
304 addFavoriteDelegateItem->deleteLater();
305 }
306 }
307
308 // calculate the starting index for the favorites
309 for (auto &favorite : favorites)
310 createButtonDelegate(insertIndex++, favorite.toLocalFile(), folderIconSource());
311
312 q->setCurrentIndex(-1);
313}
314
315void QQuickSideBarPrivate::buttonClicked()
316{
317 Q_Q(QQuickSideBar);
318 if (QQuickAbstractButton *button = qobject_cast<QQuickAbstractButton *>(object: q->sender())) {
319 const int buttonIndex = contentModel->indexOf(object: button, objectContext: nullptr);
320 q->setCurrentIndex(buttonIndex);
321
322 currentButtonClickedUrl = QUrl();
323 // calculate the starting index for the favorites
324 const int offset = q->effectiveFolderPaths().size() + (showSeparator ? 1 : 0);
325 if (buttonIndex >= offset)
326 currentButtonClickedUrl = q->favoritePaths().at(i: buttonIndex - offset);
327 else
328 currentButtonClickedUrl = QUrl::fromLocalFile(
329 localfile: QStandardPaths::writableLocation(type: q->effectiveFolderPaths().at(i: buttonIndex)));
330
331 currentButtonClickedUrl.setScheme("file"_L1);
332 setDialogFolder(currentButtonClickedUrl);
333 }
334}
335
336void QQuickSideBarPrivate::folderChanged()
337{
338 Q_Q(QQuickSideBar);
339
340 if (dialog->property(name: "currentFolder").toUrl() != currentButtonClickedUrl)
341 q->setCurrentIndex(-1);
342}
343
344QString QQuickSideBarPrivate::displayNameFromFolderPath(const QString &folderPath)
345{
346 return folderPath.section(asep: QLatin1Char('/'), astart: -1);
347}
348
349QUrl QQuickSideBarPrivate::dialogFolder() const
350{
351 return dialog->property(name: "currentFolder").toUrl();
352}
353
354void QQuickSideBarPrivate::setDialogFolder(const QUrl &folder)
355{
356 Q_Q(QQuickSideBar);
357 if (!dialog->setProperty(name: "currentFolder", value: folder))
358 qmlWarning(me: q) << "Failed to set currentFolder property of dialog" << dialog->objectName()
359 << "to" << folder;
360}
361
362void QQuickSideBar::componentComplete()
363{
364 Q_D(QQuickSideBar);
365 QQuickContainer::componentComplete();
366 d->repopulate();
367 d->initContextMenu();
368}
369
370QUrl QQuickSideBarPrivate::folderIconSource() const
371{
372 return QUrl("qrc:/qt-project.org/imports/QtQuick/Dialogs/quickimpl/images/sidebar-folder.png"_L1);
373}
374
375QUrl QQuickSideBarPrivate::folderIconSource(QStandardPaths::StandardLocation stdLocation) const
376{
377 switch (stdLocation) {
378 case QStandardPaths::DesktopLocation:
379 return QUrl("qrc:/qt-project.org/imports/QtQuick/Dialogs/quickimpl/images/sidebar-desktop.png"_L1);
380 case QStandardPaths::DocumentsLocation:
381 return QUrl("qrc:/qt-project.org/imports/QtQuick/Dialogs/quickimpl/images/sidebar-documents.png"_L1);
382 case QStandardPaths::MusicLocation:
383 return QUrl("qrc:/qt-project.org/imports/QtQuick/Dialogs/quickimpl/images/sidebar-music.png"_L1);
384 case QStandardPaths::MoviesLocation:
385 return QUrl("qrc:/qt-project.org/imports/QtQuick/Dialogs/quickimpl/images/sidebar-video.png"_L1);
386 case QStandardPaths::PicturesLocation:
387 return QUrl("qrc:/qt-project.org/imports/QtQuick/Dialogs/quickimpl/images/sidebar-photo.png"_L1);
388 case QStandardPaths::HomeLocation:
389 return QUrl("qrc:/qt-project.org/imports/QtQuick/Dialogs/quickimpl/images/sidebar-home.png"_L1);
390 case QStandardPaths::DownloadLocation:
391 return QUrl("qrc:/qt-project.org/imports/QtQuick/Dialogs/quickimpl/images/sidebar-downloads.png"_L1);
392 default:
393 return QUrl("qrc:/qt-project.org/imports/QtQuick/Dialogs/quickimpl/images/sidebar-folder.png"_L1);
394 }
395}
396
397QSize QQuickSideBarPrivate::dialogIconSize() const
398{
399 return QSize(16, 16);
400}
401
402#if QT_CONFIG(settings)
403void QQuickSideBarPrivate::writeSettings() const
404{
405 QSettings settings("QtProject"_L1, "qquickfiledialog"_L1);
406 settings.beginWriteArray(prefix: "favorites");
407
408 for (int i = 0; i < favoritePaths.size(); ++i) {
409 settings.setArrayIndex(i);
410 settings.setValue(key: "favorite", value: favoritePaths.at(i));
411 }
412 settings.endArray();
413}
414
415void QQuickSideBarPrivate::readSettings()
416{
417 favoritePaths.clear();
418 QSettings settings("QtProject"_L1, "qquickfiledialog"_L1);
419 const int size = settings.beginReadArray(prefix: "favorites");
420
421 QList<QUrl> newPaths;
422
423 for (int i = 0; i < size; ++i) {
424 settings.setArrayIndex(i);
425 const QUrl favorite = settings.value(key: "favorite").toUrl();
426 const QFileInfo info(favorite.toLocalFile());
427
428 if (info.isDir())
429 // check it is not a duplicate
430 if (!newPaths.contains(t: favorite))
431 newPaths.append(t: favorite);
432 }
433 settings.endArray();
434
435 favoritePaths = newPaths;
436}
437#endif
438
439void QQuickSideBarPrivate::addFavorite(const QUrl &favorite)
440{
441 Q_Q(QQuickSideBar);
442 QList<QUrl> newPaths = q->favoritePaths();
443 const QFileInfo info(favorite.toLocalFile());
444 if (info.isDir()) {
445 // check it is not a duplicate
446 if (!newPaths.contains(t: favorite)) {
447 newPaths.prepend(t: favorite);
448 q->setFavoritePaths(newPaths);
449 }
450 }
451}
452
453void QQuickSideBarPrivate::removeFavorite(const QUrl &favorite)
454{
455 Q_Q(QQuickSideBar);
456 QList<QUrl> paths = q->favoritePaths();
457 bool success = paths.removeOne(t: favorite);
458 if (success)
459 q->setFavoritePaths(paths);
460 else
461 qmlWarning(me: q) << "Failed to remove favorite path" << favorite;
462}
463
464bool QQuickSideBarPrivate::showAddFavoriteDelegate() const
465{
466 return addFavoriteDelegateVisible;
467}
468
469void QQuickSideBarPrivate::setShowAddFavoriteDelegate(bool show)
470{
471 if (show == addFavoriteDelegateVisible)
472 return;
473
474 addFavoriteDelegateVisible = show;
475 repopulate();
476}
477
478bool QQuickSideBarPrivate::addFavoriteDelegateHovered() const
479{
480 return addFavoriteHovered;
481}
482
483void QQuickSideBarPrivate::setAddFavoriteDelegateHovered(bool hovered)
484{
485 if (hovered == addFavoriteHovered)
486 return;
487
488 addFavoriteHovered = hovered;
489 repopulate();
490}
491
492QUrl QQuickSideBarPrivate::addFavoriteIconUrl() const
493{
494 return QUrl("qrc:/qt-project.org/imports/QtQuick/Dialogs/quickimpl/images/sidebar-plus.png"_L1);
495}
496
497void QQuickSideBarPrivate::initContextMenu()
498{
499 Q_Q(QQuickSideBar);
500 contextMenu = new QQuickContextMenu(q);
501 connect(sender: contextMenu, signal: &QQuickContextMenu::requested, receiverPrivate: this, slot: &QQuickSideBarPrivate::handleContextMenuRequested);
502}
503
504void QQuickSideBarPrivate::handleContextMenuRequested(QPointF pos)
505{
506 Q_Q(QQuickSideBar);
507 const int offset = q->effectiveFolderPaths().size() + (showSeparator ? 1 : 0);
508 for (int i = offset; i < q->count(); ++i) {
509 QQuickItem *itm = q->itemAt(index: i);
510 if (itm->contains(point: itm->mapFromItem(item: q, point: pos))) {
511 auto favorites = q->favoritePaths();
512 urlToBeRemoved = favorites.value(i: i - offset);
513
514 if (!urlToBeRemoved.isEmpty() && !menu) {
515 QQmlEngine *eng = qmlEngine(q);
516 Q_ASSERT(eng);
517 QQmlContext *context = qmlContext(q);
518 QQmlComponent component(eng);
519 component.loadFromModule(uri: "QtQuick.Controls", typeName: "Menu");
520 menu = qobject_cast<QQuickMenu*>(object: component.create(context));
521 if (menu) {
522 auto *removeAction = new QQuickAction(menu);
523 removeAction->setText(QCoreApplication::translate(context: "FileDialog", key: "Remove"));
524 menu->addAction(action: removeAction);
525 connect(sender: removeAction, signal: &QQuickAction::triggered, receiverPrivate: this, slot: &QQuickSideBarPrivate::handleRemoveAction);
526 }
527 }
528 contextMenu->setMenu(menu);
529 return;
530 }
531 }
532 contextMenu->setMenu(nullptr); // prevent the Context menu from popping up otherwise
533}
534
535void QQuickSideBarPrivate::handleRemoveAction()
536{
537 if (!urlToBeRemoved.isEmpty())
538 removeFavorite(favorite: urlToBeRemoved);
539 urlToBeRemoved.clear();
540}
541
542qreal QQuickSideBarPrivate::getContentWidth() const
543{
544 Q_Q(const QQuickSideBar);
545 if (!contentModel)
546 return 0;
547
548 const int count = contentModel->count();
549 qreal maxWidth = 0;
550 for (int i = 0; i < count; ++i) {
551 QQuickItem *item = q->itemAt(index: i);
552 if (item)
553 maxWidth = qMax(a: maxWidth, b: item->implicitWidth());
554 }
555 return maxWidth;
556}
557
558qreal QQuickSideBarPrivate::getContentHeight() const
559{
560 Q_Q(const QQuickSideBar);
561 if (!contentModel)
562 return 0;
563 // All StandardPaths buttons + spacing + separator + AddFavoriteButton
564 const int modelCount = contentModel->count();
565 const int folderPathCount = q->effectiveFolderPaths().count();
566 qreal spacing = 0;
567 if (contentItem) {
568 QQuickListView *listView = contentItem->findChild<QQuickListView*>();
569 if (listView)
570 spacing = listView->spacing();
571 }
572 qreal totalHeight = 0;
573 int i = 0;
574 for (; i < qMin(a: modelCount, b: folderPathCount); ++i) {
575 QQuickItem *item = q->itemAt(index: i);
576 if (item) {
577 totalHeight += item->implicitHeight();
578 }
579 }
580 // Add spacing
581 if (i)
582 totalHeight += (i - 1) * spacing;
583
584 if (!qFuzzyIsNull(d: separatorImplicitSize))
585 totalHeight += separatorImplicitSize + spacing;
586 if (!qFuzzyIsNull(d: addFavoriteButtonImplicitSize))
587 totalHeight += addFavoriteButtonImplicitSize + spacing;
588
589 return totalHeight;
590}
591
592void QQuickSideBarPrivate::itemGeometryChanged(QQuickItem *item, QQuickGeometryChange change, const QRectF &diff)
593{
594 QQuickContainerPrivate::itemGeometryChanged(item, change, diff);
595 if (change.sizeChange())
596 updateImplicitContentSize();
597}
598
599void QQuickSideBarPrivate::itemImplicitWidthChanged(QQuickItem *item)
600{
601 QQuickContainerPrivate::itemImplicitWidthChanged(item);
602 if (item != contentItem)
603 updateImplicitContentWidth();
604}
605
606void QQuickSideBarPrivate::itemImplicitHeightChanged(QQuickItem *item)
607{
608 QQuickContainerPrivate::itemImplicitHeightChanged(item);
609 if (item != contentItem)
610 updateImplicitContentHeight();
611}
612

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