1 /*
2  * Bittorrent Client using Qt and libtorrent.
3  * Copyright (C) 2015  Vladimir Golovnev <glassez@yandex.ru>
4  * Copyright (C) 2010  Christophe Dumez <chris@qbittorrent.org>
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU General Public License
8  * as published by the Free Software Foundation; either version 2
9  * of the License, or (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, write to the Free Software
18  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19  *
20  * In addition, as a special exception, the copyright holders give permission to
21  * link this program with the OpenSSL project's "OpenSSL" library (or with
22  * modified versions of it that use the same license as the "OpenSSL" library),
23  * and distribute the linked executables. You must obey the GNU General Public
24  * License in all respects for all of the code used other than "OpenSSL".  If you
25  * modify file(s), you may extend this exception to your version of the file(s),
26  * but you are not obligated to do so. If you do not wish to do so, delete this
27  * exception statement from your version.
28  */
29 
30 #include "transferlistmodel.h"
31 
32 #include <QApplication>
33 #include <QDateTime>
34 #include <QDebug>
35 #include <QIcon>
36 #include <QPalette>
37 
38 #include "base/bittorrent/session.h"
39 #include "base/bittorrent/torrent.h"
40 #include "base/global.h"
41 #include "base/preferences.h"
42 #include "base/unicodestrings.h"
43 #include "base/utils/fs.h"
44 #include "base/utils/misc.h"
45 #include "base/utils/string.h"
46 #include "uithememanager.h"
47 
48 static QIcon getIconByState(BitTorrent::TorrentState state);
49 static QColor getDefaultColorByState(BitTorrent::TorrentState state);
50 
51 static QIcon getPausedIcon();
52 static QIcon getQueuedIcon();
53 static QIcon getDownloadingIcon();
54 static QIcon getStalledDownloadingIcon();
55 static QIcon getUploadingIcon();
56 static QIcon getStalledUploadingIcon();
57 static QIcon getCompletedIcon();
58 static QIcon getCheckingIcon();
59 static QIcon getErrorIcon();
60 
61 static bool isDarkTheme();
62 
63 namespace
64 {
torrentStateColorsFromUITheme()65     QHash<BitTorrent::TorrentState, QColor> torrentStateColorsFromUITheme()
66     {
67         struct TorrentStateColorDescriptor
68         {
69             const BitTorrent::TorrentState state;
70             const QString id;
71         };
72 
73         const TorrentStateColorDescriptor colorDescriptors[] =
74         {
75             {BitTorrent::TorrentState::Downloading, QLatin1String("TransferList.Downloading")},
76             {BitTorrent::TorrentState::StalledDownloading, QLatin1String("TransferList.StalledDownloading")},
77             {BitTorrent::TorrentState::DownloadingMetadata, QLatin1String("TransferList.DownloadingMetadata")},
78             {BitTorrent::TorrentState::ForcedDownloading, QLatin1String("TransferList.ForcedDownloading")},
79             {BitTorrent::TorrentState::Uploading, QLatin1String("TransferList.Uploading")},
80             {BitTorrent::TorrentState::StalledUploading, QLatin1String("TransferList.StalledUploading")},
81             {BitTorrent::TorrentState::ForcedUploading, QLatin1String("TransferList.ForcedUploading")},
82             {BitTorrent::TorrentState::QueuedDownloading, QLatin1String("TransferList.QueuedDownloading")},
83             {BitTorrent::TorrentState::QueuedUploading, QLatin1String("TransferList.QueuedUploading")},
84             {BitTorrent::TorrentState::CheckingDownloading, QLatin1String("TransferList.CheckingDownloading")},
85             {BitTorrent::TorrentState::CheckingUploading, QLatin1String("TransferList.CheckingUploading")},
86             {BitTorrent::TorrentState::CheckingResumeData, QLatin1String("TransferList.CheckingResumeData")},
87             {BitTorrent::TorrentState::PausedDownloading, QLatin1String("TransferList.PausedDownloading")},
88             {BitTorrent::TorrentState::PausedUploading, QLatin1String("TransferList.PausedUploading")},
89             {BitTorrent::TorrentState::Moving, QLatin1String("TransferList.Moving")},
90             {BitTorrent::TorrentState::MissingFiles, QLatin1String("TransferList.MissingFiles")},
91             {BitTorrent::TorrentState::Error, QLatin1String("TransferList.Error")}
92         };
93 
94         QHash<BitTorrent::TorrentState, QColor> colors;
95         for (const TorrentStateColorDescriptor &colorDescriptor : colorDescriptors)
96         {
97             const QColor themeColor = UIThemeManager::instance()->getColor(colorDescriptor.id, QColor());
98             if (themeColor.isValid())
99                 colors.insert(colorDescriptor.state, themeColor);
100         }
101         return colors;
102     }
103 }
104 
105 // TransferListModel
106 
TransferListModel(QObject * parent)107 TransferListModel::TransferListModel(QObject *parent)
108     : QAbstractListModel {parent}
109     , m_statusStrings
110     {
111           {BitTorrent::TorrentState::Downloading, tr("Downloading")},
112           {BitTorrent::TorrentState::StalledDownloading, tr("Stalled", "Torrent is waiting for download to begin")},
113           {BitTorrent::TorrentState::DownloadingMetadata, tr("Downloading metadata", "Used when loading a magnet link")},
114           {BitTorrent::TorrentState::ForcedDownloading, tr("[F] Downloading", "Used when the torrent is forced started. You probably shouldn't translate the F.")},
115           {BitTorrent::TorrentState::Uploading, tr("Seeding", "Torrent is complete and in upload-only mode")},
116           {BitTorrent::TorrentState::StalledUploading, tr("Seeding", "Torrent is complete and in upload-only mode")},
117           {BitTorrent::TorrentState::ForcedUploading, tr("[F] Seeding", "Used when the torrent is forced started. You probably shouldn't translate the F.")},
118           {BitTorrent::TorrentState::QueuedDownloading, tr("Queued", "Torrent is queued")},
119           {BitTorrent::TorrentState::QueuedUploading, tr("Queued", "Torrent is queued")},
120           {BitTorrent::TorrentState::CheckingDownloading, tr("Checking", "Torrent local data is being checked")},
121           {BitTorrent::TorrentState::CheckingUploading, tr("Checking", "Torrent local data is being checked")},
122           {BitTorrent::TorrentState::CheckingResumeData, tr("Checking resume data", "Used when loading the torrents from disk after qbt is launched. It checks the correctness of the .fastresume file. Normally it is completed in a fraction of a second, unless loading many many torrents.")},
123           {BitTorrent::TorrentState::PausedDownloading, tr("Paused")},
124           {BitTorrent::TorrentState::PausedUploading, tr("Completed")},
125           {BitTorrent::TorrentState::Moving, tr("Moving", "Torrent local data are being moved/relocated")},
126           {BitTorrent::TorrentState::MissingFiles, tr("Missing Files")},
127           {BitTorrent::TorrentState::Error, tr("Errored", "Torrent status, the torrent has an error")}
128       }
129     , m_stateThemeColors {torrentStateColorsFromUITheme()}
130 {
131     configure();
132     connect(Preferences::instance(), &Preferences::changed, this, &TransferListModel::configure);
133 
134     // Load the torrents
135     using namespace BitTorrent;
136     for (Torrent *const torrent : asConst(Session::instance()->torrents()))
137         addTorrent(torrent);
138 
139     // Listen for torrent changes
140     connect(Session::instance(), &Session::torrentLoaded, this, &TransferListModel::addTorrent);
141     connect(Session::instance(), &Session::torrentAboutToBeRemoved, this, &TransferListModel::handleTorrentAboutToBeRemoved);
142     connect(Session::instance(), &Session::torrentsUpdated, this, &TransferListModel::handleTorrentsUpdated);
143 
144     connect(Session::instance(), &Session::torrentFinished, this, &TransferListModel::handleTorrentStatusUpdated);
145     connect(Session::instance(), &Session::torrentMetadataReceived, this, &TransferListModel::handleTorrentStatusUpdated);
146     connect(Session::instance(), &Session::torrentResumed, this, &TransferListModel::handleTorrentStatusUpdated);
147     connect(Session::instance(), &Session::torrentPaused, this, &TransferListModel::handleTorrentStatusUpdated);
148     connect(Session::instance(), &Session::torrentFinishedChecking, this, &TransferListModel::handleTorrentStatusUpdated);
149 }
150 
rowCount(const QModelIndex &) const151 int TransferListModel::rowCount(const QModelIndex &) const
152 {
153     return m_torrentList.size();
154 }
155 
columnCount(const QModelIndex &) const156 int TransferListModel::columnCount(const QModelIndex &) const
157 {
158     return NB_COLUMNS;
159 }
160 
headerData(int section,Qt::Orientation orientation,int role) const161 QVariant TransferListModel::headerData(int section, Qt::Orientation orientation, int role) const
162 {
163     if (orientation == Qt::Horizontal)
164     {
165         if (role == Qt::DisplayRole)
166         {
167             switch (section)
168             {
169             case TR_QUEUE_POSITION: return QChar('#');
170             case TR_NAME: return tr("Name", "i.e: torrent name");
171             case TR_SIZE: return tr("Size", "i.e: torrent size");
172             case TR_PROGRESS: return tr("Progress", "% Done");
173             case TR_STATUS: return tr("Status", "Torrent status (e.g. downloading, seeding, paused)");
174             case TR_SEEDS: return tr("Seeds", "i.e. full sources (often untranslated)");
175             case TR_PEERS: return tr("Peers", "i.e. partial sources (often untranslated)");
176             case TR_DLSPEED: return tr("Down Speed", "i.e: Download speed");
177             case TR_UPSPEED: return tr("Up Speed", "i.e: Upload speed");
178             case TR_RATIO: return tr("Ratio", "Share ratio");
179             case TR_ETA: return tr("ETA", "i.e: Estimated Time of Arrival / Time left");
180             case TR_CATEGORY: return tr("Category");
181             case TR_TAGS: return tr("Tags");
182             case TR_ADD_DATE: return tr("Added On", "Torrent was added to transfer list on 01/01/2010 08:00");
183             case TR_SEED_DATE: return tr("Completed On", "Torrent was completed on 01/01/2010 08:00");
184             case TR_TRACKER: return tr("Tracker");
185             case TR_DLLIMIT: return tr("Down Limit", "i.e: Download limit");
186             case TR_UPLIMIT: return tr("Up Limit", "i.e: Upload limit");
187             case TR_AMOUNT_DOWNLOADED: return tr("Downloaded", "Amount of data downloaded (e.g. in MB)");
188             case TR_AMOUNT_UPLOADED: return tr("Uploaded", "Amount of data uploaded (e.g. in MB)");
189             case TR_AMOUNT_DOWNLOADED_SESSION: return tr("Session Download", "Amount of data downloaded since program open (e.g. in MB)");
190             case TR_AMOUNT_UPLOADED_SESSION: return tr("Session Upload", "Amount of data uploaded since program open (e.g. in MB)");
191             case TR_AMOUNT_LEFT: return tr("Remaining", "Amount of data left to download (e.g. in MB)");
192             case TR_TIME_ELAPSED: return tr("Time Active", "Time (duration) the torrent is active (not paused)");
193             case TR_SAVE_PATH: return tr("Save path", "Torrent save path");
194             case TR_COMPLETED: return tr("Completed", "Amount of data completed (e.g. in MB)");
195             case TR_RATIO_LIMIT: return tr("Ratio Limit", "Upload share ratio limit");
196             case TR_SEEN_COMPLETE_DATE: return tr("Last Seen Complete", "Indicates the time when the torrent was last seen complete/whole");
197             case TR_LAST_ACTIVITY: return tr("Last Activity", "Time passed since a chunk was downloaded/uploaded");
198             case TR_TOTAL_SIZE: return tr("Total Size", "i.e. Size including unwanted data");
199             case TR_AVAILABILITY: return tr("Availability", "The number of distributed copies of the torrent");
200             default: return {};
201             }
202         }
203         else if (role == Qt::TextAlignmentRole)
204         {
205             switch (section)
206             {
207             case TR_AMOUNT_DOWNLOADED:
208             case TR_AMOUNT_UPLOADED:
209             case TR_AMOUNT_DOWNLOADED_SESSION:
210             case TR_AMOUNT_UPLOADED_SESSION:
211             case TR_AMOUNT_LEFT:
212             case TR_COMPLETED:
213             case TR_SIZE:
214             case TR_TOTAL_SIZE:
215             case TR_ETA:
216             case TR_SEEDS:
217             case TR_PEERS:
218             case TR_UPSPEED:
219             case TR_DLSPEED:
220             case TR_UPLIMIT:
221             case TR_DLLIMIT:
222             case TR_RATIO_LIMIT:
223             case TR_RATIO:
224             case TR_QUEUE_POSITION:
225             case TR_LAST_ACTIVITY:
226             case TR_AVAILABILITY:
227                 return QVariant(Qt::AlignRight | Qt::AlignVCenter);
228             default:
229                 return QAbstractListModel::headerData(section, orientation, role);
230             }
231         }
232     }
233 
234     return {};
235 }
236 
displayValue(const BitTorrent::Torrent * torrent,const int column) const237 QString TransferListModel::displayValue(const BitTorrent::Torrent *torrent, const int column) const
238 {
239     bool hideValues = false;
240     if (m_hideZeroValuesMode == HideZeroValuesMode::Always)
241         hideValues = true;
242     else if (m_hideZeroValuesMode == HideZeroValuesMode::Paused)
243         hideValues = (torrent->state() == BitTorrent::TorrentState::PausedDownloading);
244 
245     const auto availabilityString = [hideValues](const qreal value) -> QString
246     {
247         if (hideValues && (value == 0))
248             return {};
249         return (value >= 0)
250             ? Utils::String::fromDouble(value, 3)
251             : tr("N/A");
252     };
253 
254     const auto unitString = [hideValues](const qint64 value, const bool isSpeedUnit = false) -> QString
255     {
256         return (hideValues && (value == 0))
257                 ? QString {} : Utils::Misc::friendlyUnit(value, isSpeedUnit);
258     };
259 
260     const auto limitString = [hideValues](const qint64 value) -> QString
261     {
262         if (hideValues && (value <= 0))
263             return {};
264 
265         return (value > 0)
266                 ? Utils::Misc::friendlyUnit(value, true)
267                 : QString::fromUtf8(C_INFINITY);
268     };
269 
270     const auto amountString = [hideValues](const qint64 value, const qint64 total) -> QString
271     {
272         if (hideValues && (value == 0) && (total == 0))
273             return {};
274         return QString::fromLatin1("%1 (%2)").arg(QString::number(value), QString::number(total));
275     };
276 
277     const auto etaString = [hideValues](const qlonglong value) -> QString
278     {
279         if (hideValues && (value >= MAX_ETA))
280             return {};
281         return Utils::Misc::userFriendlyDuration(value, MAX_ETA);
282     };
283 
284     const auto ratioString = [hideValues](const qreal value) -> QString
285     {
286         if (hideValues && (value <= 0))
287             return {};
288 
289          return ((static_cast<int>(value) == -1) || (value > BitTorrent::Torrent::MAX_RATIO))
290                   ? QString::fromUtf8(C_INFINITY) : Utils::String::fromDouble(value, 2);
291     };
292 
293     const auto queuePositionString = [](const qint64 value) -> QString
294     {
295         return (value >= 0) ? QString::number(value + 1) : QLatin1String("*");
296     };
297 
298     const auto lastActivityString = [hideValues](qint64 value) -> QString
299     {
300         if (hideValues && ((value < 0) || (value >= MAX_ETA)))
301             return {};
302 
303         // Show '< 1m ago' when elapsed time is 0
304         if (value == 0)
305             value = 1;
306 
307         return (value >= 0)
308             ? tr("%1 ago", "e.g.: 1h 20m ago").arg(Utils::Misc::userFriendlyDuration(value))
309             : Utils::Misc::userFriendlyDuration(value);
310     };
311 
312     const auto timeElapsedString = [hideValues](const qint64 elapsedTime, const qint64 seedingTime) -> QString
313     {
314         if (seedingTime <= 0)
315         {
316             if (hideValues && (elapsedTime == 0))
317                 return {};
318             return Utils::Misc::userFriendlyDuration(elapsedTime);
319         }
320 
321         return tr("%1 (seeded for %2)", "e.g. 4m39s (seeded for 3m10s)")
322                 .arg(Utils::Misc::userFriendlyDuration(elapsedTime)
323                      , Utils::Misc::userFriendlyDuration(seedingTime));
324     };
325 
326     const auto tagsString = [](const QSet<QString> &tags) -> QString
327     {
328         QStringList tagsList = tags.values();
329         tagsList.sort();
330         return tagsList.join(", ");
331     };
332 
333     const auto progressString = [](const qreal progress) -> QString
334     {
335         return (progress >= 1)
336                 ? QString::fromLatin1("100%")
337                 : Utils::String::fromDouble((progress * 100), 1) + QLatin1Char('%');
338     };
339 
340     const auto statusString = [this](const BitTorrent::TorrentState state, const QString &errorMessage) -> QString
341     {
342         return (state == BitTorrent::TorrentState::Error)
343                    ? m_statusStrings[state] + ": " + errorMessage
344                    : m_statusStrings[state];
345     };
346 
347     switch (column)
348     {
349     case TR_NAME:
350         return torrent->name();
351     case TR_QUEUE_POSITION:
352         return queuePositionString(torrent->queuePosition());
353     case TR_SIZE:
354         return unitString(torrent->wantedSize());
355     case TR_PROGRESS:
356         return progressString(torrent->progress());
357     case TR_STATUS:
358         return statusString(torrent->state(), torrent->error());
359     case TR_SEEDS:
360         return amountString(torrent->seedsCount(), torrent->totalSeedsCount());
361     case TR_PEERS:
362         return amountString(torrent->leechsCount(), torrent->totalLeechersCount());
363     case TR_DLSPEED:
364         return unitString(torrent->downloadPayloadRate(), true);
365     case TR_UPSPEED:
366         return unitString(torrent->uploadPayloadRate(), true);
367     case TR_ETA:
368         return etaString(torrent->eta());
369     case TR_RATIO:
370         return ratioString(torrent->realRatio());
371     case TR_RATIO_LIMIT:
372         return ratioString(torrent->maxRatio());
373     case TR_CATEGORY:
374         return torrent->category();
375     case TR_TAGS:
376         return tagsString(torrent->tags());
377     case TR_ADD_DATE:
378         return QLocale().toString(torrent->addedTime().toLocalTime(), QLocale::ShortFormat);
379     case TR_SEED_DATE:
380         return QLocale().toString(torrent->completedTime().toLocalTime(), QLocale::ShortFormat);
381     case TR_TRACKER:
382         return torrent->currentTracker();
383     case TR_DLLIMIT:
384         return limitString(torrent->downloadLimit());
385     case TR_UPLIMIT:
386         return limitString(torrent->uploadLimit());
387     case TR_AMOUNT_DOWNLOADED:
388         return unitString(torrent->totalDownload());
389     case TR_AMOUNT_UPLOADED:
390         return unitString(torrent->totalUpload());
391     case TR_AMOUNT_DOWNLOADED_SESSION:
392         return unitString(torrent->totalPayloadDownload());
393     case TR_AMOUNT_UPLOADED_SESSION:
394         return unitString(torrent->totalPayloadUpload());
395     case TR_AMOUNT_LEFT:
396         return unitString(torrent->remainingSize());
397     case TR_TIME_ELAPSED:
398         return timeElapsedString(torrent->activeTime(), torrent->seedingTime());
399     case TR_SAVE_PATH:
400         return Utils::Fs::toNativePath(torrent->savePath());
401     case TR_COMPLETED:
402         return unitString(torrent->completedSize());
403     case TR_SEEN_COMPLETE_DATE:
404         return QLocale().toString(torrent->lastSeenComplete().toLocalTime(), QLocale::ShortFormat);
405     case TR_LAST_ACTIVITY:
406         return lastActivityString((torrent->isPaused() || torrent->isChecking()) ? -1 : torrent->timeSinceActivity());
407     case TR_AVAILABILITY:
408         return availabilityString(torrent->distributedCopies());
409     case TR_TOTAL_SIZE:
410         return unitString(torrent->totalSize());
411     }
412 
413     return {};
414 }
415 
internalValue(const BitTorrent::Torrent * torrent,const int column,const bool alt) const416 QVariant TransferListModel::internalValue(const BitTorrent::Torrent *torrent, const int column, const bool alt) const
417 {
418     switch (column)
419     {
420     case TR_NAME:
421         return torrent->name();
422     case TR_QUEUE_POSITION:
423         return torrent->queuePosition();
424     case TR_SIZE:
425         return torrent->wantedSize();
426     case TR_PROGRESS:
427         return torrent->progress() * 100;
428     case TR_STATUS:
429         return QVariant::fromValue(torrent->state());
430     case TR_SEEDS:
431         return !alt ? torrent->seedsCount() : torrent->totalSeedsCount();
432     case TR_PEERS:
433         return !alt ? torrent->leechsCount() : torrent->totalLeechersCount();
434     case TR_DLSPEED:
435         return torrent->downloadPayloadRate();
436     case TR_UPSPEED:
437         return torrent->uploadPayloadRate();
438     case TR_ETA:
439         return torrent->eta();
440     case TR_RATIO:
441         return torrent->realRatio();
442     case TR_CATEGORY:
443         return torrent->category();
444     case TR_TAGS:
445         return QStringList {torrent->tags().values()};
446     case TR_ADD_DATE:
447         return torrent->addedTime();
448     case TR_SEED_DATE:
449         return torrent->completedTime();
450     case TR_TRACKER:
451         return torrent->currentTracker();
452     case TR_DLLIMIT:
453         return torrent->downloadLimit();
454     case TR_UPLIMIT:
455         return torrent->uploadLimit();
456     case TR_AMOUNT_DOWNLOADED:
457         return torrent->totalDownload();
458     case TR_AMOUNT_UPLOADED:
459         return torrent->totalUpload();
460     case TR_AMOUNT_DOWNLOADED_SESSION:
461         return torrent->totalPayloadDownload();
462     case TR_AMOUNT_UPLOADED_SESSION:
463         return torrent->totalPayloadUpload();
464     case TR_AMOUNT_LEFT:
465         return torrent->remainingSize();
466     case TR_TIME_ELAPSED:
467         return !alt ? torrent->activeTime() : torrent->seedingTime();
468     case TR_SAVE_PATH:
469         return Utils::Fs::toNativePath(torrent->savePath());
470     case TR_COMPLETED:
471         return torrent->completedSize();
472     case TR_RATIO_LIMIT:
473         return torrent->maxRatio();
474     case TR_SEEN_COMPLETE_DATE:
475         return torrent->lastSeenComplete();
476     case TR_LAST_ACTIVITY:
477         return (torrent->isPaused() || torrent->isChecking()) ? -1 : torrent->timeSinceActivity();
478     case TR_AVAILABILITY:
479         return torrent->distributedCopies();
480     case TR_TOTAL_SIZE:
481         return torrent->totalSize();
482     }
483 
484     return {};
485 }
486 
data(const QModelIndex & index,const int role) const487 QVariant TransferListModel::data(const QModelIndex &index, const int role) const
488 {
489     if (!index.isValid()) return {};
490 
491     const BitTorrent::Torrent *torrent = m_torrentList.value(index.row());
492     if (!torrent) return {};
493 
494     switch (role)
495     {
496     case Qt::ForegroundRole:
497         return m_stateThemeColors.value(torrent->state(), getDefaultColorByState(torrent->state()));
498     case Qt::DisplayRole:
499         return displayValue(torrent, index.column());
500     case UnderlyingDataRole:
501         return internalValue(torrent, index.column(), false);
502     case AdditionalUnderlyingDataRole:
503         return internalValue(torrent, index.column(), true);
504     case Qt::DecorationRole:
505         if (index.column() == TR_NAME)
506             return getIconByState(torrent->state());
507         break;
508     case Qt::ToolTipRole:
509         switch (index.column())
510         {
511         case TR_NAME:
512         case TR_STATUS:
513         case TR_CATEGORY:
514         case TR_TAGS:
515         case TR_TRACKER:
516         case TR_SAVE_PATH:
517             return displayValue(torrent, index.column());
518         }
519         break;
520     case Qt::TextAlignmentRole:
521         switch (index.column())
522         {
523         case TR_AMOUNT_DOWNLOADED:
524         case TR_AMOUNT_UPLOADED:
525         case TR_AMOUNT_DOWNLOADED_SESSION:
526         case TR_AMOUNT_UPLOADED_SESSION:
527         case TR_AMOUNT_LEFT:
528         case TR_COMPLETED:
529         case TR_SIZE:
530         case TR_TOTAL_SIZE:
531         case TR_ETA:
532         case TR_SEEDS:
533         case TR_PEERS:
534         case TR_UPSPEED:
535         case TR_DLSPEED:
536         case TR_UPLIMIT:
537         case TR_DLLIMIT:
538         case TR_RATIO_LIMIT:
539         case TR_RATIO:
540         case TR_QUEUE_POSITION:
541         case TR_LAST_ACTIVITY:
542         case TR_AVAILABILITY:
543             return QVariant {Qt::AlignRight | Qt::AlignVCenter};
544         }
545     }
546 
547     return {};
548 }
549 
setData(const QModelIndex & index,const QVariant & value,int role)550 bool TransferListModel::setData(const QModelIndex &index, const QVariant &value, int role)
551 {
552     if (!index.isValid() || (role != Qt::DisplayRole)) return false;
553 
554     BitTorrent::Torrent *const torrent = m_torrentList.value(index.row());
555     if (!torrent) return false;
556 
557     // Category and Name columns can be edited
558     switch (index.column())
559     {
560     case TR_NAME:
561         torrent->setName(value.toString());
562         break;
563     case TR_CATEGORY:
564         torrent->setCategory(value.toString());
565         break;
566     default:
567         return false;
568     }
569 
570     return true;
571 }
572 
addTorrent(BitTorrent::Torrent * const torrent)573 void TransferListModel::addTorrent(BitTorrent::Torrent *const torrent)
574 {
575     Q_ASSERT(!m_torrentMap.contains(torrent));
576 
577     const int row = m_torrentList.size();
578 
579     beginInsertRows({}, row, row);
580     m_torrentList << torrent;
581     m_torrentMap[torrent] = row;
582     endInsertRows();
583 }
584 
flags(const QModelIndex & index) const585 Qt::ItemFlags TransferListModel::flags(const QModelIndex &index) const
586 {
587     if (!index.isValid()) return Qt::NoItemFlags;
588 
589     // Explicitly mark as editable
590     return QAbstractListModel::flags(index) | Qt::ItemIsEditable;
591 }
592 
torrentHandle(const QModelIndex & index) const593 BitTorrent::Torrent *TransferListModel::torrentHandle(const QModelIndex &index) const
594 {
595     if (!index.isValid()) return nullptr;
596 
597     return m_torrentList.value(index.row());
598 }
599 
handleTorrentAboutToBeRemoved(BitTorrent::Torrent * const torrent)600 void TransferListModel::handleTorrentAboutToBeRemoved(BitTorrent::Torrent *const torrent)
601 {
602     const int row = m_torrentMap.value(torrent, -1);
603     Q_ASSERT(row >= 0);
604 
605     beginRemoveRows({}, row, row);
606     m_torrentList.removeAt(row);
607     m_torrentMap.remove(torrent);
608     for (int &value : m_torrentMap)
609     {
610         if (value > row)
611             --value;
612     }
613     endRemoveRows();
614 }
615 
handleTorrentStatusUpdated(BitTorrent::Torrent * const torrent)616 void TransferListModel::handleTorrentStatusUpdated(BitTorrent::Torrent *const torrent)
617 {
618     const int row = m_torrentMap.value(torrent, -1);
619     Q_ASSERT(row >= 0);
620 
621     emit dataChanged(index(row, 0), index(row, columnCount() - 1));
622 }
623 
handleTorrentsUpdated(const QVector<BitTorrent::Torrent * > & torrents)624 void TransferListModel::handleTorrentsUpdated(const QVector<BitTorrent::Torrent *> &torrents)
625 {
626     const int columns = (columnCount() - 1);
627 
628     if (torrents.size() <= (m_torrentList.size() * 0.5))
629     {
630         for (BitTorrent::Torrent *const torrent : torrents)
631         {
632             const int row = m_torrentMap.value(torrent, -1);
633             Q_ASSERT(row >= 0);
634 
635             emit dataChanged(index(row, 0), index(row, columns));
636         }
637     }
638     else
639     {
640         // save the overhead when more than half of the torrent list needs update
641         emit dataChanged(index(0, 0), index((rowCount() - 1), columns));
642     }
643 }
644 
configure()645 void TransferListModel::configure()
646 {
647     const Preferences *pref = Preferences::instance();
648 
649     HideZeroValuesMode hideZeroValuesMode = HideZeroValuesMode::Never;
650     if (pref->getHideZeroValues())
651     {
652         if (pref->getHideZeroComboValues() == 1)
653             hideZeroValuesMode = HideZeroValuesMode::Paused;
654         else
655             hideZeroValuesMode = HideZeroValuesMode::Always;
656     }
657 
658     if (m_hideZeroValuesMode != hideZeroValuesMode)
659     {
660         m_hideZeroValuesMode = hideZeroValuesMode;
661         emit dataChanged(index(0, 0), index((rowCount() - 1), (columnCount() - 1)));
662     }
663 }
664 
665 // Static functions
666 
getIconByState(const BitTorrent::TorrentState state)667 QIcon getIconByState(const BitTorrent::TorrentState state)
668 {
669     switch (state)
670     {
671     case BitTorrent::TorrentState::Downloading:
672     case BitTorrent::TorrentState::ForcedDownloading:
673     case BitTorrent::TorrentState::DownloadingMetadata:
674         return getDownloadingIcon();
675     case BitTorrent::TorrentState::StalledDownloading:
676         return getStalledDownloadingIcon();
677     case BitTorrent::TorrentState::StalledUploading:
678         return getStalledUploadingIcon();
679     case BitTorrent::TorrentState::Uploading:
680     case BitTorrent::TorrentState::ForcedUploading:
681         return getUploadingIcon();
682     case BitTorrent::TorrentState::PausedDownloading:
683         return getPausedIcon();
684     case BitTorrent::TorrentState::PausedUploading:
685         return getCompletedIcon();
686     case BitTorrent::TorrentState::QueuedDownloading:
687     case BitTorrent::TorrentState::QueuedUploading:
688         return getQueuedIcon();
689     case BitTorrent::TorrentState::CheckingDownloading:
690     case BitTorrent::TorrentState::CheckingUploading:
691     case BitTorrent::TorrentState::CheckingResumeData:
692     case BitTorrent::TorrentState::Moving:
693         return getCheckingIcon();
694     case BitTorrent::TorrentState::Unknown:
695     case BitTorrent::TorrentState::MissingFiles:
696     case BitTorrent::TorrentState::Error:
697         return getErrorIcon();
698     default:
699         Q_ASSERT(false);
700         return getErrorIcon();
701     }
702 }
703 
getDefaultColorByState(const BitTorrent::TorrentState state)704 QColor getDefaultColorByState(const BitTorrent::TorrentState state)
705 {
706     // Color names taken from http://cloford.com/resources/colours/500col.htm
707     bool dark = isDarkTheme();
708 
709     switch (state)
710     {
711     case BitTorrent::TorrentState::Downloading:
712     case BitTorrent::TorrentState::ForcedDownloading:
713     case BitTorrent::TorrentState::DownloadingMetadata:
714         if (!dark)
715             return {34, 139, 34}; // Forest Green
716         else
717             return {50, 205, 50}; // Lime Green
718     case BitTorrent::TorrentState::StalledDownloading:
719     case BitTorrent::TorrentState::StalledUploading:
720         if (!dark)
721             return {0, 0, 0}; // Black
722         else
723             return {204, 204, 204}; // Gray 80
724     case BitTorrent::TorrentState::Uploading:
725     case BitTorrent::TorrentState::ForcedUploading:
726         if (!dark)
727             return {65, 105, 225}; // Royal Blue
728         else
729             return {99, 184, 255}; // Steel Blue 1
730     case BitTorrent::TorrentState::PausedDownloading:
731         return {250, 128, 114}; // Salmon
732     case BitTorrent::TorrentState::PausedUploading:
733         if (!dark)
734             return {0, 0, 139}; // Dark Blue
735         else
736             return {79, 148, 205}; // Steel Blue 3
737     case BitTorrent::TorrentState::Error:
738     case BitTorrent::TorrentState::MissingFiles:
739         return {255, 0, 0}; // red
740     case BitTorrent::TorrentState::QueuedDownloading:
741     case BitTorrent::TorrentState::QueuedUploading:
742     case BitTorrent::TorrentState::CheckingDownloading:
743     case BitTorrent::TorrentState::CheckingUploading:
744     case BitTorrent::TorrentState::CheckingResumeData:
745     case BitTorrent::TorrentState::Moving:
746         if (!dark)
747             return {0, 128, 128}; // Teal
748         else
749             return {0, 205, 205}; // Cyan 3
750     case BitTorrent::TorrentState::Unknown:
751         return {255, 0, 0}; // red
752     default:
753         Q_ASSERT(false);
754         return {255, 0, 0}; // red
755     }
756 }
757 
getPausedIcon()758 QIcon getPausedIcon()
759 {
760     static QIcon cached = UIThemeManager::instance()->getIcon(QLatin1String("paused"));
761     return cached;
762 }
763 
getQueuedIcon()764 QIcon getQueuedIcon()
765 {
766     static QIcon cached = UIThemeManager::instance()->getIcon(QLatin1String("queued"));
767     return cached;
768 }
769 
getDownloadingIcon()770 QIcon getDownloadingIcon()
771 {
772     static QIcon cached = UIThemeManager::instance()->getIcon(QLatin1String("downloading"));
773     return cached;
774 }
775 
getStalledDownloadingIcon()776 QIcon getStalledDownloadingIcon()
777 {
778     static QIcon cached = UIThemeManager::instance()->getIcon(QLatin1String("stalledDL"));
779     return cached;
780 }
781 
getUploadingIcon()782 QIcon getUploadingIcon()
783 {
784     static QIcon cached = UIThemeManager::instance()->getIcon(QLatin1String("uploading"));
785     return cached;
786 }
787 
getStalledUploadingIcon()788 QIcon getStalledUploadingIcon()
789 {
790     static QIcon cached = UIThemeManager::instance()->getIcon(QLatin1String("stalledUP"));
791     return cached;
792 }
793 
getCompletedIcon()794 QIcon getCompletedIcon()
795 {
796     static QIcon cached = UIThemeManager::instance()->getIcon(QLatin1String("completed"));
797     return cached;
798 }
799 
getCheckingIcon()800 QIcon getCheckingIcon()
801 {
802     static QIcon cached = UIThemeManager::instance()->getIcon(QLatin1String("checking"));
803     return cached;
804 }
805 
getErrorIcon()806 QIcon getErrorIcon()
807 {
808     static QIcon cached = UIThemeManager::instance()->getIcon(QLatin1String("error"));
809     return cached;
810 }
811 
isDarkTheme()812 bool isDarkTheme()
813 {
814     const QPalette pal = QApplication::palette();
815     // QPalette::Base is used for the background of the Treeview
816     const QColor &color = pal.color(QPalette::Active, QPalette::Base);
817     return (color.lightness() < 127);
818 }
819