Add option to auto hide zero status filters
[qBittorrent.git] / src / gui / transferlistwidget.cpp
blob246a72860f3559f53ab3877c7e00911bf1147457
1 /*
2 * Bittorrent Client using Qt and libtorrent.
3 * Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
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.
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.
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.
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.
29 #include "transferlistwidget.h"
31 #include <algorithm>
33 #include <QClipboard>
34 #include <QDebug>
35 #include <QFileDialog>
36 #include <QHeaderView>
37 #include <QMenu>
38 #include <QMessageBox>
39 #include <QRegularExpression>
40 #include <QSet>
41 #include <QShortcut>
42 #include <QVector>
43 #include <QWheelEvent>
45 #include "base/bittorrent/session.h"
46 #include "base/bittorrent/torrent.h"
47 #include "base/bittorrent/trackerentry.h"
48 #include "base/global.h"
49 #include "base/logger.h"
50 #include "base/path.h"
51 #include "base/preferences.h"
52 #include "base/torrentfilter.h"
53 #include "base/utils/compare.h"
54 #include "base/utils/fs.h"
55 #include "base/utils/misc.h"
56 #include "base/utils/string.h"
57 #include "autoexpandabledialog.h"
58 #include "deletionconfirmationdialog.h"
59 #include "mainwindow.h"
60 #include "optionsdialog.h"
61 #include "previewselectdialog.h"
62 #include "speedlimitdialog.h"
63 #include "torrentcategorydialog.h"
64 #include "torrentoptionsdialog.h"
65 #include "trackerentriesdialog.h"
66 #include "transferlistdelegate.h"
67 #include "transferlistsortmodel.h"
68 #include "tristateaction.h"
69 #include "uithememanager.h"
70 #include "utils.h"
72 #ifdef Q_OS_MACOS
73 #include "macutilities.h"
74 #endif
76 namespace
78 QVector<BitTorrent::TorrentID> extractIDs(const QVector<BitTorrent::Torrent *> &torrents)
80 QVector<BitTorrent::TorrentID> torrentIDs;
81 torrentIDs.reserve(torrents.size());
82 for (const BitTorrent::Torrent *torrent : torrents)
83 torrentIDs << torrent->id();
84 return torrentIDs;
87 bool torrentContainsPreviewableFiles(const BitTorrent::Torrent *const torrent)
89 if (!torrent->hasMetadata())
90 return false;
92 for (const Path &filePath : asConst(torrent->filePaths()))
94 if (Utils::Misc::isPreviewable(filePath))
95 return true;
98 return false;
101 void openDestinationFolder(const BitTorrent::Torrent *const torrent)
103 #ifdef Q_OS_MACOS
104 MacUtils::openFiles({torrent->contentPath()});
105 #else
106 if (torrent->filesCount() == 1)
107 Utils::Gui::openFolderSelect(torrent->contentPath());
108 else
109 Utils::Gui::openPath(torrent->contentPath());
110 #endif
113 void removeTorrents(const QVector<BitTorrent::Torrent *> &torrents, const bool isDeleteFileSelected)
115 auto *session = BitTorrent::Session::instance();
116 const DeleteOption deleteOption = isDeleteFileSelected ? DeleteTorrentAndFiles : DeleteTorrent;
117 for (const BitTorrent::Torrent *torrent : torrents)
118 session->deleteTorrent(torrent->id(), deleteOption);
122 TransferListWidget::TransferListWidget(QWidget *parent, MainWindow *mainWindow)
123 : QTreeView {parent}
124 , m_listModel {new TransferListModel {this}}
125 , m_sortFilterModel {new TransferListSortModel {this}}
126 , m_mainWindow {mainWindow}
128 // Load settings
129 const bool columnLoaded = loadSettings();
131 // Create and apply delegate
132 setItemDelegate(new TransferListDelegate {this});
134 m_sortFilterModel->setDynamicSortFilter(true);
135 m_sortFilterModel->setSourceModel(m_listModel);
136 m_sortFilterModel->setFilterKeyColumn(TransferListModel::TR_NAME);
137 m_sortFilterModel->setFilterRole(Qt::DisplayRole);
138 m_sortFilterModel->setSortCaseSensitivity(Qt::CaseInsensitive);
139 m_sortFilterModel->setSortRole(TransferListModel::UnderlyingDataRole);
140 setModel(m_sortFilterModel);
142 // Visual settings
143 setUniformRowHeights(true);
144 setRootIsDecorated(false);
145 setAllColumnsShowFocus(true);
146 setSortingEnabled(true);
147 setSelectionMode(QAbstractItemView::ExtendedSelection);
148 setItemsExpandable(false);
149 setAutoScroll(true);
150 setDragDropMode(QAbstractItemView::DragOnly);
151 #if defined(Q_OS_MACOS)
152 setAttribute(Qt::WA_MacShowFocusRect, false);
153 #endif
154 header()->setFirstSectionMovable(true);
155 header()->setStretchLastSection(false);
156 header()->setTextElideMode(Qt::ElideRight);
158 // Default hidden columns
159 if (!columnLoaded)
161 setColumnHidden(TransferListModel::TR_ADD_DATE, true);
162 setColumnHidden(TransferListModel::TR_SEED_DATE, true);
163 setColumnHidden(TransferListModel::TR_UPLIMIT, true);
164 setColumnHidden(TransferListModel::TR_DLLIMIT, true);
165 setColumnHidden(TransferListModel::TR_TRACKER, true);
166 setColumnHidden(TransferListModel::TR_AMOUNT_DOWNLOADED, true);
167 setColumnHidden(TransferListModel::TR_AMOUNT_UPLOADED, true);
168 setColumnHidden(TransferListModel::TR_AMOUNT_DOWNLOADED_SESSION, true);
169 setColumnHidden(TransferListModel::TR_AMOUNT_UPLOADED_SESSION, true);
170 setColumnHidden(TransferListModel::TR_AMOUNT_LEFT, true);
171 setColumnHidden(TransferListModel::TR_TIME_ELAPSED, true);
172 setColumnHidden(TransferListModel::TR_SAVE_PATH, true);
173 setColumnHidden(TransferListModel::TR_DOWNLOAD_PATH, true);
174 setColumnHidden(TransferListModel::TR_INFOHASH_V1, true);
175 setColumnHidden(TransferListModel::TR_INFOHASH_V2, true);
176 setColumnHidden(TransferListModel::TR_COMPLETED, true);
177 setColumnHidden(TransferListModel::TR_RATIO_LIMIT, true);
178 setColumnHidden(TransferListModel::TR_SEEN_COMPLETE_DATE, true);
179 setColumnHidden(TransferListModel::TR_LAST_ACTIVITY, true);
180 setColumnHidden(TransferListModel::TR_TOTAL_SIZE, true);
183 //Ensure that at least one column is visible at all times
184 bool atLeastOne = false;
185 for (int i = 0; i < TransferListModel::NB_COLUMNS; ++i)
187 if (!isColumnHidden(i))
189 atLeastOne = true;
190 break;
193 if (!atLeastOne)
194 setColumnHidden(TransferListModel::TR_NAME, false);
196 //When adding/removing columns between versions some may
197 //end up being size 0 when the new version is launched with
198 //a conf file from the previous version.
199 for (int i = 0; i < TransferListModel::NB_COLUMNS; ++i)
201 if ((columnWidth(i) <= 0) && (!isColumnHidden(i)))
202 resizeColumnToContents(i);
205 setContextMenuPolicy(Qt::CustomContextMenu);
207 // Listen for list events
208 connect(this, &QAbstractItemView::doubleClicked, this, &TransferListWidget::torrentDoubleClicked);
209 connect(this, &QWidget::customContextMenuRequested, this, &TransferListWidget::displayListMenu);
210 header()->setContextMenuPolicy(Qt::CustomContextMenu);
211 connect(header(), &QWidget::customContextMenuRequested, this, &TransferListWidget::displayColumnHeaderMenu);
212 connect(header(), &QHeaderView::sectionMoved, this, &TransferListWidget::saveSettings);
213 connect(header(), &QHeaderView::sectionResized, this, &TransferListWidget::saveSettings);
214 connect(header(), &QHeaderView::sortIndicatorChanged, this, &TransferListWidget::saveSettings);
216 const auto *editHotkey = new QShortcut(Qt::Key_F2, this, nullptr, nullptr, Qt::WidgetShortcut);
217 connect(editHotkey, &QShortcut::activated, this, &TransferListWidget::renameSelectedTorrent);
218 const auto *deleteHotkey = new QShortcut(QKeySequence::Delete, this, nullptr, nullptr, Qt::WidgetShortcut);
219 connect(deleteHotkey, &QShortcut::activated, this, &TransferListWidget::softDeleteSelectedTorrents);
220 const auto *permDeleteHotkey = new QShortcut(Qt::SHIFT + Qt::Key_Delete, this, nullptr, nullptr, Qt::WidgetShortcut);
221 connect(permDeleteHotkey, &QShortcut::activated, this, &TransferListWidget::permDeleteSelectedTorrents);
222 const auto *doubleClickHotkeyReturn = new QShortcut(Qt::Key_Return, this, nullptr, nullptr, Qt::WidgetShortcut);
223 connect(doubleClickHotkeyReturn, &QShortcut::activated, this, &TransferListWidget::torrentDoubleClicked);
224 const auto *doubleClickHotkeyEnter = new QShortcut(Qt::Key_Enter, this, nullptr, nullptr, Qt::WidgetShortcut);
225 connect(doubleClickHotkeyEnter, &QShortcut::activated, this, &TransferListWidget::torrentDoubleClicked);
226 const auto *recheckHotkey = new QShortcut(Qt::CTRL + Qt::Key_R, this, nullptr, nullptr, Qt::WidgetShortcut);
227 connect(recheckHotkey, &QShortcut::activated, this, &TransferListWidget::recheckSelectedTorrents);
228 const auto *forceStartHotkey = new QShortcut(Qt::CTRL + Qt::Key_M, this, nullptr, nullptr, Qt::WidgetShortcut);
229 connect(forceStartHotkey, &QShortcut::activated, this, &TransferListWidget::forceStartSelectedTorrents);
232 TransferListWidget::~TransferListWidget()
234 // Save settings
235 saveSettings();
238 TransferListModel *TransferListWidget::getSourceModel() const
240 return m_listModel;
243 void TransferListWidget::previewFile(const Path &filePath)
245 Utils::Gui::openPath(filePath);
248 QModelIndex TransferListWidget::mapToSource(const QModelIndex &index) const
250 Q_ASSERT(index.isValid());
251 if (index.model() == m_sortFilterModel)
252 return m_sortFilterModel->mapToSource(index);
253 return index;
256 QModelIndex TransferListWidget::mapFromSource(const QModelIndex &index) const
258 Q_ASSERT(index.isValid());
259 Q_ASSERT(index.model() == m_sortFilterModel);
260 return m_sortFilterModel->mapFromSource(index);
263 void TransferListWidget::torrentDoubleClicked()
265 const QModelIndexList selectedIndexes = selectionModel()->selectedRows();
266 if ((selectedIndexes.size() != 1) || !selectedIndexes.first().isValid()) return;
268 const QModelIndex index = m_listModel->index(mapToSource(selectedIndexes.first()).row());
269 BitTorrent::Torrent *const torrent = m_listModel->torrentHandle(index);
270 if (!torrent) return;
272 int action;
273 if (torrent->isFinished())
274 action = Preferences::instance()->getActionOnDblClOnTorrentFn();
275 else
276 action = Preferences::instance()->getActionOnDblClOnTorrentDl();
278 switch (action)
280 case TOGGLE_PAUSE:
281 if (torrent->isPaused())
282 torrent->resume();
283 else
284 torrent->pause();
285 break;
286 case PREVIEW_FILE:
287 if (torrentContainsPreviewableFiles(torrent))
289 auto *dialog = new PreviewSelectDialog(this, torrent);
290 dialog->setAttribute(Qt::WA_DeleteOnClose);
291 connect(dialog, &PreviewSelectDialog::readyToPreviewFile, this, &TransferListWidget::previewFile);
292 dialog->show();
294 else
296 openDestinationFolder(torrent);
298 break;
299 case OPEN_DEST:
300 openDestinationFolder(torrent);
301 break;
302 case SHOW_OPTIONS:
303 setTorrentOptions();
304 break;
308 QVector<BitTorrent::Torrent *> TransferListWidget::getSelectedTorrents() const
310 const QModelIndexList selectedRows = selectionModel()->selectedRows();
312 QVector<BitTorrent::Torrent *> torrents;
313 torrents.reserve(selectedRows.size());
314 for (const QModelIndex &index : selectedRows)
315 torrents << m_listModel->torrentHandle(mapToSource(index));
316 return torrents;
319 QVector<BitTorrent::Torrent *> TransferListWidget::getVisibleTorrents() const
321 const int visibleTorrentsCount = m_sortFilterModel->rowCount();
323 QVector<BitTorrent::Torrent *> torrents;
324 torrents.reserve(visibleTorrentsCount);
325 for (int i = 0; i < visibleTorrentsCount; ++i)
326 torrents << m_listModel->torrentHandle(mapToSource(m_sortFilterModel->index(i, 0)));
327 return torrents;
330 void TransferListWidget::setSelectedTorrentsLocation()
332 const QVector<BitTorrent::Torrent *> torrents = getSelectedTorrents();
333 if (torrents.isEmpty())
334 return;
336 const Path oldLocation = torrents[0]->savePath();
338 auto fileDialog = new QFileDialog(this, tr("Choose save path"), oldLocation.data());
339 fileDialog->setAttribute(Qt::WA_DeleteOnClose);
340 fileDialog->setFileMode(QFileDialog::Directory);
341 fileDialog->setOptions(QFileDialog::DontConfirmOverwrite | QFileDialog::ShowDirsOnly | QFileDialog::HideNameFilterDetails);
342 connect(fileDialog, &QDialog::accepted, this, [this, fileDialog]()
344 const QVector<BitTorrent::Torrent *> torrents = getSelectedTorrents();
345 if (torrents.isEmpty())
346 return;
348 const Path newLocation {fileDialog->selectedFiles().constFirst()};
349 if (!newLocation.exists())
350 return;
352 // Actually move storage
353 for (BitTorrent::Torrent *const torrent : torrents)
355 torrent->setAutoTMMEnabled(false);
356 torrent->setSavePath(newLocation);
360 fileDialog->open();
363 void TransferListWidget::pauseAllTorrents()
365 // Show confirmation if user would really like to Pause All
366 const QMessageBox::StandardButton ret =
367 QMessageBox::question(this, tr("Confirm pause")
368 , tr("Would you like to pause all torrents?")
369 , (QMessageBox::Yes | QMessageBox::No));
371 if (ret != QMessageBox::Yes)
372 return;
374 for (BitTorrent::Torrent *const torrent : asConst(BitTorrent::Session::instance()->torrents()))
375 torrent->pause();
378 void TransferListWidget::resumeAllTorrents()
380 // Show confirmation if user would really like to Resume All
381 const QMessageBox::StandardButton ret =
382 QMessageBox::question(this, tr("Confirm resume")
383 , tr("Would you like to resume all torrents?")
384 , (QMessageBox::Yes | QMessageBox::No));
386 if (ret != QMessageBox::Yes)
387 return;
389 for (BitTorrent::Torrent *const torrent : asConst(BitTorrent::Session::instance()->torrents()))
390 torrent->resume();
393 void TransferListWidget::startSelectedTorrents()
395 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
396 torrent->resume();
399 void TransferListWidget::forceStartSelectedTorrents()
401 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
402 torrent->resume(BitTorrent::TorrentOperatingMode::Forced);
405 void TransferListWidget::startVisibleTorrents()
407 for (BitTorrent::Torrent *const torrent : asConst(getVisibleTorrents()))
408 torrent->resume();
411 void TransferListWidget::pauseSelectedTorrents()
413 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
414 torrent->pause();
417 void TransferListWidget::pauseVisibleTorrents()
419 for (BitTorrent::Torrent *const torrent : asConst(getVisibleTorrents()))
420 torrent->pause();
423 void TransferListWidget::softDeleteSelectedTorrents()
425 deleteSelectedTorrents(false);
428 void TransferListWidget::permDeleteSelectedTorrents()
430 deleteSelectedTorrents(true);
433 void TransferListWidget::deleteSelectedTorrents(const bool deleteLocalFiles)
435 if (m_mainWindow->currentTabWidget() != this) return;
437 const QVector<BitTorrent::Torrent *> torrents = getSelectedTorrents();
438 if (torrents.empty()) return;
440 if (Preferences::instance()->confirmTorrentDeletion())
442 auto *dialog = new DeletionConfirmationDialog(this, torrents.size(), torrents[0]->name(), deleteLocalFiles);
443 dialog->setAttribute(Qt::WA_DeleteOnClose);
444 connect(dialog, &DeletionConfirmationDialog::accepted, this, [this, dialog]()
446 // Some torrents might be removed when waiting for user input, so refetch the torrent list
447 // NOTE: this will only work when dialog is modal
448 removeTorrents(getSelectedTorrents(), dialog->isDeleteFileSelected());
450 dialog->open();
452 else
454 removeTorrents(torrents, deleteLocalFiles);
458 void TransferListWidget::deleteVisibleTorrents()
460 const QVector<BitTorrent::Torrent *> torrents = getVisibleTorrents();
461 if (torrents.empty()) return;
463 if (Preferences::instance()->confirmTorrentDeletion())
465 auto *dialog = new DeletionConfirmationDialog(this, torrents.size(), torrents[0]->name(), false);
466 dialog->setAttribute(Qt::WA_DeleteOnClose);
467 connect(dialog, &DeletionConfirmationDialog::accepted, this, [this, dialog]()
469 // Some torrents might be removed when waiting for user input, so refetch the torrent list
470 // NOTE: this will only work when dialog is modal
471 removeTorrents(getVisibleTorrents(), dialog->isDeleteFileSelected());
473 dialog->open();
475 else
477 removeTorrents(torrents, false);
481 void TransferListWidget::increaseQueuePosSelectedTorrents()
483 qDebug() << Q_FUNC_INFO;
484 if (m_mainWindow->currentTabWidget() == this)
485 BitTorrent::Session::instance()->increaseTorrentsQueuePos(extractIDs(getSelectedTorrents()));
488 void TransferListWidget::decreaseQueuePosSelectedTorrents()
490 qDebug() << Q_FUNC_INFO;
491 if (m_mainWindow->currentTabWidget() == this)
492 BitTorrent::Session::instance()->decreaseTorrentsQueuePos(extractIDs(getSelectedTorrents()));
495 void TransferListWidget::topQueuePosSelectedTorrents()
497 if (m_mainWindow->currentTabWidget() == this)
498 BitTorrent::Session::instance()->topTorrentsQueuePos(extractIDs(getSelectedTorrents()));
501 void TransferListWidget::bottomQueuePosSelectedTorrents()
503 if (m_mainWindow->currentTabWidget() == this)
504 BitTorrent::Session::instance()->bottomTorrentsQueuePos(extractIDs(getSelectedTorrents()));
507 void TransferListWidget::copySelectedMagnetURIs() const
509 QStringList magnetUris;
510 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
511 magnetUris << torrent->createMagnetURI();
513 qApp->clipboard()->setText(magnetUris.join(u'\n'));
516 void TransferListWidget::copySelectedNames() const
518 QStringList torrentNames;
519 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
520 torrentNames << torrent->name();
522 qApp->clipboard()->setText(torrentNames.join(u'\n'));
525 void TransferListWidget::copySelectedInfohashes(const CopyInfohashPolicy policy) const
527 const auto selectedTorrents = getSelectedTorrents();
528 QStringList infoHashes;
529 infoHashes.reserve(selectedTorrents.size());
530 switch (policy)
532 case CopyInfohashPolicy::Version1:
533 for (const BitTorrent::Torrent *torrent : selectedTorrents)
535 if (const auto infoHash = torrent->infoHash().v1(); infoHash.isValid())
536 infoHashes << infoHash.toString();
538 break;
539 case CopyInfohashPolicy::Version2:
540 for (const BitTorrent::Torrent *torrent : selectedTorrents)
542 if (const auto infoHash = torrent->infoHash().v2(); infoHash.isValid())
543 infoHashes << infoHash.toString();
545 break;
548 qApp->clipboard()->setText(infoHashes.join(u'\n'));
551 void TransferListWidget::copySelectedIDs() const
553 QStringList torrentIDs;
554 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
555 torrentIDs << torrent->id().toString();
557 qApp->clipboard()->setText(torrentIDs.join(u'\n'));
560 void TransferListWidget::hideQueuePosColumn(bool hide)
562 setColumnHidden(TransferListModel::TR_QUEUE_POSITION, hide);
563 if (!hide && (columnWidth(TransferListModel::TR_QUEUE_POSITION) == 0))
564 resizeColumnToContents(TransferListModel::TR_QUEUE_POSITION);
567 void TransferListWidget::openSelectedTorrentsFolder() const
569 QSet<Path> paths;
570 #ifdef Q_OS_MACOS
571 // On macOS you expect both the files and folders to be opened in their parent
572 // folders prehilighted for opening, so we use a custom method.
573 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
575 const Path contentPath = torrent->contentPath();
576 paths.insert(contentPath);
578 MacUtils::openFiles(PathList(paths.cbegin(), paths.cend()));
579 #else
580 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
582 const Path contentPath = torrent->contentPath();
583 if (!paths.contains(contentPath))
585 if (torrent->filesCount() == 1)
586 Utils::Gui::openFolderSelect(contentPath);
587 else
588 Utils::Gui::openPath(contentPath);
590 paths.insert(contentPath);
592 #endif // Q_OS_MACOS
595 void TransferListWidget::previewSelectedTorrents()
597 for (const BitTorrent::Torrent *torrent : asConst(getSelectedTorrents()))
599 if (torrentContainsPreviewableFiles(torrent))
601 auto *dialog = new PreviewSelectDialog(this, torrent);
602 dialog->setAttribute(Qt::WA_DeleteOnClose);
603 connect(dialog, &PreviewSelectDialog::readyToPreviewFile, this, &TransferListWidget::previewFile);
604 dialog->show();
606 else
608 QMessageBox::critical(this, tr("Unable to preview"), tr("The selected torrent \"%1\" does not contain previewable files")
609 .arg(torrent->name()));
614 void TransferListWidget::setTorrentOptions()
616 const QVector<BitTorrent::Torrent *> selectedTorrents = getSelectedTorrents();
617 if (selectedTorrents.empty()) return;
619 auto dialog = new TorrentOptionsDialog {this, selectedTorrents};
620 dialog->setAttribute(Qt::WA_DeleteOnClose);
621 dialog->open();
624 void TransferListWidget::recheckSelectedTorrents()
626 if (Preferences::instance()->confirmTorrentRecheck())
628 QMessageBox::StandardButton ret = QMessageBox::question(this, tr("Recheck confirmation"), tr("Are you sure you want to recheck the selected torrent(s)?"), QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
629 if (ret != QMessageBox::Yes) return;
632 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
633 torrent->forceRecheck();
636 void TransferListWidget::reannounceSelectedTorrents()
638 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
639 torrent->forceReannounce();
642 int TransferListWidget::visibleColumnsCount() const
644 int count = 0;
645 for (int i = 0, iMax = header()->count(); i < iMax; ++i)
647 if (!isColumnHidden(i))
648 ++count;
651 return count;
654 // hide/show columns menu
655 void TransferListWidget::displayColumnHeaderMenu()
657 auto menu = new QMenu(this);
658 menu->setAttribute(Qt::WA_DeleteOnClose);
659 menu->setTitle(tr("Column visibility"));
660 menu->setToolTipsVisible(true);
662 for (int i = 0; i < TransferListModel::NB_COLUMNS; ++i)
664 if (!BitTorrent::Session::instance()->isQueueingSystemEnabled() && (i == TransferListModel::TR_QUEUE_POSITION))
665 continue;
667 const auto columnName = m_listModel->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString();
668 QAction *action = menu->addAction(columnName, this, [this, i](const bool checked)
670 if (!checked && (visibleColumnsCount() <= 1))
671 return;
673 setColumnHidden(i, !checked);
675 if (checked && (columnWidth(i) <= 5))
676 resizeColumnToContents(i);
678 saveSettings();
680 action->setCheckable(true);
681 action->setChecked(!isColumnHidden(i));
684 menu->addSeparator();
685 QAction *resizeAction = menu->addAction(tr("Resize columns"), this, [this]()
687 for (int i = 0, count = header()->count(); i < count; ++i)
689 if (!isColumnHidden(i))
690 resizeColumnToContents(i);
692 saveSettings();
694 resizeAction->setToolTip(tr("Resize all non-hidden columns to the size of their contents"));
696 menu->popup(QCursor::pos());
699 void TransferListWidget::setSelectedTorrentsSuperSeeding(const bool enabled) const
701 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
703 if (torrent->hasMetadata())
704 torrent->setSuperSeeding(enabled);
708 void TransferListWidget::setSelectedTorrentsSequentialDownload(const bool enabled) const
710 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
711 torrent->setSequentialDownload(enabled);
714 void TransferListWidget::setSelectedFirstLastPiecePrio(const bool enabled) const
716 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
717 torrent->setFirstLastPiecePriority(enabled);
720 void TransferListWidget::setSelectedAutoTMMEnabled(const bool enabled)
722 if (enabled)
724 const QMessageBox::StandardButton btn = QMessageBox::question(this, tr("Enable automatic torrent management")
725 , tr("Are you sure you want to enable Automatic Torrent Management for the selected torrent(s)? They may be relocated.")
726 , (QMessageBox::Yes | QMessageBox::No), QMessageBox::Yes);
727 if (btn != QMessageBox::Yes) return;
730 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
731 torrent->setAutoTMMEnabled(enabled);
734 void TransferListWidget::askNewCategoryForSelection()
736 const QString newCategoryName = TorrentCategoryDialog::createCategory(this);
737 if (!newCategoryName.isEmpty())
738 setSelectionCategory(newCategoryName);
741 void TransferListWidget::askAddTagsForSelection()
743 const QStringList tags = askTagsForSelection(tr("Add Tags"));
744 for (const QString &tag : tags)
745 addSelectionTag(tag);
748 void TransferListWidget::editTorrentTrackers()
750 const QVector<BitTorrent::Torrent *> torrents = getSelectedTorrents();
751 QVector<BitTorrent::TrackerEntry> commonTrackers;
753 if (!torrents.empty())
755 commonTrackers = torrents[0]->trackers();
757 for (const BitTorrent::Torrent *torrent : torrents)
759 QSet<BitTorrent::TrackerEntry> trackerSet;
761 for (const BitTorrent::TrackerEntry &entry : asConst(torrent->trackers()))
762 trackerSet.insert(entry);
764 commonTrackers.erase(std::remove_if(commonTrackers.begin(), commonTrackers.end()
765 , [&trackerSet](const BitTorrent::TrackerEntry &entry) { return !trackerSet.contains(entry); })
766 , commonTrackers.end());
770 auto trackerDialog = new TrackerEntriesDialog(this);
771 trackerDialog->setAttribute(Qt::WA_DeleteOnClose);
772 trackerDialog->setTrackers(commonTrackers);
774 connect(trackerDialog, &QDialog::accepted, this, [torrents, trackerDialog]()
776 for (BitTorrent::Torrent *torrent : torrents)
777 torrent->replaceTrackers(trackerDialog->trackers());
780 trackerDialog->open();
783 void TransferListWidget::exportTorrent()
785 if (getSelectedTorrents().isEmpty())
786 return;
788 auto fileDialog = new QFileDialog(this, tr("Choose folder to save exported .torrent files"));
789 fileDialog->setAttribute(Qt::WA_DeleteOnClose);
790 fileDialog->setFileMode(QFileDialog::Directory);
791 fileDialog->setOptions(QFileDialog::ShowDirsOnly);
792 connect(fileDialog, &QFileDialog::fileSelected, this, [this](const QString &dir)
794 const QVector<BitTorrent::Torrent *> torrents = getSelectedTorrents();
795 if (torrents.isEmpty())
796 return;
798 const Path savePath {dir};
799 if (!savePath.exists())
800 return;
802 const QString errorMsg = tr("Export .torrent file failed. Torrent: \"%1\". Save path: \"%2\". Reason: \"%3\"");
804 bool hasError = false;
805 for (const BitTorrent::Torrent *torrent : torrents)
807 const Path filePath = savePath / Path(torrent->name() + u".torrent");
808 if (filePath.exists())
810 LogMsg(errorMsg.arg(torrent->name(), filePath.toString(), tr("A file with the same name already exists")) , Log::WARNING);
811 hasError = true;
812 continue;
815 const nonstd::expected<void, QString> result = torrent->exportToFile(filePath);
816 if (!result)
818 LogMsg(errorMsg.arg(torrent->name(), filePath.toString(), result.error()) , Log::WARNING);
819 hasError = true;
820 continue;
824 if (hasError)
826 QMessageBox::warning(this, tr("Export .torrent file error")
827 , tr("Errors occurred when exporting .torrent files. Check execution log for details."));
831 fileDialog->open();
834 void TransferListWidget::confirmRemoveAllTagsForSelection()
836 QMessageBox::StandardButton response = QMessageBox::question(
837 this, tr("Remove All Tags"), tr("Remove all tags from selected torrents?"),
838 QMessageBox::Yes | QMessageBox::No);
839 if (response == QMessageBox::Yes)
840 clearSelectionTags();
843 QStringList TransferListWidget::askTagsForSelection(const QString &dialogTitle)
845 QStringList tags;
846 bool invalid = true;
847 while (invalid)
849 bool ok = false;
850 invalid = false;
851 const QString tagsInput = AutoExpandableDialog::getText(
852 this, dialogTitle, tr("Comma-separated tags:"), QLineEdit::Normal, {}, &ok).trimmed();
853 if (!ok || tagsInput.isEmpty())
854 return {};
855 tags = tagsInput.split(u',', Qt::SkipEmptyParts);
856 for (QString &tag : tags)
858 tag = tag.trimmed();
859 if (!BitTorrent::Session::isValidTag(tag))
861 QMessageBox::warning(this, tr("Invalid tag")
862 , tr("Tag name: '%1' is invalid").arg(tag));
863 invalid = true;
867 return tags;
870 void TransferListWidget::applyToSelectedTorrents(const std::function<void (BitTorrent::Torrent *const)> &fn)
872 for (const QModelIndex &index : asConst(selectionModel()->selectedRows()))
874 BitTorrent::Torrent *const torrent = m_listModel->torrentHandle(mapToSource(index));
875 Q_ASSERT(torrent);
876 fn(torrent);
880 void TransferListWidget::renameSelectedTorrent()
882 const QModelIndexList selectedIndexes = selectionModel()->selectedRows();
883 if ((selectedIndexes.size() != 1) || !selectedIndexes.first().isValid()) return;
885 const QModelIndex mi = m_listModel->index(mapToSource(selectedIndexes.first()).row(), TransferListModel::TR_NAME);
886 BitTorrent::Torrent *const torrent = m_listModel->torrentHandle(mi);
887 if (!torrent) return;
889 // Ask for a new Name
890 bool ok = false;
891 QString name = AutoExpandableDialog::getText(this, tr("Rename"), tr("New name:"), QLineEdit::Normal, torrent->name(), &ok);
892 if (ok && !name.isEmpty())
894 name.replace(QRegularExpression(u"\r?\n|\r"_qs), u" "_qs);
895 // Rename the torrent
896 m_listModel->setData(mi, name, Qt::DisplayRole);
900 void TransferListWidget::setSelectionCategory(const QString &category)
902 for (const QModelIndex &index : asConst(selectionModel()->selectedRows()))
903 m_listModel->setData(m_listModel->index(mapToSource(index).row(), TransferListModel::TR_CATEGORY), category, Qt::DisplayRole);
906 void TransferListWidget::addSelectionTag(const QString &tag)
908 applyToSelectedTorrents([&tag](BitTorrent::Torrent *const torrent) { torrent->addTag(tag); });
911 void TransferListWidget::removeSelectionTag(const QString &tag)
913 applyToSelectedTorrents([&tag](BitTorrent::Torrent *const torrent) { torrent->removeTag(tag); });
916 void TransferListWidget::clearSelectionTags()
918 applyToSelectedTorrents([](BitTorrent::Torrent *const torrent) { torrent->removeAllTags(); });
921 void TransferListWidget::displayListMenu()
923 const QModelIndexList selectedIndexes = selectionModel()->selectedRows();
924 if (selectedIndexes.isEmpty()) return;
926 auto *listMenu = new QMenu(this);
927 listMenu->setAttribute(Qt::WA_DeleteOnClose);
928 listMenu->setToolTipsVisible(true);
930 // Create actions
932 auto *actionStart = new QAction(UIThemeManager::instance()->getIcon(u"torrent-start"_qs, u"media-playback-start"_qs), tr("&Resume", "Resume/start the torrent"), listMenu);
933 connect(actionStart, &QAction::triggered, this, &TransferListWidget::startSelectedTorrents);
934 auto *actionPause = new QAction(UIThemeManager::instance()->getIcon(u"torrent-stop"_qs, u"media-playback-pause"_qs), tr("&Pause", "Pause the torrent"), listMenu);
935 connect(actionPause, &QAction::triggered, this, &TransferListWidget::pauseSelectedTorrents);
936 auto *actionForceStart = new QAction(UIThemeManager::instance()->getIcon(u"torrent-start-forced"_qs, u"media-playback-start"_qs), tr("Force Resu&me", "Force Resume/start the torrent"), listMenu);
937 connect(actionForceStart, &QAction::triggered, this, &TransferListWidget::forceStartSelectedTorrents);
938 auto *actionDelete = new QAction(UIThemeManager::instance()->getIcon(u"list-remove"_qs), tr("&Remove", "Remove the torrent"), listMenu);
939 connect(actionDelete, &QAction::triggered, this, &TransferListWidget::softDeleteSelectedTorrents);
940 auto *actionPreviewFile = new QAction(UIThemeManager::instance()->getIcon(u"view-preview"_qs), tr("Pre&view file..."), listMenu);
941 connect(actionPreviewFile, &QAction::triggered, this, &TransferListWidget::previewSelectedTorrents);
942 auto *actionTorrentOptions = new QAction(UIThemeManager::instance()->getIcon(u"configure"_qs), tr("Torrent &options..."), listMenu);
943 connect(actionTorrentOptions, &QAction::triggered, this, &TransferListWidget::setTorrentOptions);
944 auto *actionOpenDestinationFolder = new QAction(UIThemeManager::instance()->getIcon(u"directory"_qs), tr("Open destination &folder"), listMenu);
945 connect(actionOpenDestinationFolder, &QAction::triggered, this, &TransferListWidget::openSelectedTorrentsFolder);
946 auto *actionIncreaseQueuePos = new QAction(UIThemeManager::instance()->getIcon(u"go-up"_qs), tr("Move &up", "i.e. move up in the queue"), listMenu);
947 connect(actionIncreaseQueuePos, &QAction::triggered, this, &TransferListWidget::increaseQueuePosSelectedTorrents);
948 auto *actionDecreaseQueuePos = new QAction(UIThemeManager::instance()->getIcon(u"go-down"_qs), tr("Move &down", "i.e. Move down in the queue"), listMenu);
949 connect(actionDecreaseQueuePos, &QAction::triggered, this, &TransferListWidget::decreaseQueuePosSelectedTorrents);
950 auto *actionTopQueuePos = new QAction(UIThemeManager::instance()->getIcon(u"go-top"_qs), tr("Move to &top", "i.e. Move to top of the queue"), listMenu);
951 connect(actionTopQueuePos, &QAction::triggered, this, &TransferListWidget::topQueuePosSelectedTorrents);
952 auto *actionBottomQueuePos = new QAction(UIThemeManager::instance()->getIcon(u"go-bottom"_qs), tr("Move to &bottom", "i.e. Move to bottom of the queue"), listMenu);
953 connect(actionBottomQueuePos, &QAction::triggered, this, &TransferListWidget::bottomQueuePosSelectedTorrents);
954 auto *actionSetTorrentPath = new QAction(UIThemeManager::instance()->getIcon(u"set-location"_qs, u"inode-directory"_qs), tr("Set loc&ation..."), listMenu);
955 connect(actionSetTorrentPath, &QAction::triggered, this, &TransferListWidget::setSelectedTorrentsLocation);
956 auto *actionForceRecheck = new QAction(UIThemeManager::instance()->getIcon(u"force-recheck"_qs, u"document-edit-verify"_qs), tr("Force rec&heck"), listMenu);
957 connect(actionForceRecheck, &QAction::triggered, this, &TransferListWidget::recheckSelectedTorrents);
958 auto *actionForceReannounce = new QAction(UIThemeManager::instance()->getIcon(u"reannounce"_qs, u"document-edit-verify"_qs), tr("Force r&eannounce"), listMenu);
959 connect(actionForceReannounce, &QAction::triggered, this, &TransferListWidget::reannounceSelectedTorrents);
960 auto *actionCopyMagnetLink = new QAction(UIThemeManager::instance()->getIcon(u"torrent-magnet"_qs, u"kt-magnet"_qs), tr("&Magnet link"), listMenu);
961 connect(actionCopyMagnetLink, &QAction::triggered, this, &TransferListWidget::copySelectedMagnetURIs);
962 auto *actionCopyID = new QAction(UIThemeManager::instance()->getIcon(u"help-about"_qs, u"edit-copy"_qs), tr("Torrent &ID"), listMenu);
963 connect(actionCopyID, &QAction::triggered, this, &TransferListWidget::copySelectedIDs);
964 auto *actionCopyName = new QAction(UIThemeManager::instance()->getIcon(u"name"_qs, u"edit-copy"_qs), tr("&Name"), listMenu);
965 connect(actionCopyName, &QAction::triggered, this, &TransferListWidget::copySelectedNames);
966 auto *actionCopyHash1 = new QAction(UIThemeManager::instance()->getIcon(u"hash"_qs, u"edit-copy"_qs), tr("Info &hash v1"), listMenu);
967 connect(actionCopyHash1, &QAction::triggered, this, [this]() { copySelectedInfohashes(CopyInfohashPolicy::Version1); });
968 auto *actionCopyHash2 = new QAction(UIThemeManager::instance()->getIcon(u"hash"_qs, u"edit-copy"_qs), tr("Info h&ash v2"), listMenu);
969 connect(actionCopyHash2, &QAction::triggered, this, [this]() { copySelectedInfohashes(CopyInfohashPolicy::Version2); });
970 auto *actionSuperSeedingMode = new TriStateAction(tr("Super seeding mode"), listMenu);
971 connect(actionSuperSeedingMode, &QAction::triggered, this, &TransferListWidget::setSelectedTorrentsSuperSeeding);
972 auto *actionRename = new QAction(UIThemeManager::instance()->getIcon(u"edit-rename"_qs), tr("Re&name..."), listMenu);
973 connect(actionRename, &QAction::triggered, this, &TransferListWidget::renameSelectedTorrent);
974 auto *actionSequentialDownload = new TriStateAction(tr("Download in sequential order"), listMenu);
975 connect(actionSequentialDownload, &QAction::triggered, this, &TransferListWidget::setSelectedTorrentsSequentialDownload);
976 auto *actionFirstLastPiecePrio = new TriStateAction(tr("Download first and last pieces first"), listMenu);
977 connect(actionFirstLastPiecePrio, &QAction::triggered, this, &TransferListWidget::setSelectedFirstLastPiecePrio);
978 auto *actionAutoTMM = new TriStateAction(tr("Automatic Torrent Management"), listMenu);
979 actionAutoTMM->setToolTip(tr("Automatic mode means that various torrent properties (e.g. save path) will be decided by the associated category"));
980 connect(actionAutoTMM, &QAction::triggered, this, &TransferListWidget::setSelectedAutoTMMEnabled);
981 auto *actionEditTracker = new QAction(UIThemeManager::instance()->getIcon(u"edit-rename"_qs), tr("Edit trac&kers..."), listMenu);
982 connect(actionEditTracker, &QAction::triggered, this, &TransferListWidget::editTorrentTrackers);
983 auto *actionExportTorrent = new QAction(UIThemeManager::instance()->getIcon(u"edit-copy"_qs), tr("E&xport .torrent..."), listMenu);
984 connect(actionExportTorrent, &QAction::triggered, this, &TransferListWidget::exportTorrent);
985 // End of actions
987 // Enable/disable pause/start action given the DL state
988 bool needsPause = false, needsStart = false, needsForce = false, needsPreview = false;
989 bool allSameSuperSeeding = true;
990 bool superSeedingMode = false;
991 bool allSameSequentialDownloadMode = true, allSamePrioFirstlast = true;
992 bool sequentialDownloadMode = false, prioritizeFirstLast = false;
993 bool oneHasMetadata = false, oneNotFinished = false;
994 bool allSameCategory = true;
995 bool allSameAutoTMM = true;
996 bool firstAutoTMM = false;
997 QString firstCategory;
998 bool first = true;
999 TagSet tagsInAny;
1000 TagSet tagsInAll;
1001 bool hasInfohashV1 = false, hasInfohashV2 = false;
1002 bool oneCanForceReannounce = false;
1004 for (const QModelIndex &index : selectedIndexes)
1006 // Get the file name
1007 // Get handle and pause the torrent
1008 const BitTorrent::Torrent *torrent = m_listModel->torrentHandle(mapToSource(index));
1009 if (!torrent) continue;
1011 if (firstCategory.isEmpty() && first)
1012 firstCategory = torrent->category();
1013 if (firstCategory != torrent->category())
1014 allSameCategory = false;
1016 const TagSet torrentTags = torrent->tags();
1017 tagsInAny.unite(torrentTags);
1019 if (first)
1021 firstAutoTMM = torrent->isAutoTMMEnabled();
1022 tagsInAll = torrentTags;
1024 else
1026 tagsInAll.intersect(torrentTags);
1029 if (firstAutoTMM != torrent->isAutoTMMEnabled())
1030 allSameAutoTMM = false;
1032 if (torrent->hasMetadata())
1033 oneHasMetadata = true;
1034 if (!torrent->isFinished())
1036 oneNotFinished = true;
1037 if (first)
1039 sequentialDownloadMode = torrent->isSequentialDownload();
1040 prioritizeFirstLast = torrent->hasFirstLastPiecePriority();
1042 else
1044 if (sequentialDownloadMode != torrent->isSequentialDownload())
1045 allSameSequentialDownloadMode = false;
1046 if (prioritizeFirstLast != torrent->hasFirstLastPiecePriority())
1047 allSamePrioFirstlast = false;
1050 else
1052 if (!oneNotFinished && allSameSuperSeeding && torrent->hasMetadata())
1054 if (first)
1055 superSeedingMode = torrent->superSeeding();
1056 else if (superSeedingMode != torrent->superSeeding())
1057 allSameSuperSeeding = false;
1061 if (!torrent->isForced())
1062 needsForce = true;
1063 else
1064 needsStart = true;
1066 const bool isPaused = torrent->isPaused();
1067 if (isPaused)
1068 needsStart = true;
1069 else
1070 needsPause = true;
1072 if (torrent->isErrored() || torrent->hasMissingFiles())
1074 // If torrent is in "errored" or "missing files" state
1075 // it cannot keep further processing until you restart it.
1076 needsStart = true;
1077 needsForce = true;
1080 if (torrent->hasMetadata())
1081 needsPreview = true;
1083 if (!hasInfohashV1 && torrent->infoHash().v1().isValid())
1084 hasInfohashV1 = true;
1085 if (!hasInfohashV2 && torrent->infoHash().v2().isValid())
1086 hasInfohashV2 = true;
1088 first = false;
1090 const bool rechecking = torrent->isChecking();
1091 if (rechecking)
1093 needsStart = true;
1094 needsPause = true;
1097 const bool queued = (BitTorrent::Session::instance()->isQueueingSystemEnabled() && torrent->isQueued());
1099 if (!isPaused && !rechecking && !queued)
1100 oneCanForceReannounce = true;
1102 if (oneHasMetadata && oneNotFinished && !allSameSequentialDownloadMode
1103 && !allSamePrioFirstlast && !allSameSuperSeeding && !allSameCategory
1104 && needsStart && needsForce && needsPause && needsPreview && !allSameAutoTMM
1105 && hasInfohashV1 && hasInfohashV2 && oneCanForceReannounce)
1107 break;
1111 if (needsStart)
1112 listMenu->addAction(actionStart);
1113 if (needsPause)
1114 listMenu->addAction(actionPause);
1115 if (needsForce)
1116 listMenu->addAction(actionForceStart);
1117 listMenu->addSeparator();
1118 listMenu->addAction(actionDelete);
1119 listMenu->addSeparator();
1120 listMenu->addAction(actionSetTorrentPath);
1121 if (selectedIndexes.size() == 1)
1122 listMenu->addAction(actionRename);
1123 listMenu->addAction(actionEditTracker);
1125 // Category Menu
1126 QStringList categories = BitTorrent::Session::instance()->categories();
1127 std::sort(categories.begin(), categories.end(), Utils::Compare::NaturalLessThan<Qt::CaseInsensitive>());
1129 QMenu *categoryMenu = listMenu->addMenu(UIThemeManager::instance()->getIcon(u"view-categories"_qs), tr("Categor&y"));
1131 categoryMenu->addAction(UIThemeManager::instance()->getIcon(u"list-add"_qs), tr("&New...", "New category...")
1132 , this, &TransferListWidget::askNewCategoryForSelection);
1133 categoryMenu->addAction(UIThemeManager::instance()->getIcon(u"edit-clear"_qs), tr("&Reset", "Reset category")
1134 , this, [this]() { setSelectionCategory(u""_qs); });
1135 categoryMenu->addSeparator();
1137 for (const QString &category : asConst(categories))
1139 const QString escapedCategory = QString(category).replace(u'&', u"&&"_qs); // avoid '&' becomes accelerator key
1140 QAction *categoryAction = categoryMenu->addAction(UIThemeManager::instance()->getIcon(u"view-categories"_qs), escapedCategory
1141 , this, [this, category]() { setSelectionCategory(category); });
1143 if (allSameCategory && (category == firstCategory))
1145 categoryAction->setCheckable(true);
1146 categoryAction->setChecked(true);
1150 // Tag Menu
1151 QStringList tags(BitTorrent::Session::instance()->tags().values());
1152 std::sort(tags.begin(), tags.end(), Utils::Compare::NaturalLessThan<Qt::CaseInsensitive>());
1154 QMenu *tagsMenu = listMenu->addMenu(UIThemeManager::instance()->getIcon(u"tags"_qs, u"view-categories"_qs), tr("Ta&gs"));
1156 tagsMenu->addAction(UIThemeManager::instance()->getIcon(u"list-add"_qs), tr("&Add...", "Add / assign multiple tags...")
1157 , this, &TransferListWidget::askAddTagsForSelection);
1158 tagsMenu->addAction(UIThemeManager::instance()->getIcon(u"edit-clear"_qs), tr("&Remove All", "Remove all tags")
1159 , this, [this]()
1161 if (Preferences::instance()->confirmRemoveAllTags())
1162 confirmRemoveAllTagsForSelection();
1163 else
1164 clearSelectionTags();
1166 tagsMenu->addSeparator();
1168 for (const QString &tag : asConst(tags))
1170 auto *action = new TriStateAction(tag, tagsMenu);
1171 action->setCloseOnInteraction(false);
1173 const Qt::CheckState initialState = tagsInAll.contains(tag) ? Qt::Checked
1174 : tagsInAny.contains(tag) ? Qt::PartiallyChecked : Qt::Unchecked;
1175 action->setCheckState(initialState);
1177 connect(action, &QAction::toggled, this, [this, tag](const bool checked)
1179 if (checked)
1180 addSelectionTag(tag);
1181 else
1182 removeSelectionTag(tag);
1185 tagsMenu->addAction(action);
1188 actionAutoTMM->setCheckState(allSameAutoTMM
1189 ? (firstAutoTMM ? Qt::Checked : Qt::Unchecked)
1190 : Qt::PartiallyChecked);
1191 listMenu->addAction(actionAutoTMM);
1193 listMenu->addSeparator();
1194 listMenu->addAction(actionTorrentOptions);
1195 if (!oneNotFinished && oneHasMetadata)
1197 actionSuperSeedingMode->setCheckState(allSameSuperSeeding
1198 ? (superSeedingMode ? Qt::Checked : Qt::Unchecked)
1199 : Qt::PartiallyChecked);
1200 listMenu->addAction(actionSuperSeedingMode);
1202 listMenu->addSeparator();
1203 bool addedPreviewAction = false;
1204 if (needsPreview)
1206 listMenu->addAction(actionPreviewFile);
1207 addedPreviewAction = true;
1209 if (oneNotFinished)
1211 actionSequentialDownload->setCheckState(allSameSequentialDownloadMode
1212 ? (sequentialDownloadMode ? Qt::Checked : Qt::Unchecked)
1213 : Qt::PartiallyChecked);
1214 listMenu->addAction(actionSequentialDownload);
1216 actionFirstLastPiecePrio->setCheckState(allSamePrioFirstlast
1217 ? (prioritizeFirstLast ? Qt::Checked : Qt::Unchecked)
1218 : Qt::PartiallyChecked);
1219 listMenu->addAction(actionFirstLastPiecePrio);
1221 addedPreviewAction = true;
1224 if (addedPreviewAction)
1225 listMenu->addSeparator();
1226 if (oneHasMetadata)
1227 listMenu->addAction(actionForceRecheck);
1228 // We can not force reannounce torrents that are paused/errored/checking/missing files/queued.
1229 // We may already have the tracker list from magnet url. So we can force reannounce torrents without metadata anyway.
1230 listMenu->addAction(actionForceReannounce);
1231 actionForceReannounce->setEnabled(oneCanForceReannounce);
1232 if (!oneCanForceReannounce)
1233 actionForceReannounce->setToolTip(tr("Can not force reannounce if torrent is Paused/Queued/Errored/Checking"));
1234 listMenu->addSeparator();
1235 listMenu->addAction(actionOpenDestinationFolder);
1236 if (BitTorrent::Session::instance()->isQueueingSystemEnabled() && oneNotFinished)
1238 listMenu->addSeparator();
1239 QMenu *queueMenu = listMenu->addMenu(
1240 UIThemeManager::instance()->getIcon(u"queued"_qs), tr("&Queue"));
1241 queueMenu->addAction(actionTopQueuePos);
1242 queueMenu->addAction(actionIncreaseQueuePos);
1243 queueMenu->addAction(actionDecreaseQueuePos);
1244 queueMenu->addAction(actionBottomQueuePos);
1247 QMenu *copySubMenu = listMenu->addMenu(UIThemeManager::instance()->getIcon(u"edit-copy"_qs), tr("&Copy"));
1248 copySubMenu->addAction(actionCopyName);
1249 copySubMenu->addAction(actionCopyHash1);
1250 actionCopyHash1->setEnabled(hasInfohashV1);
1251 copySubMenu->addAction(actionCopyHash2);
1252 actionCopyHash2->setEnabled(hasInfohashV2);
1253 copySubMenu->addAction(actionCopyMagnetLink);
1254 copySubMenu->addAction(actionCopyID);
1256 actionExportTorrent->setToolTip(tr("Exported torrent is not necessarily the same as the imported"));
1257 listMenu->addAction(actionExportTorrent);
1259 listMenu->popup(QCursor::pos());
1262 void TransferListWidget::currentChanged(const QModelIndex &current, const QModelIndex&)
1264 qDebug("CURRENT CHANGED");
1265 BitTorrent::Torrent *torrent = nullptr;
1266 if (current.isValid())
1268 torrent = m_listModel->torrentHandle(mapToSource(current));
1269 // Fix scrolling to the lowermost visible torrent
1270 QMetaObject::invokeMethod(this, [this, current] { scrollTo(current); }, Qt::QueuedConnection);
1272 emit currentTorrentChanged(torrent);
1275 void TransferListWidget::applyCategoryFilter(const QString &category)
1277 if (category.isNull())
1278 m_sortFilterModel->disableCategoryFilter();
1279 else
1280 m_sortFilterModel->setCategoryFilter(category);
1283 void TransferListWidget::applyTagFilter(const QString &tag)
1285 if (tag.isNull())
1286 m_sortFilterModel->disableTagFilter();
1287 else
1288 m_sortFilterModel->setTagFilter(tag);
1291 void TransferListWidget::applyTrackerFilterAll()
1293 m_sortFilterModel->disableTrackerFilter();
1296 void TransferListWidget::applyTrackerFilter(const QSet<BitTorrent::TorrentID> &torrentIDs)
1298 m_sortFilterModel->setTrackerFilter(torrentIDs);
1301 void TransferListWidget::applyFilter(const QString &name, const TransferListModel::Column &type)
1303 m_sortFilterModel->setFilterKeyColumn(type);
1304 const QString pattern = (Preferences::instance()->getRegexAsFilteringPatternForTransferList()
1305 ? name : Utils::String::wildcardToRegexPattern(name));
1306 m_sortFilterModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption));
1309 void TransferListWidget::applyStatusFilter(int f)
1311 m_sortFilterModel->setStatusFilter(static_cast<TorrentFilter::Type>(f));
1312 // Select first item if nothing is selected
1313 if (selectionModel()->selectedRows(0).empty() && (m_sortFilterModel->rowCount() > 0))
1315 qDebug("Nothing is selected, selecting first row: %s", qUtf8Printable(m_sortFilterModel->index(0, TransferListModel::TR_NAME).data().toString()));
1316 selectionModel()->setCurrentIndex(m_sortFilterModel->index(0, TransferListModel::TR_NAME), QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows);
1320 void TransferListWidget::saveSettings()
1322 Preferences::instance()->setTransHeaderState(header()->saveState());
1325 bool TransferListWidget::loadSettings()
1327 return header()->restoreState(Preferences::instance()->getTransHeaderState());
1330 void TransferListWidget::wheelEvent(QWheelEvent *event)
1332 if (event->modifiers() & Qt::ShiftModifier)
1334 // Shift + scroll = horizontal scroll
1335 event->accept();
1336 QWheelEvent scrollHEvent {event->position(), event->globalPosition()
1337 , event->pixelDelta(), event->angleDelta().transposed(), event->buttons()
1338 , event->modifiers(), event->phase(), event->inverted(), event->source()};
1339 QTreeView::wheelEvent(&scrollHEvent);
1340 return;
1343 QTreeView::wheelEvent(event); // event delegated to base class