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