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
qsidebar.cpp
Go to the documentation of this file.
1// Copyright (C) 2016 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
5#include "qsidebar_p.h"
6
7#include <qaction.h>
8#include <qurl.h>
9#if QT_CONFIG(menu)
10#include <qmenu.h>
11#endif
12#include <qmimedata.h>
13#include <qevent.h>
14#include <qdebug.h>
15#include <qfilesystemmodel.h>
16#include <qabstractfileiconprovider.h>
17#include <qfiledialog.h>
18
19QT_BEGIN_NAMESPACE
20
21using namespace Qt::StringLiterals;
22
23void QSideBarDelegate::initStyleOption(QStyleOptionViewItem *option,
24 const QModelIndex &index) const
25{
26 QStyledItemDelegate::initStyleOption(option,index);
27 QVariant value = index.data(QUrlModel::EnabledRole);
28 if (value.isValid()) {
29 //If the bookmark/entry is not enabled then we paint it in gray
30 if (!qvariant_cast<bool>(value))
31 option->state &= ~QStyle::State_Enabled;
32 }
33}
34
35/*!
36 \internal
37 \class QUrlModel
38 QUrlModel lets you have indexes from a QFileSystemModel to a list. When QFileSystemModel
39 changes them QUrlModel will automatically update.
40
41 Example usage: File dialog sidebar and combo box
42 */
43QUrlModel::QUrlModel(QObject *parent) : QStandardItemModel(parent), showFullPath(false), fileSystemModel(nullptr)
44{
45}
46
47QUrlModel::~QUrlModel()
48{
49 for (const auto &conn : std::as_const(modelConnections))
50 disconnect(conn);
51}
52
53constexpr char uriListMimeType[] = "text/uri-list";
54
55#if QT_CONFIG(draganddrop)
56static bool hasSupportedFormat(const QMimeData *data)
57{
58 return data->hasFormat(QLatin1StringView(uriListMimeType));
59}
60#endif // QT_CONFIG(draganddrop)
61
62/*!
63 \reimp
64*/
65QStringList QUrlModel::mimeTypes() const
66{
67 return QStringList(QLatin1StringView(uriListMimeType));
68}
69
70/*!
71 \reimp
72*/
73Qt::ItemFlags QUrlModel::flags(const QModelIndex &index) const
74{
75 Qt::ItemFlags flags = QStandardItemModel::flags(index);
76 if (index.isValid()) {
77 flags &= ~Qt::ItemIsEditable;
78 // ### some future version could support "moving" urls onto a folder
79 flags &= ~Qt::ItemIsDropEnabled;
80 }
81
82 if (index.data(Qt::DecorationRole).isNull())
83 flags &= ~Qt::ItemIsEnabled;
84
85 return flags;
86}
87
88/*!
89 \reimp
90*/
91QMimeData *QUrlModel::mimeData(const QModelIndexList &indexes) const
92{
93 QList<QUrl> list;
94 for (const auto &index : indexes) {
95 if (index.column() == 0)
96 list.append(index.data(UrlRole).toUrl());
97 }
98 QMimeData *data = new QMimeData();
99 data->setUrls(list);
100 return data;
101}
102
103#if QT_CONFIG(draganddrop)
104
105/*!
106 Decide based upon the data if it should be accepted or not
107
108 We only accept dirs and not files
109*/
110bool QUrlModel::canDrop(QDragEnterEvent *event)
111{
112 if (!hasSupportedFormat(event->mimeData()))
113 return false;
114
115 const QList<QUrl> list = event->mimeData()->urls();
116 for (const auto &url : list) {
117 const QModelIndex idx = fileSystemModel->index(url.toLocalFile());
118 if (!fileSystemModel->isDir(idx))
119 return false;
120 }
121 return true;
122}
123
124/*!
125 \reimp
126*/
127bool QUrlModel::dropMimeData(const QMimeData *data, Qt::DropAction action,
128 int row, int column, const QModelIndex &parent)
129{
130 if (!hasSupportedFormat(data))
131 return false;
132 Q_UNUSED(action);
133 Q_UNUSED(column);
134 Q_UNUSED(parent);
135 addUrls(data->urls(), row);
136 return true;
137}
138
139#endif // QT_CONFIG(draganddrop)
140
141/*!
142 \reimp
143
144 If the role is the UrlRole then handle otherwise just pass to QStandardItemModel
145*/
146bool QUrlModel::setData(const QModelIndex &index, const QVariant &value, int role)
147{
148 if (value.userType() == QMetaType::QUrl) {
149 QUrl url = value.toUrl();
150 QModelIndex dirIndex = fileSystemModel->index(url.toLocalFile());
151 //On windows the popup display the "C:\", convert to nativeSeparators
152 if (showFullPath)
153 QStandardItemModel::setData(index, QDir::toNativeSeparators(fileSystemModel->data(dirIndex, QFileSystemModel::FilePathRole).toString()));
154 else {
155 QStandardItemModel::setData(index, QDir::toNativeSeparators(fileSystemModel->data(dirIndex, QFileSystemModel::FilePathRole).toString()), Qt::ToolTipRole);
156 QStandardItemModel::setData(index, fileSystemModel->data(dirIndex).toString());
157 }
158 QStandardItemModel::setData(index, fileSystemModel->data(dirIndex, Qt::DecorationRole),
159 Qt::DecorationRole);
160 QStandardItemModel::setData(index, url, UrlRole);
161 return true;
162 }
163 return QStandardItemModel::setData(index, value, role);
164}
165
166void QUrlModel::setUrl(const QModelIndex &index, const QUrl &url, const QModelIndex &dirIndex)
167{
168 setData(index, url, UrlRole);
169 if (url.path().isEmpty()) {
170 setData(index, fileSystemModel->myComputer());
171 setData(index, fileSystemModel->myComputer(Qt::DecorationRole), Qt::DecorationRole);
172 } else {
173 QString newName;
174 if (showFullPath) {
175 //On windows the popup display the "C:\", convert to nativeSeparators
176 newName = QDir::toNativeSeparators(dirIndex.data(QFileSystemModel::FilePathRole).toString());
177 } else {
178 newName = dirIndex.data().toString();
179 }
180
181 QIcon newIcon = qvariant_cast<QIcon>(dirIndex.data(Qt::DecorationRole));
182 if (!dirIndex.isValid()) {
183 const QAbstractFileIconProvider *provider = fileSystemModel->iconProvider();
184 if (provider)
185 newIcon = provider->icon(QAbstractFileIconProvider::Folder);
186 newName = QFileInfo(url.toLocalFile()).fileName();
187 if (!invalidUrls.contains(url))
188 invalidUrls.append(url);
189 //The bookmark is invalid then we set to false the EnabledRole
190 setData(index, false, EnabledRole);
191 } else {
192 //The bookmark is valid then we set to true the EnabledRole
193 setData(index, true, EnabledRole);
194 }
195
196 // newIcon could be null if fileSystemModel->iconProvider() returns null
197 if (!newIcon.isNull()) {
198 // Make sure that we have at least 32x32 images
199 const QSize size = newIcon.actualSize(QSize(32,32));
200 if (size.width() < 32) {
201 const auto widget = qobject_cast<QWidget *>(parent());
202 const auto dpr = widget ? widget->devicePixelRatio() : qApp->devicePixelRatio();
203 const auto smallPixmap = newIcon.pixmap(QSize(32, 32), dpr);
204 const auto newPixmap = smallPixmap.scaledToWidth(32 * dpr, Qt::SmoothTransformation);
205 newIcon.addPixmap(newPixmap);
206 }
207 }
208
209 if (index.data().toString() != newName)
210 setData(index, newName);
211 QIcon oldIcon = qvariant_cast<QIcon>(index.data(Qt::DecorationRole));
212 if (oldIcon.cacheKey() != newIcon.cacheKey())
213 setData(index, newIcon, Qt::DecorationRole);
214 }
215}
216
217void QUrlModel::setUrls(const QList<QUrl> &list)
218{
219 removeRows(0, rowCount());
220 invalidUrls.clear();
221 watching.clear();
222 addUrls(list, 0);
223}
224
225/*!
226 Add urls \a list into the list at \a row. If move then movie
227 existing ones to row.
228
229 \sa dropMimeData()
230*/
231void QUrlModel::addUrls(const QList<QUrl> &list, int row, bool move)
232{
233 if (row == -1)
234 row = rowCount();
235 row = qMin(row, rowCount());
236 const auto rend = list.crend();
237 for (auto it = list.crbegin(); it != rend; ++it) {
238 QUrl url = *it;
239 if (!url.isValid() || url.scheme() != "file"_L1)
240 continue;
241 //this makes sure the url is clean
242 const QString cleanUrl = QDir::cleanPath(url.toLocalFile());
243 if (!cleanUrl.isEmpty())
244 url = QUrl::fromLocalFile(cleanUrl);
245
246 for (int j = 0; move && j < rowCount(); ++j) {
247 QString local = index(j, 0).data(UrlRole).toUrl().toLocalFile();
248#if defined(Q_OS_WIN)
249 const Qt::CaseSensitivity cs = Qt::CaseInsensitive;
250#else
251 const Qt::CaseSensitivity cs = Qt::CaseSensitive;
252#endif
253 if (!cleanUrl.compare(local, cs)) {
254 removeRow(j);
255 if (j <= row)
256 row--;
257 break;
258 }
259 }
260 row = qMax(row, 0);
261 QModelIndex idx = fileSystemModel->index(cleanUrl);
262 if (!fileSystemModel->isDir(idx))
263 continue;
264 insertRows(row, 1);
265 setUrl(index(row, 0), url, idx);
266 watching.append({idx, cleanUrl});
267 }
268}
269
270/*!
271 Return the complete list of urls in a QList.
272*/
273QList<QUrl> QUrlModel::urls() const
274{
275 QList<QUrl> list;
276 const int numRows = rowCount();
277 list.reserve(numRows);
278 for (int i = 0; i < numRows; ++i)
279 list.append(data(index(i, 0), UrlRole).toUrl());
280 return list;
281}
282
283/*!
284 QFileSystemModel to get index's from, clears existing rows
285*/
286void QUrlModel::setFileSystemModel(QFileSystemModel *model)
287{
288 if (model == fileSystemModel)
289 return;
290 if (fileSystemModel != nullptr) {
291 for (const auto &conn : std::as_const(modelConnections))
292 disconnect(conn);
293 }
294 fileSystemModel = model;
295 if (fileSystemModel != nullptr) {
296 modelConnections = {
297 connect(model, &QFileSystemModel::dataChanged,
298 this, &QUrlModel::dataChanged),
299 connect(model, &QFileSystemModel::layoutChanged,
300 this, &QUrlModel::layoutChanged),
301 connect(model, &QFileSystemModel::rowsRemoved,
302 this, &QUrlModel::layoutChanged),
303 };
304 }
305 clear();
306 insertColumns(0, 1);
307}
308
309/*
310 If one of the index's we are watching has changed update our internal data
311*/
312void QUrlModel::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight)
313{
314 QModelIndex parent = topLeft.parent();
315 for (int i = 0; i < watching.size(); ++i) {
316 QModelIndex index = watching.at(i).index;
317 if (index.model() && topLeft.model()) {
318 Q_ASSERT(index.model() == topLeft.model());
319 }
320 if ( index.row() >= topLeft.row()
321 && index.row() <= bottomRight.row()
322 && index.column() >= topLeft.column()
323 && index.column() <= bottomRight.column()
324 && index.parent() == parent) {
325 changed(watching.at(i).path);
326 }
327 }
328}
329
330/*!
331 Re-get all of our data, anything could have changed!
332 */
333void QUrlModel::layoutChanged()
334{
335 QStringList paths;
336 paths.reserve(watching.size());
337 for (const WatchItem &item : std::as_const(watching))
338 paths.append(item.path);
339 watching.clear();
340 for (const auto &path : paths) {
341 QModelIndex newIndex = fileSystemModel->index(path);
342 watching.append({newIndex, path});
343 if (newIndex.isValid())
344 changed(path);
345 }
346}
347
348/*!
349 The following path changed data update our copy of that data
350
351 \sa layoutChanged(), dataChanged()
352*/
353void QUrlModel::changed(const QString &path)
354{
355 for (int i = 0; i < rowCount(); ++i) {
356 QModelIndex idx = index(i, 0);
357 if (idx.data(UrlRole).toUrl().toLocalFile() == path) {
358 setData(idx, idx.data(UrlRole).toUrl());
359 }
360 }
361}
362
363QSidebar::QSidebar(QWidget *parent) : QListView(parent)
364{
365}
366
367void QSidebar::setModelAndUrls(QFileSystemModel *model, const QList<QUrl> &newUrls)
368{
369 setUniformItemSizes(true);
370 urlModel = new QUrlModel(this);
371 urlModel->setFileSystemModel(model);
372 setModel(urlModel);
373 setItemDelegate(new QSideBarDelegate(this));
374
375 connect(selectionModel(), &QItemSelectionModel::currentChanged,
376 this, &QSidebar::clicked);
377#if QT_CONFIG(draganddrop)
378 setDragDropMode(QAbstractItemView::DragDrop);
379#endif
380#if QT_CONFIG(menu)
381 setContextMenuPolicy(Qt::CustomContextMenu);
382 connect(this, &QSidebar::customContextMenuRequested,
383 this, &QSidebar::showContextMenu);
384#endif
385 urlModel->setUrls(newUrls);
386 setCurrentIndex(this->model()->index(0,0));
387}
388
389QSidebar::~QSidebar()
390{
391}
392
393#if QT_CONFIG(draganddrop)
394void QSidebar::dragEnterEvent(QDragEnterEvent *event)
395{
396 if (urlModel->canDrop(event))
397 QListView::dragEnterEvent(event);
398}
399#endif // QT_CONFIG(draganddrop)
400
401QSize QSidebar::sizeHint() const
402{
403 if (model())
404 return QListView::sizeHintForIndex(model()->index(0, 0)) + QSize(2 * frameWidth(), 2 * frameWidth());
405 return QListView::sizeHint();
406}
407
408void QSidebar::selectUrl(const QUrl &url)
409{
410 disconnect(selectionModel(), &QItemSelectionModel::currentChanged,
411 this, &QSidebar::clicked);
412
413 selectionModel()->clear();
414 for (int i = 0; i < model()->rowCount(); ++i) {
415 if (model()->index(i, 0).data(QUrlModel::UrlRole).toUrl() == url) {
416 emit goToUrl(url);
417 selectionModel()->setCurrentIndex(model()->index(i, 0),
418 QItemSelectionModel::SelectCurrent);
419 break;
420 }
421 }
422
423 connect(selectionModel(), &QItemSelectionModel::currentChanged,
424 this, &QSidebar::clicked);
425}
426
427#if QT_CONFIG(menu)
428/*!
429 \internal
430
431 \sa removeEntry()
432*/
433void QSidebar::showContextMenu(const QPoint &position)
434{
435 QList<QAction *> actions;
436 if (indexAt(position).isValid()) {
437 QAction *action = new QAction(QFileDialog::tr("Remove"), this);
438 if (indexAt(position).data(QUrlModel::UrlRole).toUrl().path().isEmpty())
439 action->setEnabled(false);
440 connect(action, &QAction::triggered, this, &QSidebar::removeEntry);
441 actions.append(action);
442 }
443 if (actions.size() > 0)
444 QMenu::exec(actions, mapToGlobal(position));
445}
446#endif // QT_CONFIG(menu)
447
448/*!
449 \internal
450
451 \sa showContextMenu()
452*/
453void QSidebar::removeEntry()
454{
455 const QList<QModelIndex> idxs = selectionModel()->selectedIndexes();
456 // Create a list of QPersistentModelIndex as the removeRow() calls below could
457 // invalidate the indexes in "idxs"
458 const QList<QPersistentModelIndex> persIndexes(idxs.cbegin(), idxs.cend());
459 for (const QPersistentModelIndex &persistent : persIndexes) {
460 if (!persistent.data(QUrlModel::UrlRole).toUrl().path().isEmpty())
461 model()->removeRow(persistent.row());
462 }
463}
464
465/*!
466 \internal
467
468 \sa goToUrl()
469*/
470void QSidebar::clicked(const QModelIndex &index)
471{
472 QUrl url = model()->index(index.row(), 0).data(QUrlModel::UrlRole).toUrl();
473 selectUrl(url);
474}
475
476/*!
477 \reimp
478 Don't automatically select something
479 */
480void QSidebar::focusInEvent(QFocusEvent *event)
481{
482 QAbstractScrollArea::focusInEvent(event);
483 viewport()->update();
484}
485
486/*!
487 \reimp
488 */
489bool QSidebar::event(QEvent * event)
490{
491 if (event->type() == QEvent::KeyRelease) {
492 QKeyEvent *ke = static_cast<QKeyEvent *>(event);
493 if (ke->key() == Qt::Key_Delete) {
494 removeEntry();
495 return true;
496 }
497 }
498 return QListView::event(event);
499}
500
501QT_END_NAMESPACE
502
503#include "moc_qsidebar_p.cpp"
\inmodule QtCore
constexpr char uriListMimeType[]
Definition qsidebar.cpp:53