qBittorrent
torrentcontentmodel.cpp
Go to the documentation of this file.
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 #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
41 #include <QtWin>
42 #endif
43 #else
44 #include <QMimeDatabase>
45 #include <QMimeType>
46 #endif
47 
48 #if defined Q_OS_WIN || defined Q_OS_MACOS
49 #define QBT_PIXMAP_CACHE_FOR_FILE_ICONS
50 #include <QPixmapCache>
51 #endif
52 
55 #include "base/global.h"
56 #include "base/utils/fs.h"
60 #include "uithememanager.h"
61 
62 #ifdef Q_OS_MACOS
63 #include "macutilities.h"
64 #endif
65 
66 namespace
67 {
68  class UnifiedFileIconProvider : public QFileIconProvider
69  {
70  public:
71  using QFileIconProvider::icon;
72 
73  QIcon icon(const QFileInfo &info) const override
74  {
75  Q_UNUSED(info);
76  static QIcon cached = UIThemeManager::instance()->getIcon("text-plain");
77  return cached;
78  }
79  };
80 
81 #ifdef QBT_PIXMAP_CACHE_FOR_FILE_ICONS
82  class CachingFileIconProvider : public UnifiedFileIconProvider
83  {
84  public:
85  using QFileIconProvider::icon;
86 
87  QIcon icon(const QFileInfo &info) const final
88  {
89  const QString ext = info.suffix();
90  if (!ext.isEmpty())
91  {
92  QPixmap cached;
93  if (QPixmapCache::find(ext, &cached)) return {cached};
94 
95  const QPixmap pixmap = pixmapForExtension(ext);
96  if (!pixmap.isNull())
97  {
98  QPixmapCache::insert(ext, pixmap);
99  return {pixmap};
100  }
101  }
102  return UnifiedFileIconProvider::icon(info);
103  }
104 
105  protected:
106  virtual QPixmap pixmapForExtension(const QString &ext) const = 0;
107  };
108 #endif // QBT_PIXMAP_CACHE_FOR_FILE_ICONS
109 
110 #if defined(Q_OS_WIN)
111  // See QTBUG-25319 for explanation why this is required
112  class WinShellFileIconProvider final : public CachingFileIconProvider
113  {
114  QPixmap pixmapForExtension(const QString &ext) const override
115  {
116  const QString extWithDot = QLatin1Char('.') + ext;
117  SHFILEINFO sfi {};
118  HRESULT hr = ::SHGetFileInfoW(extWithDot.toStdWString().c_str(),
119  FILE_ATTRIBUTE_NORMAL, &sfi, sizeof(sfi), SHGFI_ICON | SHGFI_USEFILEATTRIBUTES);
120  if (FAILED(hr))
121  return {};
122 
123 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
124  auto iconPixmap = QPixmap::fromImage(QImage::fromHICON(sfi.hIcon));
125 #else
126  QPixmap iconPixmap = QtWin::fromHICON(sfi.hIcon);
127 #endif
128  ::DestroyIcon(sfi.hIcon);
129  return iconPixmap;
130  }
131  };
132 #elif defined(Q_OS_MACOS)
133  // There is a similar bug on macOS, to be reported to Qt
134  // https://github.com/qbittorrent/qBittorrent/pull/6156#issuecomment-316302615
135  class MacFileIconProvider final : public CachingFileIconProvider
136  {
137  QPixmap pixmapForExtension(const QString &ext) const override
138  {
139  return MacUtils::pixmapForExtension(ext, QSize(32, 32));
140  }
141  };
142 #else
152  {
153  QFileIconProvider provider;
154  const char PSEUDO_UNIQUE_FILE_NAME[] = "/tmp/qBittorrent-test-QFileIconProvider-845eb448-7ad5-4cdb-b764-b3f322a266a9";
155  QIcon testIcon1 = provider.icon(QFileInfo(
156  QLatin1String(PSEUDO_UNIQUE_FILE_NAME) + QLatin1String(".pdf")));
157  QIcon testIcon2 = provider.icon(QFileInfo(
158  QLatin1String(PSEUDO_UNIQUE_FILE_NAME) + QLatin1String(".png")));
159 
160  return (!testIcon1.isNull() || !testIcon2.isNull());
161  }
162 
164  {
165  using QFileIconProvider::icon;
166 
167  QIcon icon(const QFileInfo &info) const override
168  {
169  const QMimeType mimeType = m_db.mimeTypeForFile(info, QMimeDatabase::MatchExtension);
170  QIcon res = QIcon::fromTheme(mimeType.iconName());
171  if (!res.isNull())
172  {
173  return res;
174  }
175 
176  res = QIcon::fromTheme(mimeType.genericIconName());
177  if (!res.isNull())
178  {
179  return res;
180  }
181 
182  return UnifiedFileIconProvider::icon(info);
183  }
184 
185  private:
186  QMimeDatabase m_db;
187  };
188 #endif // Q_OS_WIN
189 }
190 
192  : QAbstractItemModel(parent)
193  , m_rootItem(new TorrentContentModelFolder(QVector<QString>({ tr("Name"), tr("Size"), tr("Progress"), tr("Download Priority"), tr("Remaining"), tr("Availability") })))
194 {
195 #if defined(Q_OS_WIN)
196  m_fileIconProvider = new WinShellFileIconProvider();
197 #elif defined(Q_OS_MACOS)
198  m_fileIconProvider = new MacFileIconProvider();
199 #else
200  static bool doesBuiltInProviderWork = doesQFileIconProviderWork();
201  m_fileIconProvider = doesBuiltInProviderWork ? new QFileIconProvider() : new MimeFileIconProvider();
202 #endif
203 }
204 
206 {
207  delete m_fileIconProvider;
208  delete m_rootItem;
209 }
210 
211 void TorrentContentModel::updateFilesProgress(const QVector<qreal> &fp)
212 {
213  Q_ASSERT(m_filesIndex.size() == fp.size());
214  // XXX: Why is this necessary?
215  if (m_filesIndex.size() != fp.size()) return;
216 
217  emit layoutAboutToBeChanged();
218  for (int i = 0; i < fp.size(); ++i)
219  m_filesIndex[i]->setProgress(fp[i]);
220  // Update folders progress in the tree
223  emit dataChanged(index(0, 0), index((rowCount() - 1), (columnCount() - 1)));
224 }
225 
226 void TorrentContentModel::updateFilesPriorities(const QVector<BitTorrent::DownloadPriority> &fprio)
227 {
228  Q_ASSERT(m_filesIndex.size() == fprio.size());
229  // XXX: Why is this necessary?
230  if (m_filesIndex.size() != fprio.size())
231  return;
232 
233  emit layoutAboutToBeChanged();
234  for (int i = 0; i < fprio.size(); ++i)
235  m_filesIndex[i]->setPriority(static_cast<BitTorrent::DownloadPriority>(fprio[i]));
236  emit dataChanged(index(0, 0), index((rowCount() - 1), (columnCount() - 1)));
237 }
238 
239 void TorrentContentModel::updateFilesAvailability(const QVector<qreal> &fa)
240 {
241  Q_ASSERT(m_filesIndex.size() == fa.size());
242  // XXX: Why is this necessary?
243  if (m_filesIndex.size() != fa.size()) return;
244 
245  emit layoutAboutToBeChanged();
246  for (int i = 0; i < m_filesIndex.size(); ++i)
247  m_filesIndex[i]->setAvailability(fa[i]);
248  // Update folders progress in the tree
250  emit dataChanged(index(0, 0), index((rowCount() - 1), (columnCount() - 1)));
251 }
252 
253 QVector<BitTorrent::DownloadPriority> TorrentContentModel::getFilePriorities() const
254 {
255  QVector<BitTorrent::DownloadPriority> prio;
256  prio.reserve(m_filesIndex.size());
258  prio.push_back(file->priority());
259  return prio;
260 }
261 
263 {
264  return std::all_of(m_filesIndex.cbegin(), m_filesIndex.cend(), [](const TorrentContentModelFile *fileItem)
265  {
266  return (fileItem->priority() == BitTorrent::DownloadPriority::Ignored);
267  });
268 }
269 
270 int TorrentContentModel::columnCount(const QModelIndex &parent) const
271 {
272  if (parent.isValid())
273  return static_cast<TorrentContentModelItem*>(parent.internalPointer())->columnCount();
274 
275  return m_rootItem->columnCount();
276 }
277 
278 bool TorrentContentModel::setData(const QModelIndex &index, const QVariant &value, int role)
279 {
280  if (!index.isValid())
281  return false;
282 
283  if ((index.column() == TorrentContentModelItem::COL_NAME) && (role == Qt::CheckStateRole))
284  {
285  auto *item = static_cast<TorrentContentModelItem*>(index.internalPointer());
286  qDebug("setData(%s, %d)", qUtf8Printable(item->name()), value.toInt());
287 
289  if (value.toInt() == Qt::PartiallyChecked)
291  else if (value.toInt() == Qt::Unchecked)
293 
294  if (item->priority() != prio)
295  {
296  item->setPriority(prio);
297  // Update folders progress in the tree
300  emit dataChanged(this->index(0, 0), this->index((rowCount() - 1), (columnCount() - 1)));
301  emit filteredFilesChanged();
302  }
303  return true;
304  }
305 
306  if (role == Qt::EditRole)
307  {
308  Q_ASSERT(index.isValid());
309  auto *item = static_cast<TorrentContentModelItem*>(index.internalPointer());
310  switch (index.column())
311  {
313  item->setName(value.toString());
314  break;
316  item->setPriority(static_cast<BitTorrent::DownloadPriority>(value.toInt()));
317  break;
318  default:
319  return false;
320  }
321  emit dataChanged(index, index);
322  return true;
323  }
324 
325  return false;
326 }
327 
329 {
330  return static_cast<const TorrentContentModelItem*>(index.internalPointer())->itemType();
331 }
332 
333 int TorrentContentModel::getFileIndex(const QModelIndex &index)
334 {
335  auto *item = static_cast<TorrentContentModelItem*>(index.internalPointer());
336  if (item->itemType() == TorrentContentModelItem::FileType)
337  return static_cast<TorrentContentModelFile*>(item)->fileIndex();
338 
339  Q_ASSERT(item->itemType() == TorrentContentModelItem::FileType);
340  return -1;
341 }
342 
343 QVariant TorrentContentModel::data(const QModelIndex &index, const int role) const
344 {
345  if (!index.isValid())
346  return {};
347 
348  auto *item = static_cast<TorrentContentModelItem*>(index.internalPointer());
349 
350  switch (role)
351  {
352  case Qt::DecorationRole:
353  {
355  return {};
356 
357  if (item->itemType() == TorrentContentModelItem::FolderType)
358  return m_fileIconProvider->icon(QFileIconProvider::Folder);
359  return m_fileIconProvider->icon(QFileInfo(item->name()));
360  }
361  case Qt::CheckStateRole:
362  {
364  return {};
365 
366  if (item->priority() == BitTorrent::DownloadPriority::Ignored)
367  return Qt::Unchecked;
368  if (item->priority() == BitTorrent::DownloadPriority::Mixed)
369  return Qt::PartiallyChecked;
370  return Qt::Checked;
371  }
372  case Qt::TextAlignmentRole:
373  if ((index.column() == TorrentContentModelItem::COL_SIZE)
375  return QVariant {Qt::AlignRight | Qt::AlignVCenter};
376  return {};
377 
378  case Qt::DisplayRole:
379  case Qt::ToolTipRole:
380  return item->displayData(index.column());
381 
382  case Roles::UnderlyingDataRole:
383  return item->underlyingData(index.column());
384 
385  default:
386  return {};
387  }
388 }
389 
390 Qt::ItemFlags TorrentContentModel::flags(const QModelIndex &index) const
391 {
392  if (!index.isValid())
393  return Qt::NoItemFlags;
394 
395  Qt::ItemFlags flags {Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable};
397  flags |= Qt::ItemIsAutoTristate;
399  flags |= Qt::ItemIsEditable;
400 
401  return flags;
402 }
403 
404 QVariant TorrentContentModel::headerData(int section, Qt::Orientation orientation, int role) const
405 {
406  if (orientation != Qt::Horizontal)
407  return {};
408 
409  switch (role)
410  {
411  case Qt::DisplayRole:
412  return m_rootItem->displayData(section);
413 
414  case Qt::TextAlignmentRole:
415  if ((section == TorrentContentModelItem::COL_SIZE)
417  return QVariant {Qt::AlignRight | Qt::AlignVCenter};
418  return {};
419 
420  default:
421  return {};
422  }
423 }
424 
425 QModelIndex TorrentContentModel::index(int row, int column, const QModelIndex &parent) const
426 {
427  if (parent.isValid() && (parent.column() != 0))
428  return {};
429 
430  if (column >= TorrentContentModelItem::NB_COL)
431  return {};
432 
433  TorrentContentModelFolder *parentItem;
434  if (!parent.isValid())
435  parentItem = m_rootItem;
436  else
437  parentItem = static_cast<TorrentContentModelFolder*>(parent.internalPointer());
438  Q_ASSERT(parentItem);
439 
440  if (row >= parentItem->childCount())
441  return {};
442 
443  TorrentContentModelItem *childItem = parentItem->child(row);
444  if (childItem)
445  return createIndex(row, column, childItem);
446  return {};
447 }
448 
449 QModelIndex TorrentContentModel::parent(const QModelIndex &index) const
450 {
451  if (!index.isValid())
452  return {};
453 
454  auto *childItem = static_cast<TorrentContentModelItem*>(index.internalPointer());
455  if (!childItem)
456  return {};
457 
458  TorrentContentModelItem *parentItem = childItem->parent();
459  if (parentItem == m_rootItem)
460  return {};
461 
462  return createIndex(parentItem->row(), 0, parentItem);
463 }
464 
465 int TorrentContentModel::rowCount(const QModelIndex &parent) const
466 {
467  if (parent.column() > 0)
468  return 0;
469 
470  TorrentContentModelFolder *parentItem;
471  if (!parent.isValid())
472  parentItem = m_rootItem;
473  else
474  parentItem = dynamic_cast<TorrentContentModelFolder*>(static_cast<TorrentContentModelItem*>(parent.internalPointer()));
475 
476  return parentItem ? parentItem->childCount() : 0;
477 }
478 
480 {
481  qDebug("clear called");
482  beginResetModel();
483  m_filesIndex.clear();
485  endResetModel();
486 }
487 
489 {
490  qDebug("setup model data called");
491  const int filesCount = info.filesCount();
492  if (filesCount <= 0)
493  return;
494 
495  emit layoutAboutToBeChanged();
496  // Initialize files_index array
497  qDebug("Torrent contains %d files", filesCount);
498  m_filesIndex.reserve(filesCount);
499 
500  TorrentContentModelFolder *currentParent;
501  // Iterate over files
502  for (int i = 0; i < filesCount; ++i)
503  {
504  currentParent = m_rootItem;
505  const QString path = Utils::Fs::toUniformPath(info.filePath(i));
506 
507  // Iterate of parts of the path to create necessary folders
508  QList<QStringView> pathFolders = QStringView(path).split(u'/', Qt::SkipEmptyParts);
509  pathFolders.removeLast();
510 
511  for (const QStringView pathPart : asConst(pathFolders))
512  {
513  const QString folderPath = pathPart.toString();
514  TorrentContentModelFolder *newParent = currentParent->childFolderWithName(folderPath);
515  if (!newParent)
516  {
517  newParent = new TorrentContentModelFolder(folderPath, currentParent);
518  currentParent->appendChild(newParent);
519  }
520  currentParent = newParent;
521  }
522  // Actually create the file
524  Utils::Fs::fileName(info.filePath(i)), info.fileSize(i), currentParent, i);
525  currentParent->appendChild(fileItem);
526  m_filesIndex.push_back(fileItem);
527  }
528  emit layoutChanged();
529 }
530 
532 {
533  for (int i = 0; i < m_rootItem->childCount(); ++i)
534  {
538  }
539  emit dataChanged(index(0, 0), index((rowCount() - 1), (columnCount() - 1)));
540 }
541 
543 {
544  for (int i = 0; i < m_rootItem->childCount(); ++i)
546  emit dataChanged(index(0, 0), index((rowCount() - 1), (columnCount() - 1)));
547 }
virtual int filesCount() const =0
virtual qlonglong fileSize(int index) const =0
virtual QString filePath(int index) const =0
TorrentContentModelFolder * childFolderWithName(const QString &name) const
void appendChild(TorrentContentModelItem *item)
TorrentContentModelItem * child(int row) const
void updateFilesPriorities(const QVector< BitTorrent::DownloadPriority > &fprio)
QVector< BitTorrent::DownloadPriority > getFilePriorities() const
Qt::ItemFlags flags(const QModelIndex &index) const override
void updateFilesProgress(const QVector< qreal > &fp)
void setupModelData(const BitTorrent::AbstractFileStorage &info)
bool setData(const QModelIndex &index, const QVariant &value, int role=Qt::EditRole) override
TorrentContentModelItem::ItemType itemType(const QModelIndex &index) const
TorrentContentModelFolder * m_rootItem
QFileIconProvider * m_fileIconProvider
QVariant data(const QModelIndex &index, int role=Qt::DisplayRole) const override
QVector< TorrentContentModelFile * > m_filesIndex
QModelIndex index(int row, int column, const QModelIndex &parent={}) const override
int getFileIndex(const QModelIndex &index)
QModelIndex parent(const QModelIndex &index) const override
void updateFilesAvailability(const QVector< qreal > &fa)
int columnCount(const QModelIndex &parent={}) const override
QVariant headerData(int section, Qt::Orientation orientation, int role) const override
int rowCount(const QModelIndex &parent={}) const override
TorrentContentModel(QObject *parent=nullptr)
BitTorrent::DownloadPriority priority() const
virtual void setPriority(BitTorrent::DownloadPriority newPriority, bool updateParent=true)=0
QString displayData(int column) const
TorrentContentModelFolder * parent() const
static UIThemeManager * instance()
QIcon getIcon(const QString &iconId, const QString &fallback={}) const
constexpr std::add_const_t< T > & asConst(T &t) noexcept
Definition: global.h:42
QPixmap pixmapForExtension(const QString &ext, const QSize &size)
Definition: macutilities.mm:43
QString fileName(const QString &filePath)
Definition: fs.cpp:87
QString toUniformPath(const QString &path)
Definition: fs.cpp:69
T value(const QString &key, const T &defaultValue={})
Definition: preferences.cpp:64
bool doesQFileIconProviderWork()
Tests whether QFileIconProvider actually works.
file(GLOB QBT_TS_FILES "${qBittorrent_SOURCE_DIR}/src/lang/*.ts") set_source_files_properties($
Definition: CMakeLists.txt:5