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

Provided by KDAB

Privacy Policy
Learn Advanced QML with KDAB
Find out more

source code of qtbase/src/widgets/dialogs/qsidebar.cpp