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 | |
54 | QT_BEGIN_NAMESPACE |
55 | |
56 | void QSideBarDelegate::(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 | */ |
76 | QUrlModel::QUrlModel(QObject *parent) : QStandardItemModel(parent), showFullPath(false), fileSystemModel(nullptr) |
77 | { |
78 | } |
79 | |
80 | /*! |
81 | \reimp |
82 | */ |
83 | QStringList QUrlModel::mimeTypes() const |
84 | { |
85 | return QStringList(QLatin1String("text/uri-list" )); |
86 | } |
87 | |
88 | /*! |
89 | \reimp |
90 | */ |
91 | 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 | */ |
109 | 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 | */ |
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 | */ |
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 | */ |
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 | |
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 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 | |
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 | */ |
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 | */ |
284 | 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 | */ |
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 | */ |
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 | */ |
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 | */ |
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 | |
378 | QSidebar::(QWidget *parent) : QListView(parent) |
379 | { |
380 | } |
381 | |
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 | |
402 | QSidebar::() |
403 | { |
404 | } |
405 | |
406 | #if QT_CONFIG(draganddrop) |
407 | void QSidebar::(QDragEnterEvent *event) |
408 | { |
409 | if (urlModel->canDrop(event)) |
410 | QListView::dragEnterEvent(event); |
411 | } |
412 | #endif // QT_CONFIG(draganddrop) |
413 | |
414 | QSize QSidebar::() const |
415 | { |
416 | if (model()) |
417 | return QListView::sizeHintForIndex(model()->index(0, 0)) + QSize(2 * frameWidth(), 2 * frameWidth()); |
418 | return QListView::sizeHint(); |
419 | } |
420 | |
421 | void QSidebar::(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 | */ |
444 | void QSidebar::(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 | */ |
464 | void QSidebar::() |
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 | */ |
484 | void QSidebar::(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 | */ |
495 | void QSidebar::(QFocusEvent *event) |
496 | { |
497 | QAbstractScrollArea::focusInEvent(event); |
498 | viewport()->update(); |
499 | } |
500 | |
501 | /*! |
502 | \reimp |
503 | */ |
504 | bool QSidebar::(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 | |