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

Provided by KDAB

Privacy Policy
Start learning QML with our Intro Training
Find out more

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