1 /*
2 * Bittorrent Client using Qt and libtorrent.
3 * Copyright (C) 2006-2012 Christophe Dumez <chris@qbittorrent.org>
4 *
5 * This program is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU General Public License
7 * as published by the Free Software Foundation; either version 2
8 * of the License, or (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 *
19 * In addition, as a special exception, the copyright holders give permission to
20 * link this program with the OpenSSL project's "OpenSSL" library (or with
21 * modified versions of it that use the same license as the "OpenSSL" library),
22 * and distribute the linked executables. You must obey the GNU General Public
23 * License in all respects for all of the code used other than "OpenSSL". If you
24 * modify file(s), you may extend this exception to your version of the file(s),
25 * but you are not obligated to do so. If you do not wish to do so, delete this
26 * exception statement from your version.
27 */
28
29 #include "torrentcontentmodel.h"
30
31 #include <algorithm>
32
33 #include <QFileIconProvider>
34 #include <QFileInfo>
35 #include <QIcon>
36
37 #if defined(Q_OS_WIN)
38 #include <Windows.h>
39 #include <Shellapi.h>
40 #include <QtWin>
41 #else
42 #include <QMimeDatabase>
43 #include <QMimeType>
44 #endif
45
46 #if defined Q_OS_WIN || defined Q_OS_MACOS
47 #define QBT_PIXMAP_CACHE_FOR_FILE_ICONS
48 #include <QPixmapCache>
49 #endif
50
51 #include "base/bittorrent/downloadpriority.h"
52 #include "base/bittorrent/torrentinfo.h"
53 #include "base/global.h"
54 #include "base/utils/fs.h"
55 #include "torrentcontentmodelfile.h"
56 #include "torrentcontentmodelfolder.h"
57 #include "torrentcontentmodelitem.h"
58 #include "uithememanager.h"
59
60 #ifdef Q_OS_MACOS
61 #include "macutilities.h"
62 #endif
63
64 namespace
65 {
66 class UnifiedFileIconProvider : public QFileIconProvider
67 {
68 public:
69 using QFileIconProvider::icon;
70
icon(const QFileInfo & info) const71 QIcon icon(const QFileInfo &info) const override
72 {
73 Q_UNUSED(info);
74 static QIcon cached = UIThemeManager::instance()->getIcon("text-plain");
75 return cached;
76 }
77 };
78
79 #ifdef QBT_PIXMAP_CACHE_FOR_FILE_ICONS
80 class CachingFileIconProvider : public UnifiedFileIconProvider
81 {
82 public:
83 using QFileIconProvider::icon;
84
icon(const QFileInfo & info) const85 QIcon icon(const QFileInfo &info) const final
86 {
87 const QString ext = info.suffix();
88 if (!ext.isEmpty())
89 {
90 QPixmap cached;
91 if (QPixmapCache::find(ext, &cached)) return {cached};
92
93 const QPixmap pixmap = pixmapForExtension(ext);
94 if (!pixmap.isNull())
95 {
96 QPixmapCache::insert(ext, pixmap);
97 return {pixmap};
98 }
99 }
100 return UnifiedFileIconProvider::icon(info);
101 }
102
103 protected:
104 virtual QPixmap pixmapForExtension(const QString &ext) const = 0;
105 };
106 #endif // QBT_PIXMAP_CACHE_FOR_FILE_ICONS
107
108 #if defined(Q_OS_WIN)
109 // See QTBUG-25319 for explanation why this is required
110 class WinShellFileIconProvider final : public CachingFileIconProvider
111 {
pixmapForExtension(const QString & ext) const112 QPixmap pixmapForExtension(const QString &ext) const override
113 {
114 const QString extWithDot = QLatin1Char('.') + ext;
115 SHFILEINFO sfi {};
116 HRESULT hr = ::SHGetFileInfoW(extWithDot.toStdWString().c_str(),
117 FILE_ATTRIBUTE_NORMAL, &sfi, sizeof(sfi), SHGFI_ICON | SHGFI_USEFILEATTRIBUTES);
118 if (FAILED(hr))
119 return {};
120
121 QPixmap iconPixmap = QtWin::fromHICON(sfi.hIcon);
122 ::DestroyIcon(sfi.hIcon);
123 return iconPixmap;
124 }
125 };
126 #elif defined(Q_OS_MACOS)
127 // There is a similar bug on macOS, to be reported to Qt
128 // https://github.com/qbittorrent/qBittorrent/pull/6156#issuecomment-316302615
129 class MacFileIconProvider final : public CachingFileIconProvider
130 {
pixmapForExtension(const QString & ext) const131 QPixmap pixmapForExtension(const QString &ext) const override
132 {
133 return MacUtils::pixmapForExtension(ext, QSize(32, 32));
134 }
135 };
136 #else
137 /**
138 * @brief Tests whether QFileIconProvider actually works
139 *
140 * Some QPA plugins do not implement QPlatformTheme::fileIcon(), and
141 * QFileIconProvider::icon() returns empty icons as the result. Here we ask it for
142 * two icons for probably absent files and when both icons are null, we assume that
143 * the current QPA plugin does not implement QPlatformTheme::fileIcon().
144 */
doesQFileIconProviderWork()145 bool doesQFileIconProviderWork()
146 {
147 QFileIconProvider provider;
148 const char PSEUDO_UNIQUE_FILE_NAME[] = "/tmp/qBittorrent-test-QFileIconProvider-845eb448-7ad5-4cdb-b764-b3f322a266a9";
149 QIcon testIcon1 = provider.icon(QFileInfo(
150 QLatin1String(PSEUDO_UNIQUE_FILE_NAME) + QLatin1String(".pdf")));
151 QIcon testIcon2 = provider.icon(QFileInfo(
152 QLatin1String(PSEUDO_UNIQUE_FILE_NAME) + QLatin1String(".png")));
153
154 return (!testIcon1.isNull() || !testIcon2.isNull());
155 }
156
157 class MimeFileIconProvider : public UnifiedFileIconProvider
158 {
159 using QFileIconProvider::icon;
160
icon(const QFileInfo & info) const161 QIcon icon(const QFileInfo &info) const override
162 {
163 const QMimeType mimeType = m_db.mimeTypeForFile(info, QMimeDatabase::MatchExtension);
164 QIcon res = QIcon::fromTheme(mimeType.iconName());
165 if (!res.isNull())
166 {
167 return res;
168 }
169
170 res = QIcon::fromTheme(mimeType.genericIconName());
171 if (!res.isNull())
172 {
173 return res;
174 }
175
176 return UnifiedFileIconProvider::icon(info);
177 }
178
179 private:
180 QMimeDatabase m_db;
181 };
182 #endif // Q_OS_WIN
183 }
184
TorrentContentModel(QObject * parent)185 TorrentContentModel::TorrentContentModel(QObject *parent)
186 : QAbstractItemModel(parent)
187 , m_rootItem(new TorrentContentModelFolder(QVector<QString>({ tr("Name"), tr("Size"), tr("Progress"), tr("Download Priority"), tr("Remaining"), tr("Availability") })))
188 {
189 #if defined(Q_OS_WIN)
190 m_fileIconProvider = new WinShellFileIconProvider();
191 #elif defined(Q_OS_MACOS)
192 m_fileIconProvider = new MacFileIconProvider();
193 #else
194 static bool doesBuiltInProviderWork = doesQFileIconProviderWork();
195 m_fileIconProvider = doesBuiltInProviderWork ? new QFileIconProvider() : new MimeFileIconProvider();
196 #endif
197 }
198
~TorrentContentModel()199 TorrentContentModel::~TorrentContentModel()
200 {
201 delete m_fileIconProvider;
202 delete m_rootItem;
203 }
204
updateFilesProgress(const QVector<qreal> & fp)205 void TorrentContentModel::updateFilesProgress(const QVector<qreal> &fp)
206 {
207 Q_ASSERT(m_filesIndex.size() == fp.size());
208 // XXX: Why is this necessary?
209 if (m_filesIndex.size() != fp.size()) return;
210
211 emit layoutAboutToBeChanged();
212 for (int i = 0; i < fp.size(); ++i)
213 m_filesIndex[i]->setProgress(fp[i]);
214 // Update folders progress in the tree
215 m_rootItem->recalculateProgress();
216 m_rootItem->recalculateAvailability();
217 emit dataChanged(index(0, 0), index((rowCount() - 1), (columnCount() - 1)));
218 }
219
updateFilesPriorities(const QVector<BitTorrent::DownloadPriority> & fprio)220 void TorrentContentModel::updateFilesPriorities(const QVector<BitTorrent::DownloadPriority> &fprio)
221 {
222 Q_ASSERT(m_filesIndex.size() == fprio.size());
223 // XXX: Why is this necessary?
224 if (m_filesIndex.size() != fprio.size())
225 return;
226
227 emit layoutAboutToBeChanged();
228 for (int i = 0; i < fprio.size(); ++i)
229 m_filesIndex[i]->setPriority(static_cast<BitTorrent::DownloadPriority>(fprio[i]));
230 emit dataChanged(index(0, 0), index((rowCount() - 1), (columnCount() - 1)));
231 }
232
updateFilesAvailability(const QVector<qreal> & fa)233 void TorrentContentModel::updateFilesAvailability(const QVector<qreal> &fa)
234 {
235 Q_ASSERT(m_filesIndex.size() == fa.size());
236 // XXX: Why is this necessary?
237 if (m_filesIndex.size() != fa.size()) return;
238
239 emit layoutAboutToBeChanged();
240 for (int i = 0; i < m_filesIndex.size(); ++i)
241 m_filesIndex[i]->setAvailability(fa[i]);
242 // Update folders progress in the tree
243 m_rootItem->recalculateProgress();
244 emit dataChanged(index(0, 0), index((rowCount() - 1), (columnCount() - 1)));
245 }
246
getFilePriorities() const247 QVector<BitTorrent::DownloadPriority> TorrentContentModel::getFilePriorities() const
248 {
249 QVector<BitTorrent::DownloadPriority> prio;
250 prio.reserve(m_filesIndex.size());
251 for (const TorrentContentModelFile *file : asConst(m_filesIndex))
252 prio.push_back(file->priority());
253 return prio;
254 }
255
allFiltered() const256 bool TorrentContentModel::allFiltered() const
257 {
258 return std::all_of(m_filesIndex.cbegin(), m_filesIndex.cend(), [](const TorrentContentModelFile *fileItem)
259 {
260 return (fileItem->priority() == BitTorrent::DownloadPriority::Ignored);
261 });
262 }
263
columnCount(const QModelIndex & parent) const264 int TorrentContentModel::columnCount(const QModelIndex &parent) const
265 {
266 if (parent.isValid())
267 return static_cast<TorrentContentModelItem*>(parent.internalPointer())->columnCount();
268
269 return m_rootItem->columnCount();
270 }
271
setData(const QModelIndex & index,const QVariant & value,int role)272 bool TorrentContentModel::setData(const QModelIndex &index, const QVariant &value, int role)
273 {
274 if (!index.isValid())
275 return false;
276
277 if ((index.column() == TorrentContentModelItem::COL_NAME) && (role == Qt::CheckStateRole))
278 {
279 auto *item = static_cast<TorrentContentModelItem*>(index.internalPointer());
280 qDebug("setData(%s, %d", qUtf8Printable(item->name()), value.toInt());
281 if (static_cast<int>(item->priority()) != value.toInt())
282 {
283 BitTorrent::DownloadPriority prio = BitTorrent::DownloadPriority::Normal;
284 if (value.toInt() == Qt::PartiallyChecked)
285 prio = BitTorrent::DownloadPriority::Mixed;
286 else if (value.toInt() == Qt::Unchecked)
287 prio = BitTorrent::DownloadPriority::Ignored;
288
289 item->setPriority(prio);
290 // Update folders progress in the tree
291 m_rootItem->recalculateProgress();
292 m_rootItem->recalculateAvailability();
293 emit dataChanged(this->index(0, 0), this->index((rowCount() - 1), (columnCount() - 1)));
294 emit filteredFilesChanged();
295 }
296 return true;
297 }
298
299 if (role == Qt::EditRole)
300 {
301 Q_ASSERT(index.isValid());
302 auto *item = static_cast<TorrentContentModelItem*>(index.internalPointer());
303 switch (index.column())
304 {
305 case TorrentContentModelItem::COL_NAME:
306 item->setName(value.toString());
307 break;
308 case TorrentContentModelItem::COL_PRIO:
309 item->setPriority(static_cast<BitTorrent::DownloadPriority>(value.toInt()));
310 break;
311 default:
312 return false;
313 }
314 emit dataChanged(index, index);
315 return true;
316 }
317
318 return false;
319 }
320
itemType(const QModelIndex & index) const321 TorrentContentModelItem::ItemType TorrentContentModel::itemType(const QModelIndex &index) const
322 {
323 return static_cast<const TorrentContentModelItem*>(index.internalPointer())->itemType();
324 }
325
getFileIndex(const QModelIndex & index)326 int TorrentContentModel::getFileIndex(const QModelIndex &index)
327 {
328 auto *item = static_cast<TorrentContentModelItem*>(index.internalPointer());
329 if (item->itemType() == TorrentContentModelItem::FileType)
330 return static_cast<TorrentContentModelFile*>(item)->fileIndex();
331
332 Q_ASSERT(item->itemType() == TorrentContentModelItem::FileType);
333 return -1;
334 }
335
data(const QModelIndex & index,int role) const336 QVariant TorrentContentModel::data(const QModelIndex &index, int role) const
337 {
338 if (!index.isValid())
339 return {};
340
341 auto *item = static_cast<TorrentContentModelItem*>(index.internalPointer());
342
343 switch (role)
344 {
345 case Qt::DecorationRole:
346 {
347 if (index.column() != TorrentContentModelItem::COL_NAME)
348 return {};
349
350 if (item->itemType() == TorrentContentModelItem::FolderType)
351 return m_fileIconProvider->icon(QFileIconProvider::Folder);
352 return m_fileIconProvider->icon(QFileInfo(item->name()));
353 }
354 case Qt::CheckStateRole:
355 {
356 if (index.column() != TorrentContentModelItem::COL_NAME)
357 return {};
358
359 if (item->priority() == BitTorrent::DownloadPriority::Ignored)
360 return Qt::Unchecked;
361 if (item->priority() == BitTorrent::DownloadPriority::Mixed)
362 return Qt::PartiallyChecked;
363 return Qt::Checked;
364 }
365 case Qt::TextAlignmentRole:
366 if ((index.column() == TorrentContentModelItem::COL_SIZE)
367 || (index.column() == TorrentContentModelItem::COL_REMAINING))
368 return QVariant {Qt::AlignRight | Qt::AlignVCenter};
369 return {};
370
371 case Qt::DisplayRole:
372 return item->displayData(index.column());
373
374 case Roles::UnderlyingDataRole:
375 return item->underlyingData(index.column());
376
377 default:
378 return {};
379 }
380 }
381
flags(const QModelIndex & index) const382 Qt::ItemFlags TorrentContentModel::flags(const QModelIndex &index) const
383 {
384 if (!index.isValid())
385 return Qt::NoItemFlags;
386
387 Qt::ItemFlags flags {Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable};
388 if (itemType(index) == TorrentContentModelItem::FolderType)
389 flags |= Qt::ItemIsAutoTristate;
390 if (index.column() == TorrentContentModelItem::COL_PRIO)
391 flags |= Qt::ItemIsEditable;
392
393 return flags;
394 }
395
headerData(int section,Qt::Orientation orientation,int role) const396 QVariant TorrentContentModel::headerData(int section, Qt::Orientation orientation, int role) const
397 {
398 if (orientation != Qt::Horizontal)
399 return {};
400
401 switch (role)
402 {
403 case Qt::DisplayRole:
404 return m_rootItem->displayData(section);
405
406 case Qt::TextAlignmentRole:
407 if ((section == TorrentContentModelItem::COL_SIZE)
408 || (section == TorrentContentModelItem::COL_REMAINING))
409 return QVariant {Qt::AlignRight | Qt::AlignVCenter};
410 return {};
411
412 default:
413 return {};
414 }
415 }
416
index(int row,int column,const QModelIndex & parent) const417 QModelIndex TorrentContentModel::index(int row, int column, const QModelIndex &parent) const
418 {
419 if (parent.isValid() && (parent.column() != 0))
420 return {};
421
422 if (column >= TorrentContentModelItem::NB_COL)
423 return {};
424
425 TorrentContentModelFolder *parentItem;
426 if (!parent.isValid())
427 parentItem = m_rootItem;
428 else
429 parentItem = static_cast<TorrentContentModelFolder*>(parent.internalPointer());
430 Q_ASSERT(parentItem);
431
432 if (row >= parentItem->childCount())
433 return {};
434
435 TorrentContentModelItem *childItem = parentItem->child(row);
436 if (childItem)
437 return createIndex(row, column, childItem);
438 return {};
439 }
440
parent(const QModelIndex & index) const441 QModelIndex TorrentContentModel::parent(const QModelIndex &index) const
442 {
443 if (!index.isValid())
444 return {};
445
446 auto *childItem = static_cast<TorrentContentModelItem*>(index.internalPointer());
447 if (!childItem)
448 return {};
449
450 TorrentContentModelItem *parentItem = childItem->parent();
451 if (parentItem == m_rootItem)
452 return {};
453
454 return createIndex(parentItem->row(), 0, parentItem);
455 }
456
rowCount(const QModelIndex & parent) const457 int TorrentContentModel::rowCount(const QModelIndex &parent) const
458 {
459 if (parent.column() > 0)
460 return 0;
461
462 TorrentContentModelFolder *parentItem;
463 if (!parent.isValid())
464 parentItem = m_rootItem;
465 else
466 parentItem = dynamic_cast<TorrentContentModelFolder*>(static_cast<TorrentContentModelItem*>(parent.internalPointer()));
467
468 return parentItem ? parentItem->childCount() : 0;
469 }
470
clear()471 void TorrentContentModel::clear()
472 {
473 qDebug("clear called");
474 beginResetModel();
475 m_filesIndex.clear();
476 m_rootItem->deleteAllChildren();
477 endResetModel();
478 }
479
setupModelData(const BitTorrent::TorrentInfo & info)480 void TorrentContentModel::setupModelData(const BitTorrent::TorrentInfo &info)
481 {
482 qDebug("setup model data called");
483 const int filesCount = info.filesCount();
484 if (filesCount <= 0)
485 return;
486
487 emit layoutAboutToBeChanged();
488 // Initialize files_index array
489 qDebug("Torrent contains %d files", filesCount);
490 m_filesIndex.reserve(filesCount);
491
492 TorrentContentModelFolder *currentParent;
493 // Iterate over files
494 for (int i = 0; i < filesCount; ++i)
495 {
496 currentParent = m_rootItem;
497 const QString path = Utils::Fs::toUniformPath(info.filePath(i));
498
499 // Iterate of parts of the path to create necessary folders
500 QVector<QStringRef> pathFolders = path.splitRef('/', QString::SkipEmptyParts);
501 pathFolders.removeLast();
502
503 for (const QStringRef &pathPartRef : asConst(pathFolders))
504 {
505 const QString pathPart = pathPartRef.toString();
506 TorrentContentModelFolder *newParent = currentParent->childFolderWithName(pathPart);
507 if (!newParent)
508 {
509 newParent = new TorrentContentModelFolder(pathPart, currentParent);
510 currentParent->appendChild(newParent);
511 }
512 currentParent = newParent;
513 }
514 // Actually create the file
515 TorrentContentModelFile *fileItem = new TorrentContentModelFile(info.fileName(i), info.fileSize(i), currentParent, i);
516 currentParent->appendChild(fileItem);
517 m_filesIndex.push_back(fileItem);
518 }
519 emit layoutChanged();
520 }
521
selectAll()522 void TorrentContentModel::selectAll()
523 {
524 for (int i = 0; i < m_rootItem->childCount(); ++i)
525 {
526 TorrentContentModelItem* child = m_rootItem->child(i);
527 if (child->priority() == BitTorrent::DownloadPriority::Ignored)
528 child->setPriority(BitTorrent::DownloadPriority::Normal);
529 }
530 emit dataChanged(index(0, 0), index((rowCount() - 1), (columnCount() - 1)));
531 }
532
selectNone()533 void TorrentContentModel::selectNone()
534 {
535 for (int i = 0; i < m_rootItem->childCount(); ++i)
536 m_rootItem->child(i)->setPriority(BitTorrent::DownloadPriority::Ignored);
537 emit dataChanged(index(0, 0), index((rowCount() - 1), (columnCount() - 1)));
538 }
539