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 #include "qfilesystemmodel.h"
42 
43 #include <qaction.h>
44 #include <qurl.h>
45 #if QT_CONFIG(menu)
46 #include <qmenu.h>
47 #endif
48 #include <qmimedata.h>
49 #include <qevent.h>
50 #include <qdebug.h>
51 #include <qfileiconprovider.h>
52 #include <qfiledialog.h>
53 
54 QT_BEGIN_NAMESPACE
55 
initStyleOption(QStyleOptionViewItem * option,const QModelIndex & index) const56 void 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  */
QUrlModel(QObject * parent)76 QUrlModel::QUrlModel(QObject *parent) : QStandardItemModel(parent), showFullPath(false), fileSystemModel(nullptr)
77 {
78 }
79 
80 /*!
81     \reimp
82 */
mimeTypes() const83 QStringList QUrlModel::mimeTypes() const
84 {
85     return QStringList(QLatin1String("text/uri-list"));
86 }
87 
88 /*!
89     \reimp
90 */
flags(const QModelIndex & index) const91 Qt::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 */
mimeData(const QModelIndexList & indexes) const109 QMimeData *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 */
canDrop(QDragEnterEvent * event)128 bool 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 */
dropMimeData(const QMimeData * data,Qt::DropAction action,int row,int column,const QModelIndex & parent)145 bool 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 */
setData(const QModelIndex & index,const QVariant & value,int role)164 bool 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 
setUrl(const QModelIndex & index,const QUrl & url,const QModelIndex & dirIndex)184 void 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 QFileIconProvider *provider = fileSystemModel->iconProvider();
202             if (provider)
203                 newIcon = provider->icon(QFileIconProvider::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 
setUrls(const QList<QUrl> & list)229 void 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 */
addUrls(const QList<QUrl> & list,int row,bool move)243 void 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 */
urls() const284 QList<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 */
setFileSystemModel(QFileSystemModel * model)297 void 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 */
dataChanged(const QModelIndex & topLeft,const QModelIndex & bottomRight)325 void 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  */
layoutChanged()346 void 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 */
changed(const QString & path)368 void 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 
QSidebar(QWidget * parent)378 QSidebar::QSidebar(QWidget *parent) : QListView(parent)
379 {
380 }
381 
setModelAndUrls(QFileSystemModel * model,const QList<QUrl> & newUrls)382 void 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 
~QSidebar()402 QSidebar::~QSidebar()
403 {
404 }
405 
406 #if QT_CONFIG(draganddrop)
dragEnterEvent(QDragEnterEvent * event)407 void QSidebar::dragEnterEvent(QDragEnterEvent *event)
408 {
409     if (urlModel->canDrop(event))
410         QListView::dragEnterEvent(event);
411 }
412 #endif // QT_CONFIG(draganddrop)
413 
sizeHint() const414 QSize 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 
selectUrl(const QUrl & url)421 void 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 */
showContextMenu(const QPoint & position)444 void 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 */
removeEntry()464 void 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 */
clicked(const QModelIndex & index)484 void 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  */
focusInEvent(QFocusEvent * event)495 void QSidebar::focusInEvent(QFocusEvent *event)
496 {
497     QAbstractScrollArea::focusInEvent(event);
498     viewport()->update();
499 }
500 
501 /*!
502     \reimp
503  */
event(QEvent * event)504 bool 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 
516 QT_END_NAMESPACE
517 
518 #include "moc_qsidebar_p.cpp"
519