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

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