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