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