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