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