1 /*
2  * Bittorrent Client using Qt and libtorrent.
3  * Copyright (C) 2006  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 "transferlistfilterswidget.h"
30 
31 #include <QCheckBox>
32 #include <QIcon>
33 #include <QListWidgetItem>
34 #include <QMenu>
35 #include <QPainter>
36 #include <QScrollArea>
37 #include <QStyleOptionButton>
38 #include <QUrl>
39 #include <QVBoxLayout>
40 
41 #include "base/bittorrent/infohash.h"
42 #include "base/bittorrent/session.h"
43 #include "base/bittorrent/torrent.h"
44 #include "base/bittorrent/trackerentry.h"
45 #include "base/global.h"
46 #include "base/logger.h"
47 #include "base/net/downloadmanager.h"
48 #include "base/preferences.h"
49 #include "base/torrentfilter.h"
50 #include "base/utils/fs.h"
51 #include "base/utils/string.h"
52 #include "categoryfilterwidget.h"
53 #include "tagfilterwidget.h"
54 #include "transferlistwidget.h"
55 #include "uithememanager.h"
56 #include "utils.h"
57 
58 namespace
59 {
60     enum TRACKER_FILTER_ROW
61     {
62         ALL_ROW,
63         TRACKERLESS_ROW,
64         ERROR_ROW,
65         WARNING_ROW
66     };
67 
getScheme(const QString & tracker)68     QString getScheme(const QString &tracker)
69     {
70         const QUrl url {tracker};
71         QString scheme = url.scheme();
72         if (scheme.isEmpty())
73             scheme = "http";
74         return scheme;
75     }
76 
getHost(const QString & tracker)77     QString getHost(const QString &tracker)
78     {
79         // We want the domain + tld. Subdomains should be disregarded
80         const QUrl url {tracker};
81         const QString host {url.host()};
82 
83         // host is in IP format
84         if (!QHostAddress(host).isNull())
85             return host;
86 
87         return host.section('.', -2, -1);
88     }
89 
90     class ArrowCheckBox final : public QCheckBox
91     {
92     public:
93         using QCheckBox::QCheckBox;
94 
95     private:
paintEvent(QPaintEvent *)96         void paintEvent(QPaintEvent *) override
97         {
98             QPainter painter(this);
99 
100             QStyleOptionViewItem indicatorOption;
101             indicatorOption.initFrom(this);
102             indicatorOption.rect = style()->subElementRect(QStyle::SE_CheckBoxIndicator, &indicatorOption, this);
103             indicatorOption.state |= (QStyle::State_Children
104                                       | (isChecked() ? QStyle::State_Open : QStyle::State_None));
105             style()->drawPrimitive(QStyle::PE_IndicatorBranch, &indicatorOption, &painter, this);
106 
107             QStyleOptionButton labelOption;
108             initStyleOption(&labelOption);
109             labelOption.rect = style()->subElementRect(QStyle::SE_CheckBoxContents, &labelOption, this);
110             style()->drawControl(QStyle::CE_CheckBoxLabel, &labelOption, &painter, this);
111         }
112     };
113 
114     const QString NULL_HOST {""};
115 }
116 
BaseFilterWidget(QWidget * parent,TransferListWidget * transferList)117 BaseFilterWidget::BaseFilterWidget(QWidget *parent, TransferListWidget *transferList)
118     : QListWidget(parent)
119     , transferList(transferList)
120 {
121     setFrameShape(QFrame::NoFrame);
122     setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
123     setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
124     setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
125     setUniformItemSizes(true);
126     setSpacing(0);
127 
128     setIconSize(Utils::Gui::smallIconSize());
129 
130 #if defined(Q_OS_MACOS)
131     setAttribute(Qt::WA_MacShowFocusRect, false);
132 #endif
133 
134     setContextMenuPolicy(Qt::CustomContextMenu);
135     connect(this, &BaseFilterWidget::customContextMenuRequested, this, &BaseFilterWidget::showMenu);
136     connect(this, &BaseFilterWidget::currentRowChanged, this, &BaseFilterWidget::applyFilter);
137 
138     connect(BitTorrent::Session::instance(), &BitTorrent::Session::torrentLoaded
139             , this, &BaseFilterWidget::handleNewTorrent);
140     connect(BitTorrent::Session::instance(), &BitTorrent::Session::torrentAboutToBeRemoved
141             , this, &BaseFilterWidget::torrentAboutToBeDeleted);
142 }
143 
sizeHint() const144 QSize BaseFilterWidget::sizeHint() const
145 {
146     return
147     {
148         // Width should be exactly the width of the content
149         sizeHintForColumn(0),
150         // Height should be exactly the height of the content
151         static_cast<int>((sizeHintForRow(0) + 2 * spacing()) * (count() + 0.5)),
152     };
153 }
154 
minimumSizeHint() const155 QSize BaseFilterWidget::minimumSizeHint() const
156 {
157     QSize size = sizeHint();
158     size.setWidth(6);
159     return size;
160 }
161 
toggleFilter(bool checked)162 void BaseFilterWidget::toggleFilter(bool checked)
163 {
164     setVisible(checked);
165     if (checked)
166         applyFilter(currentRow());
167     else
168         applyFilter(ALL_ROW);
169 }
170 
StatusFilterWidget(QWidget * parent,TransferListWidget * transferList)171 StatusFilterWidget::StatusFilterWidget(QWidget *parent, TransferListWidget *transferList)
172     : BaseFilterWidget(parent, transferList)
173 {
174     connect(BitTorrent::Session::instance(), &BitTorrent::Session::torrentLoaded
175             , this, &StatusFilterWidget::updateTorrentNumbers);
176     connect(BitTorrent::Session::instance(), &BitTorrent::Session::torrentsUpdated
177             , this, &StatusFilterWidget::updateTorrentNumbers);
178     connect(BitTorrent::Session::instance(), &BitTorrent::Session::torrentAboutToBeRemoved
179             , this, &StatusFilterWidget::updateTorrentNumbers);
180 
181     // Add status filters
182     auto *all = new QListWidgetItem(this);
183     all->setData(Qt::DisplayRole, tr("All (0)", "this is for the status filter"));
184     all->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(QLatin1String("filterall")));
185     auto *downloading = new QListWidgetItem(this);
186     downloading->setData(Qt::DisplayRole, tr("Downloading (0)"));
187     downloading->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(QLatin1String("downloading")));
188     auto *seeding = new QListWidgetItem(this);
189     seeding->setData(Qt::DisplayRole, tr("Seeding (0)"));
190     seeding->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(QLatin1String("uploading")));
191     auto *completed = new QListWidgetItem(this);
192     completed->setData(Qt::DisplayRole, tr("Completed (0)"));
193     completed->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(QLatin1String("completed")));
194     auto *resumed = new QListWidgetItem(this);
195     resumed->setData(Qt::DisplayRole, tr("Resumed (0)"));
196     resumed->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(QLatin1String("resumed")));
197     auto *paused = new QListWidgetItem(this);
198     paused->setData(Qt::DisplayRole, tr("Paused (0)"));
199     paused->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(QLatin1String("paused")));
200     auto *active = new QListWidgetItem(this);
201     active->setData(Qt::DisplayRole, tr("Active (0)"));
202     active->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(QLatin1String("filteractive")));
203     auto *inactive = new QListWidgetItem(this);
204     inactive->setData(Qt::DisplayRole, tr("Inactive (0)"));
205     inactive->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(QLatin1String("filterinactive")));
206     auto *stalled = new QListWidgetItem(this);
207     stalled->setData(Qt::DisplayRole, tr("Stalled (0)"));
208     stalled->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(QLatin1String("filterstalled")));
209     auto *stalledUploading = new QListWidgetItem(this);
210     stalledUploading->setData(Qt::DisplayRole, tr("Stalled Uploading (0)"));
211     stalledUploading->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(QLatin1String("stalledUP")));
212     auto *stalledDownloading = new QListWidgetItem(this);
213     stalledDownloading->setData(Qt::DisplayRole, tr("Stalled Downloading (0)"));
214     stalledDownloading->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(QLatin1String("stalledDL")));
215     auto *errored = new QListWidgetItem(this);
216     errored->setData(Qt::DisplayRole, tr("Errored (0)"));
217     errored->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(QLatin1String("error")));
218 
219     const Preferences *const pref = Preferences::instance();
220     setCurrentRow(pref->getTransSelFilter(), QItemSelectionModel::SelectCurrent);
221     toggleFilter(pref->getStatusFilterState());
222 }
223 
~StatusFilterWidget()224 StatusFilterWidget::~StatusFilterWidget()
225 {
226     Preferences::instance()->setTransSelFilter(currentRow());
227 }
228 
updateTorrentNumbers()229 void StatusFilterWidget::updateTorrentNumbers()
230 {
231     int nbDownloading = 0;
232     int nbSeeding = 0;
233     int nbCompleted = 0;
234     int nbResumed = 0;
235     int nbPaused = 0;
236     int nbActive = 0;
237     int nbInactive = 0;
238     int nbStalled = 0;
239     int nbStalledUploading = 0;
240     int nbStalledDownloading = 0;
241     int nbErrored = 0;
242 
243     const QVector<BitTorrent::Torrent *> torrents = BitTorrent::Session::instance()->torrents();
244     for (const BitTorrent::Torrent *torrent : torrents)
245     {
246         if (torrent->isDownloading())
247             ++nbDownloading;
248         if (torrent->isUploading())
249             ++nbSeeding;
250         if (torrent->isCompleted())
251             ++nbCompleted;
252         if (torrent->isResumed())
253             ++nbResumed;
254         if (torrent->isPaused())
255             ++nbPaused;
256         if (torrent->isActive())
257             ++nbActive;
258         if (torrent->isInactive())
259             ++nbInactive;
260         if (torrent->state() ==  BitTorrent::TorrentState::StalledUploading)
261             ++nbStalledUploading;
262         if (torrent->state() ==  BitTorrent::TorrentState::StalledDownloading)
263             ++nbStalledDownloading;
264         if (torrent->isErrored())
265             ++nbErrored;
266     }
267 
268     nbStalled = nbStalledUploading + nbStalledDownloading;
269 
270     item(TorrentFilter::All)->setData(Qt::DisplayRole, tr("All (%1)").arg(torrents.count()));
271     item(TorrentFilter::Downloading)->setData(Qt::DisplayRole, tr("Downloading (%1)").arg(nbDownloading));
272     item(TorrentFilter::Seeding)->setData(Qt::DisplayRole, tr("Seeding (%1)").arg(nbSeeding));
273     item(TorrentFilter::Completed)->setData(Qt::DisplayRole, tr("Completed (%1)").arg(nbCompleted));
274     item(TorrentFilter::Resumed)->setData(Qt::DisplayRole, tr("Resumed (%1)").arg(nbResumed));
275     item(TorrentFilter::Paused)->setData(Qt::DisplayRole, tr("Paused (%1)").arg(nbPaused));
276     item(TorrentFilter::Active)->setData(Qt::DisplayRole, tr("Active (%1)").arg(nbActive));
277     item(TorrentFilter::Inactive)->setData(Qt::DisplayRole, tr("Inactive (%1)").arg(nbInactive));
278     item(TorrentFilter::Stalled)->setData(Qt::DisplayRole, tr("Stalled (%1)").arg(nbStalled));
279     item(TorrentFilter::StalledUploading)->setData(Qt::DisplayRole, tr("Stalled Uploading (%1)").arg(nbStalledUploading));
280     item(TorrentFilter::StalledDownloading)->setData(Qt::DisplayRole, tr("Stalled Downloading (%1)").arg(nbStalledDownloading));
281     item(TorrentFilter::Errored)->setData(Qt::DisplayRole, tr("Errored (%1)").arg(nbErrored));
282 }
283 
showMenu(const QPoint &)284 void StatusFilterWidget::showMenu(const QPoint &) {}
285 
applyFilter(int row)286 void StatusFilterWidget::applyFilter(int row)
287 {
288     transferList->applyStatusFilter(row);
289 }
290 
handleNewTorrent(BitTorrent::Torrent * const)291 void StatusFilterWidget::handleNewTorrent(BitTorrent::Torrent *const) {}
292 
torrentAboutToBeDeleted(BitTorrent::Torrent * const)293 void StatusFilterWidget::torrentAboutToBeDeleted(BitTorrent::Torrent *const) {}
294 
TrackerFiltersList(QWidget * parent,TransferListWidget * transferList,const bool downloadFavicon)295 TrackerFiltersList::TrackerFiltersList(QWidget *parent, TransferListWidget *transferList, const bool downloadFavicon)
296     : BaseFilterWidget(parent, transferList)
297     , m_totalTorrents(0)
298     , m_downloadTrackerFavicon(downloadFavicon)
299 {
300     auto *allTrackers = new QListWidgetItem(this);
301     allTrackers->setData(Qt::DisplayRole, tr("All (0)", "this is for the tracker filter"));
302     allTrackers->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon("network-server"));
303     auto *noTracker = new QListWidgetItem(this);
304     noTracker->setData(Qt::DisplayRole, tr("Trackerless (0)"));
305     noTracker->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon("network-server"));
306     auto *errorTracker = new QListWidgetItem(this);
307     errorTracker->setData(Qt::DisplayRole, tr("Error (0)"));
308     errorTracker->setData(Qt::DecorationRole, style()->standardIcon(QStyle::SP_MessageBoxCritical));
309     auto *warningTracker = new QListWidgetItem(this);
310     warningTracker->setData(Qt::DisplayRole, tr("Warning (0)"));
311     warningTracker->setData(Qt::DecorationRole, style()->standardIcon(QStyle::SP_MessageBoxWarning));
312     m_trackers[NULL_HOST] = {};
313 
314     setCurrentRow(0, QItemSelectionModel::SelectCurrent);
315     toggleFilter(Preferences::instance()->getTrackerFilterState());
316 }
317 
~TrackerFiltersList()318 TrackerFiltersList::~TrackerFiltersList()
319 {
320     for (const QString &iconPath : asConst(m_iconPaths))
321         Utils::Fs::forceRemove(iconPath);
322 }
323 
addItem(const QString & tracker,const BitTorrent::TorrentID & id)324 void TrackerFiltersList::addItem(const QString &tracker, const BitTorrent::TorrentID &id)
325 {
326     const QString host {getHost(tracker)};
327     const bool exists {m_trackers.contains(host)};
328     QListWidgetItem *trackerItem {nullptr};
329 
330     if (exists)
331     {
332         if (m_trackers.value(host).contains(id))
333             return;
334 
335         trackerItem = item((host == NULL_HOST)
336             ? TRACKERLESS_ROW
337             : rowFromTracker(host));
338     }
339     else
340     {
341         trackerItem = new QListWidgetItem();
342         trackerItem->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon("network-server"));
343 
344         const QString scheme = getScheme(tracker);
345         downloadFavicon(QString::fromLatin1("%1://%2/favicon.ico").arg((scheme.startsWith("http") ? scheme : "http"), host));
346     }
347     if (!trackerItem) return;
348 
349     QSet<BitTorrent::TorrentID> &torrentIDs {m_trackers[host]};
350     torrentIDs.insert(id);
351 
352     if (host == NULL_HOST)
353     {
354         trackerItem->setText(tr("Trackerless (%1)").arg(torrentIDs.size()));
355         if (currentRow() == TRACKERLESS_ROW)
356             applyFilter(TRACKERLESS_ROW);
357         return;
358     }
359 
360     trackerItem->setText(QString::fromLatin1("%1 (%2)").arg(host, QString::number(torrentIDs.size())));
361     if (exists)
362     {
363         if (currentRow() == rowFromTracker(host))
364             applyFilter(currentRow());
365         return;
366     }
367 
368     Q_ASSERT(count() >= 4);
369     int insPos = count();
370     for (int i = 4; i < count(); ++i)
371     {
372         if (Utils::String::naturalLessThan<Qt::CaseSensitive>(host, item(i)->text()))
373         {
374             insPos = i;
375             break;
376         }
377     }
378     QListWidget::insertItem(insPos, trackerItem);
379     updateGeometry();
380 }
381 
removeItem(const QString & tracker,const BitTorrent::TorrentID & id)382 void TrackerFiltersList::removeItem(const QString &tracker, const BitTorrent::TorrentID &id)
383 {
384     const QString host = getHost(tracker);
385     QSet<BitTorrent::TorrentID> torrentIDs = m_trackers.value(host);
386 
387     if (torrentIDs.empty())
388         return;
389     torrentIDs.remove(id);
390 
391     int row = 0;
392     QListWidgetItem *trackerItem = nullptr;
393 
394     if (!host.isEmpty())
395     {
396         // Remove from 'Error' and 'Warning' view
397         trackerSuccess(id, tracker);
398         row = rowFromTracker(host);
399         trackerItem = item(row);
400 
401         if (torrentIDs.empty())
402         {
403             if (currentRow() == row)
404                 setCurrentRow(0, QItemSelectionModel::SelectCurrent);
405             delete trackerItem;
406             m_trackers.remove(host);
407             updateGeometry();
408             return;
409         }
410 
411         if (trackerItem)
412             trackerItem->setText(QString::fromLatin1("%1 (%2)").arg(host, QString::number(torrentIDs.size())));
413     }
414     else
415     {
416         row = 1;
417         trackerItem = item(TRACKERLESS_ROW);
418         trackerItem->setText(tr("Trackerless (%1)").arg(torrentIDs.size()));
419     }
420 
421     m_trackers.insert(host, torrentIDs);
422 
423     if (currentRow() == row)
424         applyFilter(row);
425 }
426 
changeTrackerless(const bool trackerless,const BitTorrent::TorrentID & id)427 void TrackerFiltersList::changeTrackerless(const bool trackerless, const BitTorrent::TorrentID &id)
428 {
429     if (trackerless)
430         addItem(NULL_HOST, id);
431     else
432         removeItem(NULL_HOST, id);
433 }
434 
setDownloadTrackerFavicon(bool value)435 void TrackerFiltersList::setDownloadTrackerFavicon(bool value)
436 {
437     if (value == m_downloadTrackerFavicon) return;
438     m_downloadTrackerFavicon = value;
439 
440     if (m_downloadTrackerFavicon)
441     {
442         for (auto i = m_trackers.cbegin(); i != m_trackers.cend(); ++i)
443         {
444             const QString &tracker = i.key();
445             if (!tracker.isEmpty())
446             {
447                 const QString scheme = getScheme(tracker);
448                 downloadFavicon(QString("%1://%2/favicon.ico")
449                                 .arg((scheme.startsWith("http") ? scheme : "http"), getHost(tracker)));
450              }
451         }
452     }
453 }
454 
trackerSuccess(const BitTorrent::TorrentID & id,const QString & tracker)455 void TrackerFiltersList::trackerSuccess(const BitTorrent::TorrentID &id, const QString &tracker)
456 {
457     const auto errorHashesIter = m_errors.find(id);
458     if (errorHashesIter != m_errors.end())
459     {
460         QSet<QString> &errored = *errorHashesIter;
461         errored.remove(tracker);
462         if (errored.empty())
463         {
464             m_errors.erase(errorHashesIter);
465             item(ERROR_ROW)->setText(tr("Error (%1)").arg(m_errors.size()));
466             if (currentRow() == ERROR_ROW)
467                 applyFilter(ERROR_ROW);
468         }
469     }
470 
471     const auto warningHashesIter = m_warnings.find(id);
472     if (warningHashesIter != m_warnings.end())
473     {
474         QSet<QString> &warned = *warningHashesIter;
475         warned.remove(tracker);
476         if (warned.empty())
477         {
478             m_warnings.erase(warningHashesIter);
479             item(WARNING_ROW)->setText(tr("Warning (%1)").arg(m_warnings.size()));
480             if (currentRow() == WARNING_ROW)
481                 applyFilter(WARNING_ROW);
482         }
483     }
484 }
485 
trackerError(const BitTorrent::TorrentID & id,const QString & tracker)486 void TrackerFiltersList::trackerError(const BitTorrent::TorrentID &id, const QString &tracker)
487 {
488     QSet<QString> &trackers {m_errors[id]};
489     if (trackers.contains(tracker))
490         return;
491 
492     trackers.insert(tracker);
493     item(ERROR_ROW)->setText(tr("Error (%1)").arg(m_errors.size()));
494     if (currentRow() == ERROR_ROW)
495         applyFilter(ERROR_ROW);
496 }
497 
trackerWarning(const BitTorrent::TorrentID & id,const QString & tracker)498 void TrackerFiltersList::trackerWarning(const BitTorrent::TorrentID &id, const QString &tracker)
499 {
500     QSet<QString> &trackers {m_warnings[id]};
501     if (trackers.contains(tracker))
502         return;
503 
504     trackers.insert(tracker);
505     item(WARNING_ROW)->setText(tr("Warning (%1)").arg(m_warnings.size()));
506     if (currentRow() == WARNING_ROW)
507         applyFilter(WARNING_ROW);
508 }
509 
downloadFavicon(const QString & url)510 void TrackerFiltersList::downloadFavicon(const QString &url)
511 {
512     if (!m_downloadTrackerFavicon) return;
513     Net::DownloadManager::instance()->download(
514                 Net::DownloadRequest(url).saveToFile(true)
515                 , this, &TrackerFiltersList::handleFavicoDownloadFinished);
516 }
517 
handleFavicoDownloadFinished(const Net::DownloadResult & result)518 void TrackerFiltersList::handleFavicoDownloadFinished(const Net::DownloadResult &result)
519 {
520     if (result.status != Net::DownloadStatus::Success)
521     {
522         if (result.url.endsWith(".ico", Qt::CaseInsensitive))
523             downloadFavicon(result.url.left(result.url.size() - 4) + ".png");
524         return;
525     }
526 
527     const QString host = getHost(result.url);
528 
529     if (!m_trackers.contains(host))
530     {
531         Utils::Fs::forceRemove(result.filePath);
532         return;
533     }
534 
535     QListWidgetItem *trackerItem = item(rowFromTracker(host));
536     if (!trackerItem) return;
537 
538     QIcon icon(result.filePath);
539     //Detect a non-decodable icon
540     QList<QSize> sizes = icon.availableSizes();
541     bool invalid = (sizes.isEmpty() || icon.pixmap(sizes.first()).isNull());
542     if (invalid)
543     {
544         if (result.url.endsWith(".ico", Qt::CaseInsensitive))
545             downloadFavicon(result.url.left(result.url.size() - 4) + ".png");
546         Utils::Fs::forceRemove(result.filePath);
547     }
548     else
549     {
550         trackerItem->setData(Qt::DecorationRole, QIcon(result.filePath));
551         m_iconPaths.append(result.filePath);
552     }
553 }
554 
showMenu(const QPoint &)555 void TrackerFiltersList::showMenu(const QPoint &)
556 {
557     QMenu *menu = new QMenu(this);
558     menu->setAttribute(Qt::WA_DeleteOnClose);
559 
560     menu->addAction(UIThemeManager::instance()->getIcon("media-playback-start"), tr("Resume torrents")
561         , transferList, &TransferListWidget::startVisibleTorrents);
562     menu->addAction(UIThemeManager::instance()->getIcon("media-playback-pause"), tr("Pause torrents")
563         , transferList, &TransferListWidget::pauseVisibleTorrents);
564     menu->addAction(UIThemeManager::instance()->getIcon("edit-delete"), tr("Delete torrents")
565         , transferList, &TransferListWidget::deleteVisibleTorrents);
566 
567     menu->popup(QCursor::pos());
568 }
569 
applyFilter(const int row)570 void TrackerFiltersList::applyFilter(const int row)
571 {
572     if (row == ALL_ROW)
573         transferList->applyTrackerFilterAll();
574     else if (isVisible())
575         transferList->applyTrackerFilter(getTorrentIDs(row));
576 }
577 
handleNewTorrent(BitTorrent::Torrent * const torrent)578 void TrackerFiltersList::handleNewTorrent(BitTorrent::Torrent *const torrent)
579 {
580     const BitTorrent::TorrentID torrentID {torrent->id()};
581     const QVector<BitTorrent::TrackerEntry> trackers {torrent->trackers()};
582     for (const BitTorrent::TrackerEntry &tracker : trackers)
583         addItem(tracker.url, torrentID);
584 
585     // Check for trackerless torrent
586     if (trackers.isEmpty())
587         addItem(NULL_HOST, torrentID);
588 
589     item(ALL_ROW)->setText(tr("All (%1)", "this is for the tracker filter").arg(++m_totalTorrents));
590 }
591 
torrentAboutToBeDeleted(BitTorrent::Torrent * const torrent)592 void TrackerFiltersList::torrentAboutToBeDeleted(BitTorrent::Torrent *const torrent)
593 {
594     const BitTorrent::TorrentID torrentID {torrent->id()};
595     const QVector<BitTorrent::TrackerEntry> trackers {torrent->trackers()};
596     for (const BitTorrent::TrackerEntry &tracker : trackers)
597         removeItem(tracker.url, torrentID);
598 
599     // Check for trackerless torrent
600     if (trackers.isEmpty())
601         removeItem(NULL_HOST, torrentID);
602 
603     item(ALL_ROW)->setText(tr("All (%1)", "this is for the tracker filter").arg(--m_totalTorrents));
604 }
605 
trackerFromRow(int row) const606 QString TrackerFiltersList::trackerFromRow(int row) const
607 {
608     Q_ASSERT(row > 1);
609     const QString tracker = item(row)->text();
610     QStringList parts = tracker.split(' ');
611     Q_ASSERT(parts.size() >= 2);
612     parts.removeLast(); // Remove trailing number
613     return parts.join(' ');
614 }
615 
rowFromTracker(const QString & tracker) const616 int TrackerFiltersList::rowFromTracker(const QString &tracker) const
617 {
618     Q_ASSERT(!tracker.isEmpty());
619     for (int i = 4; i < count(); ++i)
620     {
621         if (tracker == trackerFromRow(i))
622             return i;
623     }
624     return -1;
625 }
626 
getTorrentIDs(const int row) const627 QSet<BitTorrent::TorrentID> TrackerFiltersList::getTorrentIDs(const int row) const
628 {
629     switch (row)
630     {
631     case TRACKERLESS_ROW:
632         return m_trackers.value(NULL_HOST);
633     case ERROR_ROW:
634         return List::toSet(m_errors.keys());
635     case WARNING_ROW:
636         return List::toSet(m_warnings.keys());
637     default:
638         return m_trackers.value(trackerFromRow(row));
639     }
640 }
641 
TransferListFiltersWidget(QWidget * parent,TransferListWidget * transferList,const bool downloadFavicon)642 TransferListFiltersWidget::TransferListFiltersWidget(QWidget *parent, TransferListWidget *transferList, const bool downloadFavicon)
643     : QFrame(parent)
644     , m_transferList(transferList)
645 {
646     Preferences *const pref = Preferences::instance();
647 
648     // Construct lists
649     auto *vLayout = new QVBoxLayout(this);
650     auto *scroll = new QScrollArea(this);
651     QFrame *frame = new QFrame(scroll);
652     auto *frameLayout = new QVBoxLayout(frame);
653     QFont font;
654     font.setBold(true);
655     font.setCapitalization(QFont::AllUppercase);
656 
657     scroll->setWidgetResizable(true);
658     scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
659 
660     setStyleSheet("QFrame {background: transparent;}");
661     scroll->setStyleSheet("QFrame {border: none;}");
662     vLayout->setContentsMargins(0, 0, 0, 0);
663     frameLayout->setContentsMargins(0, 2, 0, 0);
664     frameLayout->setSpacing(2);
665     frameLayout->setAlignment(Qt::AlignLeft | Qt::AlignTop);
666 
667     frame->setLayout(frameLayout);
668     scroll->setWidget(frame);
669     vLayout->addWidget(scroll);
670     setLayout(vLayout);
671 
672     QCheckBox *statusLabel = new ArrowCheckBox(tr("Status"), this);
673     statusLabel->setChecked(pref->getStatusFilterState());
674     statusLabel->setFont(font);
675     frameLayout->addWidget(statusLabel);
676 
677     auto *statusFilters = new StatusFilterWidget(this, transferList);
678     frameLayout->addWidget(statusFilters);
679 
680     QCheckBox *categoryLabel = new ArrowCheckBox(tr("Categories"), this);
681     categoryLabel->setChecked(pref->getCategoryFilterState());
682     categoryLabel->setFont(font);
683     connect(categoryLabel, &QCheckBox::toggled, this
684             , &TransferListFiltersWidget::onCategoryFilterStateChanged);
685     frameLayout->addWidget(categoryLabel);
686 
687     m_categoryFilterWidget = new CategoryFilterWidget(this);
688     connect(m_categoryFilterWidget, &CategoryFilterWidget::actionDeleteTorrentsTriggered
689             , transferList, &TransferListWidget::deleteVisibleTorrents);
690     connect(m_categoryFilterWidget, &CategoryFilterWidget::actionPauseTorrentsTriggered
691             , transferList, &TransferListWidget::pauseVisibleTorrents);
692     connect(m_categoryFilterWidget, &CategoryFilterWidget::actionResumeTorrentsTriggered
693             , transferList, &TransferListWidget::startVisibleTorrents);
694     connect(m_categoryFilterWidget, &CategoryFilterWidget::categoryChanged
695             , transferList, &TransferListWidget::applyCategoryFilter);
696     toggleCategoryFilter(pref->getCategoryFilterState());
697     frameLayout->addWidget(m_categoryFilterWidget);
698 
699     QCheckBox *tagsLabel = new ArrowCheckBox(tr("Tags"), this);
700     tagsLabel->setChecked(pref->getTagFilterState());
701     tagsLabel->setFont(font);
702     connect(tagsLabel, &QCheckBox::toggled, this, &TransferListFiltersWidget::onTagFilterStateChanged);
703     frameLayout->addWidget(tagsLabel);
704 
705     m_tagFilterWidget = new TagFilterWidget(this);
706     connect(m_tagFilterWidget, &TagFilterWidget::actionDeleteTorrentsTriggered
707             , transferList, &TransferListWidget::deleteVisibleTorrents);
708     connect(m_tagFilterWidget, &TagFilterWidget::actionPauseTorrentsTriggered
709             , transferList, &TransferListWidget::pauseVisibleTorrents);
710     connect(m_tagFilterWidget, &TagFilterWidget::actionResumeTorrentsTriggered
711             , transferList, &TransferListWidget::startVisibleTorrents);
712     connect(m_tagFilterWidget, &TagFilterWidget::tagChanged
713             , transferList, &TransferListWidget::applyTagFilter);
714     toggleTagFilter(pref->getTagFilterState());
715     frameLayout->addWidget(m_tagFilterWidget);
716 
717     QCheckBox *trackerLabel = new ArrowCheckBox(tr("Trackers"), this);
718     trackerLabel->setChecked(pref->getTrackerFilterState());
719     trackerLabel->setFont(font);
720     frameLayout->addWidget(trackerLabel);
721 
722     m_trackerFilters = new TrackerFiltersList(this, transferList, downloadFavicon);
723     frameLayout->addWidget(m_trackerFilters);
724 
725     connect(statusLabel, &QCheckBox::toggled, statusFilters, &StatusFilterWidget::toggleFilter);
726     connect(statusLabel, &QCheckBox::toggled, pref, &Preferences::setStatusFilterState);
727     connect(trackerLabel, &QCheckBox::toggled, m_trackerFilters, &TrackerFiltersList::toggleFilter);
728     connect(trackerLabel, &QCheckBox::toggled, pref, &Preferences::setTrackerFilterState);
729 
730     connect(this, qOverload<const BitTorrent::TorrentID &, const QString &>(&TransferListFiltersWidget::trackerSuccess)
731             , m_trackerFilters, &TrackerFiltersList::trackerSuccess);
732     connect(this, qOverload<const BitTorrent::TorrentID &, const QString &>(&TransferListFiltersWidget::trackerError)
733             , m_trackerFilters, &TrackerFiltersList::trackerError);
734     connect(this, qOverload<const BitTorrent::TorrentID &, const QString &>(&TransferListFiltersWidget::trackerWarning)
735             , m_trackerFilters, &TrackerFiltersList::trackerWarning);
736 }
737 
setDownloadTrackerFavicon(bool value)738 void TransferListFiltersWidget::setDownloadTrackerFavicon(bool value)
739 {
740     m_trackerFilters->setDownloadTrackerFavicon(value);
741 }
742 
addTrackers(const BitTorrent::Torrent * torrent,const QVector<BitTorrent::TrackerEntry> & trackers)743 void TransferListFiltersWidget::addTrackers(const BitTorrent::Torrent *torrent, const QVector<BitTorrent::TrackerEntry> &trackers)
744 {
745     for (const BitTorrent::TrackerEntry &tracker : trackers)
746         m_trackerFilters->addItem(tracker.url, torrent->id());
747 }
748 
removeTrackers(const BitTorrent::Torrent * torrent,const QVector<BitTorrent::TrackerEntry> & trackers)749 void TransferListFiltersWidget::removeTrackers(const BitTorrent::Torrent *torrent, const QVector<BitTorrent::TrackerEntry> &trackers)
750 {
751     for (const BitTorrent::TrackerEntry &tracker : trackers)
752         m_trackerFilters->removeItem(tracker.url, torrent->id());
753 }
754 
changeTrackerless(const BitTorrent::Torrent * torrent,const bool trackerless)755 void TransferListFiltersWidget::changeTrackerless(const BitTorrent::Torrent *torrent, const bool trackerless)
756 {
757     m_trackerFilters->changeTrackerless(trackerless, torrent->id());
758 }
759 
trackerSuccess(const BitTorrent::Torrent * torrent,const QString & tracker)760 void TransferListFiltersWidget::trackerSuccess(const BitTorrent::Torrent *torrent, const QString &tracker)
761 {
762     emit trackerSuccess(torrent->id(), tracker);
763 }
764 
trackerWarning(const BitTorrent::Torrent * torrent,const QString & tracker)765 void TransferListFiltersWidget::trackerWarning(const BitTorrent::Torrent *torrent, const QString &tracker)
766 {
767     emit trackerWarning(torrent->id(), tracker);
768 }
769 
trackerError(const BitTorrent::Torrent * torrent,const QString & tracker)770 void TransferListFiltersWidget::trackerError(const BitTorrent::Torrent *torrent, const QString &tracker)
771 {
772     emit trackerError(torrent->id(), tracker);
773 }
774 
onCategoryFilterStateChanged(bool enabled)775 void TransferListFiltersWidget::onCategoryFilterStateChanged(bool enabled)
776 {
777     toggleCategoryFilter(enabled);
778     Preferences::instance()->setCategoryFilterState(enabled);
779 }
780 
toggleCategoryFilter(bool enabled)781 void TransferListFiltersWidget::toggleCategoryFilter(bool enabled)
782 {
783     m_categoryFilterWidget->setVisible(enabled);
784     m_transferList->applyCategoryFilter(enabled ? m_categoryFilterWidget->currentCategory() : QString());
785 }
786 
onTagFilterStateChanged(bool enabled)787 void TransferListFiltersWidget::onTagFilterStateChanged(bool enabled)
788 {
789     toggleTagFilter(enabled);
790     Preferences::instance()->setTagFilterState(enabled);
791 }
792 
toggleTagFilter(bool enabled)793 void TransferListFiltersWidget::toggleTagFilter(bool enabled)
794 {
795     m_tagFilterWidget->setVisible(enabled);
796     m_transferList->applyTagFilter(enabled ? m_tagFilterWidget->currentTag() : QString());
797 }
798