Don't stuck loading on mismatching info-hashes in resume data
[qBittorrent.git] / src / gui / transferlistwidget.cpp
blob5c06a3c15cd57a8fda42c66cdf96c83462465db3
1 /*
2 * Bittorrent Client using Qt and libtorrent.
3 * Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
4 * Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 * In addition, as a special exception, the copyright holders give permission to
21 * link this program with the OpenSSL project's "OpenSSL" library (or with
22 * modified versions of it that use the same license as the "OpenSSL" library),
23 * and distribute the linked executables. You must obey the GNU General Public
24 * License in all respects for all of the code used other than "OpenSSL". If you
25 * modify file(s), you may extend this exception to your version of the file(s),
26 * but you are not obligated to do so. If you do not wish to do so, delete this
27 * exception statement from your version.
30 #include "transferlistwidget.h"
32 #include <algorithm>
34 #include <QClipboard>
35 #include <QDebug>
36 #include <QFileDialog>
37 #include <QHeaderView>
38 #include <QMenu>
39 #include <QMessageBox>
40 #include <QRegularExpression>
41 #include <QSet>
42 #include <QShortcut>
43 #include <QVector>
44 #include <QWheelEvent>
46 #include "base/bittorrent/session.h"
47 #include "base/bittorrent/torrent.h"
48 #include "base/bittorrent/trackerentry.h"
49 #include "base/global.h"
50 #include "base/logger.h"
51 #include "base/path.h"
52 #include "base/preferences.h"
53 #include "base/torrentfilter.h"
54 #include "base/utils/compare.h"
55 #include "base/utils/fs.h"
56 #include "base/utils/misc.h"
57 #include "base/utils/string.h"
58 #include "autoexpandabledialog.h"
59 #include "deletionconfirmationdialog.h"
60 #include "mainwindow.h"
61 #include "optionsdialog.h"
62 #include "previewselectdialog.h"
63 #include "speedlimitdialog.h"
64 #include "torrentcategorydialog.h"
65 #include "torrentoptionsdialog.h"
66 #include "trackerentriesdialog.h"
67 #include "transferlistdelegate.h"
68 #include "transferlistsortmodel.h"
69 #include "tristateaction.h"
70 #include "uithememanager.h"
71 #include "utils.h"
73 #ifdef Q_OS_MACOS
74 #include "macutilities.h"
75 #endif
77 namespace
79 QVector<BitTorrent::TorrentID> extractIDs(const QVector<BitTorrent::Torrent *> &torrents)
81 QVector<BitTorrent::TorrentID> torrentIDs;
82 torrentIDs.reserve(torrents.size());
83 for (const BitTorrent::Torrent *torrent : torrents)
84 torrentIDs << torrent->id();
85 return torrentIDs;
88 bool torrentContainsPreviewableFiles(const BitTorrent::Torrent *const torrent)
90 if (!torrent->hasMetadata())
91 return false;
93 for (const Path &filePath : asConst(torrent->filePaths()))
95 if (Utils::Misc::isPreviewable(filePath))
96 return true;
99 return false;
102 void openDestinationFolder(const BitTorrent::Torrent *const torrent)
104 const Path contentPath = torrent->contentPath();
105 const Path openedPath = (!contentPath.isEmpty() ? contentPath : torrent->savePath());
106 #ifdef Q_OS_MACOS
107 MacUtils::openFiles({openedPath});
108 #else
109 if (torrent->filesCount() == 1)
110 Utils::Gui::openFolderSelect(openedPath);
111 else
112 Utils::Gui::openPath(openedPath);
113 #endif
116 void removeTorrents(const QVector<BitTorrent::Torrent *> &torrents, const bool isDeleteFileSelected)
118 auto *session = BitTorrent::Session::instance();
119 const DeleteOption deleteOption = isDeleteFileSelected ? DeleteTorrentAndFiles : DeleteTorrent;
120 for (const BitTorrent::Torrent *torrent : torrents)
121 session->deleteTorrent(torrent->id(), deleteOption);
125 TransferListWidget::TransferListWidget(QWidget *parent, MainWindow *mainWindow)
126 : QTreeView {parent}
127 , m_listModel {new TransferListModel {this}}
128 , m_sortFilterModel {new TransferListSortModel {this}}
129 , m_mainWindow {mainWindow}
131 // Load settings
132 const bool columnLoaded = loadSettings();
134 // Create and apply delegate
135 setItemDelegate(new TransferListDelegate {this});
137 m_sortFilterModel->setDynamicSortFilter(true);
138 m_sortFilterModel->setSourceModel(m_listModel);
139 m_sortFilterModel->setFilterKeyColumn(TransferListModel::TR_NAME);
140 m_sortFilterModel->setFilterRole(Qt::DisplayRole);
141 m_sortFilterModel->setSortCaseSensitivity(Qt::CaseInsensitive);
142 m_sortFilterModel->setSortRole(TransferListModel::UnderlyingDataRole);
143 setModel(m_sortFilterModel);
145 // Visual settings
146 setUniformRowHeights(true);
147 setRootIsDecorated(false);
148 setAllColumnsShowFocus(true);
149 setSortingEnabled(true);
150 setSelectionMode(QAbstractItemView::ExtendedSelection);
151 setItemsExpandable(false);
152 setAutoScroll(true);
153 setDragDropMode(QAbstractItemView::DragOnly);
154 #if defined(Q_OS_MACOS)
155 setAttribute(Qt::WA_MacShowFocusRect, false);
156 #endif
157 header()->setFirstSectionMovable(true);
158 header()->setStretchLastSection(false);
159 header()->setTextElideMode(Qt::ElideRight);
161 // Default hidden columns
162 if (!columnLoaded)
164 setColumnHidden(TransferListModel::TR_ADD_DATE, true);
165 setColumnHidden(TransferListModel::TR_SEED_DATE, true);
166 setColumnHidden(TransferListModel::TR_UPLIMIT, true);
167 setColumnHidden(TransferListModel::TR_DLLIMIT, true);
168 setColumnHidden(TransferListModel::TR_TRACKER, true);
169 setColumnHidden(TransferListModel::TR_AMOUNT_DOWNLOADED, true);
170 setColumnHidden(TransferListModel::TR_AMOUNT_UPLOADED, true);
171 setColumnHidden(TransferListModel::TR_AMOUNT_DOWNLOADED_SESSION, true);
172 setColumnHidden(TransferListModel::TR_AMOUNT_UPLOADED_SESSION, true);
173 setColumnHidden(TransferListModel::TR_AMOUNT_LEFT, true);
174 setColumnHidden(TransferListModel::TR_TIME_ELAPSED, true);
175 setColumnHidden(TransferListModel::TR_SAVE_PATH, true);
176 setColumnHidden(TransferListModel::TR_DOWNLOAD_PATH, true);
177 setColumnHidden(TransferListModel::TR_INFOHASH_V1, true);
178 setColumnHidden(TransferListModel::TR_INFOHASH_V2, true);
179 setColumnHidden(TransferListModel::TR_COMPLETED, true);
180 setColumnHidden(TransferListModel::TR_RATIO_LIMIT, true);
181 setColumnHidden(TransferListModel::TR_SEEN_COMPLETE_DATE, true);
182 setColumnHidden(TransferListModel::TR_LAST_ACTIVITY, true);
183 setColumnHidden(TransferListModel::TR_TOTAL_SIZE, true);
184 setColumnHidden(TransferListModel::TR_REANNOUNCE, true);
187 //Ensure that at least one column is visible at all times
188 bool atLeastOne = false;
189 for (int i = 0; i < TransferListModel::NB_COLUMNS; ++i)
191 if (!isColumnHidden(i))
193 atLeastOne = true;
194 break;
197 if (!atLeastOne)
198 setColumnHidden(TransferListModel::TR_NAME, false);
200 //When adding/removing columns between versions some may
201 //end up being size 0 when the new version is launched with
202 //a conf file from the previous version.
203 for (int i = 0; i < TransferListModel::NB_COLUMNS; ++i)
205 if ((columnWidth(i) <= 0) && (!isColumnHidden(i)))
206 resizeColumnToContents(i);
209 setContextMenuPolicy(Qt::CustomContextMenu);
211 // Listen for list events
212 connect(this, &QAbstractItemView::doubleClicked, this, &TransferListWidget::torrentDoubleClicked);
213 connect(this, &QWidget::customContextMenuRequested, this, &TransferListWidget::displayListMenu);
214 header()->setContextMenuPolicy(Qt::CustomContextMenu);
215 connect(header(), &QWidget::customContextMenuRequested, this, &TransferListWidget::displayColumnHeaderMenu);
216 connect(header(), &QHeaderView::sectionMoved, this, &TransferListWidget::saveSettings);
217 connect(header(), &QHeaderView::sectionResized, this, &TransferListWidget::saveSettings);
218 connect(header(), &QHeaderView::sortIndicatorChanged, this, &TransferListWidget::saveSettings);
220 const auto *editHotkey = new QShortcut(Qt::Key_F2, this, nullptr, nullptr, Qt::WidgetShortcut);
221 connect(editHotkey, &QShortcut::activated, this, &TransferListWidget::renameSelectedTorrent);
222 const auto *deleteHotkey = new QShortcut(QKeySequence::Delete, this, nullptr, nullptr, Qt::WidgetShortcut);
223 connect(deleteHotkey, &QShortcut::activated, this, &TransferListWidget::softDeleteSelectedTorrents);
224 const auto *permDeleteHotkey = new QShortcut((Qt::SHIFT | Qt::Key_Delete), this, nullptr, nullptr, Qt::WidgetShortcut);
225 connect(permDeleteHotkey, &QShortcut::activated, this, &TransferListWidget::permDeleteSelectedTorrents);
226 const auto *doubleClickHotkeyReturn = new QShortcut(Qt::Key_Return, this, nullptr, nullptr, Qt::WidgetShortcut);
227 connect(doubleClickHotkeyReturn, &QShortcut::activated, this, &TransferListWidget::torrentDoubleClicked);
228 const auto *doubleClickHotkeyEnter = new QShortcut(Qt::Key_Enter, this, nullptr, nullptr, Qt::WidgetShortcut);
229 connect(doubleClickHotkeyEnter, &QShortcut::activated, this, &TransferListWidget::torrentDoubleClicked);
230 const auto *recheckHotkey = new QShortcut((Qt::CTRL | Qt::Key_R), this, nullptr, nullptr, Qt::WidgetShortcut);
231 connect(recheckHotkey, &QShortcut::activated, this, &TransferListWidget::recheckSelectedTorrents);
232 const auto *forceStartHotkey = new QShortcut((Qt::CTRL | Qt::Key_M), this, nullptr, nullptr, Qt::WidgetShortcut);
233 connect(forceStartHotkey, &QShortcut::activated, this, &TransferListWidget::forceStartSelectedTorrents);
236 TransferListWidget::~TransferListWidget()
238 // Save settings
239 saveSettings();
242 TransferListModel *TransferListWidget::getSourceModel() const
244 return m_listModel;
247 void TransferListWidget::previewFile(const Path &filePath)
249 Utils::Gui::openPath(filePath);
252 QModelIndex TransferListWidget::mapToSource(const QModelIndex &index) const
254 Q_ASSERT(index.isValid());
255 if (index.model() == m_sortFilterModel)
256 return m_sortFilterModel->mapToSource(index);
257 return index;
260 QModelIndexList TransferListWidget::mapToSource(const QModelIndexList &indexes) const
262 QModelIndexList result;
263 result.reserve(indexes.size());
264 for (const QModelIndex &index : indexes)
265 result.append(mapToSource(index));
267 return result;
270 QModelIndex TransferListWidget::mapFromSource(const QModelIndex &index) const
272 Q_ASSERT(index.isValid());
273 Q_ASSERT(index.model() == m_sortFilterModel);
274 return m_sortFilterModel->mapFromSource(index);
277 void TransferListWidget::torrentDoubleClicked()
279 const QModelIndexList selectedIndexes = selectionModel()->selectedRows();
280 if ((selectedIndexes.size() != 1) || !selectedIndexes.first().isValid())
281 return;
283 const QModelIndex index = m_listModel->index(mapToSource(selectedIndexes.first()).row());
284 BitTorrent::Torrent *const torrent = m_listModel->torrentHandle(index);
285 if (!torrent)
286 return;
288 int action;
289 if (torrent->isFinished())
290 action = Preferences::instance()->getActionOnDblClOnTorrentFn();
291 else
292 action = Preferences::instance()->getActionOnDblClOnTorrentDl();
294 switch (action)
296 case TOGGLE_PAUSE:
297 if (torrent->isPaused())
298 torrent->resume();
299 else
300 torrent->pause();
301 break;
302 case PREVIEW_FILE:
303 if (torrentContainsPreviewableFiles(torrent))
305 auto *dialog = new PreviewSelectDialog(this, torrent);
306 dialog->setAttribute(Qt::WA_DeleteOnClose);
307 connect(dialog, &PreviewSelectDialog::readyToPreviewFile, this, &TransferListWidget::previewFile);
308 dialog->show();
310 else
312 openDestinationFolder(torrent);
314 break;
315 case OPEN_DEST:
316 openDestinationFolder(torrent);
317 break;
318 case SHOW_OPTIONS:
319 setTorrentOptions();
320 break;
324 QVector<BitTorrent::Torrent *> TransferListWidget::getSelectedTorrents() const
326 const QModelIndexList selectedRows = selectionModel()->selectedRows();
328 QVector<BitTorrent::Torrent *> torrents;
329 torrents.reserve(selectedRows.size());
330 for (const QModelIndex &index : selectedRows)
331 torrents << m_listModel->torrentHandle(mapToSource(index));
332 return torrents;
335 QVector<BitTorrent::Torrent *> TransferListWidget::getVisibleTorrents() const
337 const int visibleTorrentsCount = m_sortFilterModel->rowCount();
339 QVector<BitTorrent::Torrent *> torrents;
340 torrents.reserve(visibleTorrentsCount);
341 for (int i = 0; i < visibleTorrentsCount; ++i)
342 torrents << m_listModel->torrentHandle(mapToSource(m_sortFilterModel->index(i, 0)));
343 return torrents;
346 void TransferListWidget::setSelectedTorrentsLocation()
348 const QVector<BitTorrent::Torrent *> torrents = getSelectedTorrents();
349 if (torrents.isEmpty())
350 return;
352 const Path oldLocation = torrents[0]->savePath();
354 auto *fileDialog = new QFileDialog(this, tr("Choose save path"), oldLocation.data());
355 fileDialog->setAttribute(Qt::WA_DeleteOnClose);
356 fileDialog->setFileMode(QFileDialog::Directory);
357 fileDialog->setOptions(QFileDialog::DontConfirmOverwrite | QFileDialog::ShowDirsOnly | QFileDialog::HideNameFilterDetails);
358 connect(fileDialog, &QDialog::accepted, this, [this, fileDialog]()
360 const QVector<BitTorrent::Torrent *> torrents = getSelectedTorrents();
361 if (torrents.isEmpty())
362 return;
364 const Path newLocation {fileDialog->selectedFiles().constFirst()};
365 if (!newLocation.exists())
366 return;
368 // Actually move storage
369 for (BitTorrent::Torrent *const torrent : torrents)
371 torrent->setAutoTMMEnabled(false);
372 torrent->setSavePath(newLocation);
376 fileDialog->open();
379 void TransferListWidget::pauseAllTorrents()
381 if (Preferences::instance()->confirmPauseAndResumeAll())
383 // Show confirmation if user would really like to Pause All
384 const QMessageBox::StandardButton ret = QMessageBox::question(this, tr("Confirm pause")
385 , tr("Would you like to pause all torrents?"), (QMessageBox::Yes | QMessageBox::No));
387 if (ret != QMessageBox::Yes)
388 return;
391 for (BitTorrent::Torrent *const torrent : asConst(BitTorrent::Session::instance()->torrents()))
392 torrent->pause();
395 void TransferListWidget::resumeAllTorrents()
397 if (Preferences::instance()->confirmPauseAndResumeAll())
399 // Show confirmation if user would really like to Resume All
400 const QMessageBox::StandardButton ret = QMessageBox::question(this, tr("Confirm resume")
401 , tr("Would you like to resume all torrents?"), (QMessageBox::Yes | QMessageBox::No));
403 if (ret != QMessageBox::Yes)
404 return;
407 for (BitTorrent::Torrent *const torrent : asConst(BitTorrent::Session::instance()->torrents()))
408 torrent->resume();
411 void TransferListWidget::startSelectedTorrents()
413 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
414 torrent->resume();
417 void TransferListWidget::forceStartSelectedTorrents()
419 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
420 torrent->resume(BitTorrent::TorrentOperatingMode::Forced);
423 void TransferListWidget::startVisibleTorrents()
425 for (BitTorrent::Torrent *const torrent : asConst(getVisibleTorrents()))
426 torrent->resume();
429 void TransferListWidget::pauseSelectedTorrents()
431 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
432 torrent->pause();
435 void TransferListWidget::pauseVisibleTorrents()
437 for (BitTorrent::Torrent *const torrent : asConst(getVisibleTorrents()))
438 torrent->pause();
441 void TransferListWidget::softDeleteSelectedTorrents()
443 deleteSelectedTorrents(false);
446 void TransferListWidget::permDeleteSelectedTorrents()
448 deleteSelectedTorrents(true);
451 void TransferListWidget::deleteSelectedTorrents(const bool deleteLocalFiles)
453 if (m_mainWindow->currentTabWidget() != this) return;
455 const QVector<BitTorrent::Torrent *> torrents = getSelectedTorrents();
456 if (torrents.empty()) return;
458 if (Preferences::instance()->confirmTorrentDeletion())
460 auto *dialog = new DeletionConfirmationDialog(this, torrents.size(), torrents[0]->name(), deleteLocalFiles);
461 dialog->setAttribute(Qt::WA_DeleteOnClose);
462 connect(dialog, &DeletionConfirmationDialog::accepted, this, [this, dialog]()
464 // Some torrents might be removed when waiting for user input, so refetch the torrent list
465 // NOTE: this will only work when dialog is modal
466 removeTorrents(getSelectedTorrents(), dialog->isDeleteFileSelected());
468 dialog->open();
470 else
472 removeTorrents(torrents, deleteLocalFiles);
476 void TransferListWidget::deleteVisibleTorrents()
478 const QVector<BitTorrent::Torrent *> torrents = getVisibleTorrents();
479 if (torrents.empty()) return;
481 if (Preferences::instance()->confirmTorrentDeletion())
483 auto *dialog = new DeletionConfirmationDialog(this, torrents.size(), torrents[0]->name(), false);
484 dialog->setAttribute(Qt::WA_DeleteOnClose);
485 connect(dialog, &DeletionConfirmationDialog::accepted, this, [this, dialog]()
487 // Some torrents might be removed when waiting for user input, so refetch the torrent list
488 // NOTE: this will only work when dialog is modal
489 removeTorrents(getVisibleTorrents(), dialog->isDeleteFileSelected());
491 dialog->open();
493 else
495 removeTorrents(torrents, false);
499 void TransferListWidget::increaseQueuePosSelectedTorrents()
501 qDebug() << Q_FUNC_INFO;
502 if (m_mainWindow->currentTabWidget() == this)
503 BitTorrent::Session::instance()->increaseTorrentsQueuePos(extractIDs(getSelectedTorrents()));
506 void TransferListWidget::decreaseQueuePosSelectedTorrents()
508 qDebug() << Q_FUNC_INFO;
509 if (m_mainWindow->currentTabWidget() == this)
510 BitTorrent::Session::instance()->decreaseTorrentsQueuePos(extractIDs(getSelectedTorrents()));
513 void TransferListWidget::topQueuePosSelectedTorrents()
515 if (m_mainWindow->currentTabWidget() == this)
516 BitTorrent::Session::instance()->topTorrentsQueuePos(extractIDs(getSelectedTorrents()));
519 void TransferListWidget::bottomQueuePosSelectedTorrents()
521 if (m_mainWindow->currentTabWidget() == this)
522 BitTorrent::Session::instance()->bottomTorrentsQueuePos(extractIDs(getSelectedTorrents()));
525 void TransferListWidget::copySelectedMagnetURIs() const
527 QStringList magnetUris;
528 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
529 magnetUris << torrent->createMagnetURI();
531 qApp->clipboard()->setText(magnetUris.join(u'\n'));
534 void TransferListWidget::copySelectedNames() const
536 QStringList torrentNames;
537 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
538 torrentNames << torrent->name();
540 qApp->clipboard()->setText(torrentNames.join(u'\n'));
543 void TransferListWidget::copySelectedInfohashes(const CopyInfohashPolicy policy) const
545 const auto selectedTorrents = getSelectedTorrents();
546 QStringList infoHashes;
547 infoHashes.reserve(selectedTorrents.size());
548 switch (policy)
550 case CopyInfohashPolicy::Version1:
551 for (const BitTorrent::Torrent *torrent : selectedTorrents)
553 if (const auto infoHash = torrent->infoHash().v1(); infoHash.isValid())
554 infoHashes << infoHash.toString();
556 break;
557 case CopyInfohashPolicy::Version2:
558 for (const BitTorrent::Torrent *torrent : selectedTorrents)
560 if (const auto infoHash = torrent->infoHash().v2(); infoHash.isValid())
561 infoHashes << infoHash.toString();
563 break;
566 qApp->clipboard()->setText(infoHashes.join(u'\n'));
569 void TransferListWidget::copySelectedIDs() const
571 QStringList torrentIDs;
572 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
573 torrentIDs << torrent->id().toString();
575 qApp->clipboard()->setText(torrentIDs.join(u'\n'));
578 void TransferListWidget::copySelectedComments() const
580 QStringList torrentComments;
581 for (const BitTorrent::Torrent *torrent : asConst(getSelectedTorrents()))
583 if (!torrent->comment().isEmpty())
584 torrentComments << torrent->comment();
587 qApp->clipboard()->setText(torrentComments.join(u"\n---------\n"_s));
590 void TransferListWidget::hideQueuePosColumn(bool hide)
592 setColumnHidden(TransferListModel::TR_QUEUE_POSITION, hide);
593 if (!hide && (columnWidth(TransferListModel::TR_QUEUE_POSITION) == 0))
594 resizeColumnToContents(TransferListModel::TR_QUEUE_POSITION);
597 void TransferListWidget::openSelectedTorrentsFolder() const
599 QSet<Path> paths;
600 #ifdef Q_OS_MACOS
601 // On macOS you expect both the files and folders to be opened in their parent
602 // folders prehilighted for opening, so we use a custom method.
603 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
605 const Path contentPath = torrent->contentPath();
606 paths.insert(!contentPath.isEmpty() ? contentPath : torrent->savePath());
608 MacUtils::openFiles(PathList(paths.cbegin(), paths.cend()));
609 #else
610 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
612 const Path contentPath = torrent->contentPath();
613 const Path openedPath = (!contentPath.isEmpty() ? contentPath : torrent->savePath());
614 if (!paths.contains(openedPath))
616 if (torrent->filesCount() == 1)
617 Utils::Gui::openFolderSelect(openedPath);
618 else
619 Utils::Gui::openPath(openedPath);
621 paths.insert(openedPath);
623 #endif // Q_OS_MACOS
626 void TransferListWidget::previewSelectedTorrents()
628 for (const BitTorrent::Torrent *torrent : asConst(getSelectedTorrents()))
630 if (torrentContainsPreviewableFiles(torrent))
632 auto *dialog = new PreviewSelectDialog(this, torrent);
633 dialog->setAttribute(Qt::WA_DeleteOnClose);
634 connect(dialog, &PreviewSelectDialog::readyToPreviewFile, this, &TransferListWidget::previewFile);
635 dialog->show();
637 else
639 QMessageBox::critical(this, tr("Unable to preview"), tr("The selected torrent \"%1\" does not contain previewable files")
640 .arg(torrent->name()));
645 void TransferListWidget::setTorrentOptions()
647 const QVector<BitTorrent::Torrent *> selectedTorrents = getSelectedTorrents();
648 if (selectedTorrents.empty()) return;
650 auto *dialog = new TorrentOptionsDialog {this, selectedTorrents};
651 dialog->setAttribute(Qt::WA_DeleteOnClose);
652 dialog->open();
655 void TransferListWidget::recheckSelectedTorrents()
657 if (Preferences::instance()->confirmTorrentRecheck())
659 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);
660 if (ret != QMessageBox::Yes) return;
663 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
664 torrent->forceRecheck();
667 void TransferListWidget::reannounceSelectedTorrents()
669 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
670 torrent->forceReannounce();
673 int TransferListWidget::visibleColumnsCount() const
675 int count = 0;
676 for (int i = 0, iMax = header()->count(); i < iMax; ++i)
678 if (!isColumnHidden(i))
679 ++count;
682 return count;
685 // hide/show columns menu
686 void TransferListWidget::displayColumnHeaderMenu()
688 auto *menu = new QMenu(this);
689 menu->setAttribute(Qt::WA_DeleteOnClose);
690 menu->setTitle(tr("Column visibility"));
691 menu->setToolTipsVisible(true);
693 for (int i = 0; i < TransferListModel::NB_COLUMNS; ++i)
695 if (!BitTorrent::Session::instance()->isQueueingSystemEnabled() && (i == TransferListModel::TR_QUEUE_POSITION))
696 continue;
698 const auto columnName = m_listModel->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString();
699 QAction *action = menu->addAction(columnName, this, [this, i](const bool checked)
701 if (!checked && (visibleColumnsCount() <= 1))
702 return;
704 setColumnHidden(i, !checked);
706 if (checked && (columnWidth(i) <= 5))
707 resizeColumnToContents(i);
709 saveSettings();
711 action->setCheckable(true);
712 action->setChecked(!isColumnHidden(i));
715 menu->addSeparator();
716 QAction *resizeAction = menu->addAction(tr("Resize columns"), this, [this]()
718 for (int i = 0, count = header()->count(); i < count; ++i)
720 if (!isColumnHidden(i))
721 resizeColumnToContents(i);
723 saveSettings();
725 resizeAction->setToolTip(tr("Resize all non-hidden columns to the size of their contents"));
727 menu->popup(QCursor::pos());
730 void TransferListWidget::setSelectedTorrentsSuperSeeding(const bool enabled) const
732 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
734 if (torrent->hasMetadata())
735 torrent->setSuperSeeding(enabled);
739 void TransferListWidget::setSelectedTorrentsSequentialDownload(const bool enabled) const
741 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
742 torrent->setSequentialDownload(enabled);
745 void TransferListWidget::setSelectedFirstLastPiecePrio(const bool enabled) const
747 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
748 torrent->setFirstLastPiecePriority(enabled);
751 void TransferListWidget::setSelectedAutoTMMEnabled(const bool enabled)
753 if (enabled)
755 const QMessageBox::StandardButton btn = QMessageBox::question(this, tr("Enable automatic torrent management")
756 , tr("Are you sure you want to enable Automatic Torrent Management for the selected torrent(s)? They may be relocated.")
757 , (QMessageBox::Yes | QMessageBox::No), QMessageBox::Yes);
758 if (btn != QMessageBox::Yes) return;
761 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
762 torrent->setAutoTMMEnabled(enabled);
765 void TransferListWidget::askNewCategoryForSelection()
767 const QString newCategoryName = TorrentCategoryDialog::createCategory(this);
768 if (!newCategoryName.isEmpty())
769 setSelectionCategory(newCategoryName);
772 void TransferListWidget::askAddTagsForSelection()
774 const TagSet tags = askTagsForSelection(tr("Add Tags"));
775 for (const Tag &tag : tags)
776 addSelectionTag(tag);
779 void TransferListWidget::editTorrentTrackers()
781 const QVector<BitTorrent::Torrent *> torrents = getSelectedTorrents();
782 QVector<BitTorrent::TrackerEntry> commonTrackers;
784 if (!torrents.empty())
786 commonTrackers = torrents[0]->trackers();
788 for (const BitTorrent::Torrent *torrent : torrents)
790 QSet<BitTorrent::TrackerEntry> trackerSet;
792 for (const BitTorrent::TrackerEntry &entry : asConst(torrent->trackers()))
793 trackerSet.insert(entry);
795 commonTrackers.erase(std::remove_if(commonTrackers.begin(), commonTrackers.end()
796 , [&trackerSet](const BitTorrent::TrackerEntry &entry) { return !trackerSet.contains(entry); })
797 , commonTrackers.end());
801 auto *trackerDialog = new TrackerEntriesDialog(this);
802 trackerDialog->setAttribute(Qt::WA_DeleteOnClose);
803 trackerDialog->setTrackers(commonTrackers);
805 connect(trackerDialog, &QDialog::accepted, this, [torrents, trackerDialog]()
807 for (BitTorrent::Torrent *torrent : torrents)
808 torrent->replaceTrackers(trackerDialog->trackers());
811 trackerDialog->open();
814 void TransferListWidget::exportTorrent()
816 if (getSelectedTorrents().isEmpty())
817 return;
819 auto *fileDialog = new QFileDialog(this, tr("Choose folder to save exported .torrent files"));
820 fileDialog->setAttribute(Qt::WA_DeleteOnClose);
821 fileDialog->setFileMode(QFileDialog::Directory);
822 fileDialog->setOptions(QFileDialog::ShowDirsOnly);
823 connect(fileDialog, &QFileDialog::fileSelected, this, [this](const QString &dir)
825 const QVector<BitTorrent::Torrent *> torrents = getSelectedTorrents();
826 if (torrents.isEmpty())
827 return;
829 const Path savePath {dir};
830 if (!savePath.exists())
831 return;
833 const QString errorMsg = tr("Export .torrent file failed. Torrent: \"%1\". Save path: \"%2\". Reason: \"%3\"");
835 bool hasError = false;
836 for (const BitTorrent::Torrent *torrent : torrents)
838 const QString validName = Utils::Fs::toValidFileName(torrent->name(), u"_"_s);
839 const Path filePath = savePath / Path(validName + u".torrent");
840 if (filePath.exists())
842 LogMsg(errorMsg.arg(torrent->name(), filePath.toString(), tr("A file with the same name already exists")) , Log::WARNING);
843 hasError = true;
844 continue;
847 const nonstd::expected<void, QString> result = torrent->exportToFile(filePath);
848 if (!result)
850 LogMsg(errorMsg.arg(torrent->name(), filePath.toString(), result.error()) , Log::WARNING);
851 hasError = true;
852 continue;
856 if (hasError)
858 QMessageBox::warning(this, tr("Export .torrent file error")
859 , tr("Errors occurred when exporting .torrent files. Check execution log for details."));
863 fileDialog->open();
866 void TransferListWidget::confirmRemoveAllTagsForSelection()
868 QMessageBox::StandardButton response = QMessageBox::question(
869 this, tr("Remove All Tags"), tr("Remove all tags from selected torrents?"),
870 QMessageBox::Yes | QMessageBox::No);
871 if (response == QMessageBox::Yes)
872 clearSelectionTags();
875 TagSet TransferListWidget::askTagsForSelection(const QString &dialogTitle)
877 TagSet tags;
878 bool invalid = true;
879 while (invalid)
881 bool ok = false;
882 invalid = false;
883 const QString tagsInput = AutoExpandableDialog::getText(
884 this, dialogTitle, tr("Comma-separated tags:"), QLineEdit::Normal, {}, &ok).trimmed();
885 if (!ok || tagsInput.isEmpty())
886 return {};
888 const QStringList tagStrings = tagsInput.split(u',', Qt::SkipEmptyParts);
889 tags.clear();
890 for (const QString &tagStr : tagStrings)
892 const Tag tag {tagStr};
893 if (!tag.isValid())
895 QMessageBox::warning(this, tr("Invalid tag"), tr("Tag name: '%1' is invalid").arg(tag.toString()));
896 invalid = true;
899 if (!invalid)
900 tags.insert(tag);
904 return tags;
907 void TransferListWidget::applyToSelectedTorrents(const std::function<void (BitTorrent::Torrent *const)> &fn)
909 // Changing the data may affect the layout of the sort/filter model, which in turn may invalidate
910 // the indexes previously obtained from selection model before we process them all.
911 // Therefore, we must map all the selected indexes to source before start processing them.
912 const QModelIndexList sourceRows = mapToSource(selectionModel()->selectedRows());
913 for (const QModelIndex &index : sourceRows)
915 BitTorrent::Torrent *const torrent = m_listModel->torrentHandle(index);
916 Q_ASSERT(torrent);
917 fn(torrent);
921 void TransferListWidget::renameSelectedTorrent()
923 const QModelIndexList selectedIndexes = selectionModel()->selectedRows();
924 if ((selectedIndexes.size() != 1) || !selectedIndexes.first().isValid())
925 return;
927 const QModelIndex mi = m_listModel->index(mapToSource(selectedIndexes.first()).row(), TransferListModel::TR_NAME);
928 BitTorrent::Torrent *const torrent = m_listModel->torrentHandle(mi);
929 if (!torrent)
930 return;
932 // Ask for a new Name
933 bool ok = false;
934 QString name = AutoExpandableDialog::getText(this, tr("Rename"), tr("New name:"), QLineEdit::Normal, torrent->name(), &ok);
935 if (ok && !name.isEmpty())
937 name.replace(QRegularExpression(u"\r?\n|\r"_s), u" "_s);
938 // Rename the torrent
939 m_listModel->setData(mi, name, Qt::DisplayRole);
943 void TransferListWidget::setSelectionCategory(const QString &category)
945 applyToSelectedTorrents([&category](BitTorrent::Torrent *torrent) { torrent->setCategory(category); });
948 void TransferListWidget::addSelectionTag(const Tag &tag)
950 applyToSelectedTorrents([&tag](BitTorrent::Torrent *const torrent) { torrent->addTag(tag); });
953 void TransferListWidget::removeSelectionTag(const Tag &tag)
955 applyToSelectedTorrents([&tag](BitTorrent::Torrent *const torrent) { torrent->removeTag(tag); });
958 void TransferListWidget::clearSelectionTags()
960 applyToSelectedTorrents([](BitTorrent::Torrent *const torrent) { torrent->removeAllTags(); });
963 void TransferListWidget::displayListMenu()
965 const QModelIndexList selectedIndexes = selectionModel()->selectedRows();
966 if (selectedIndexes.isEmpty())
967 return;
969 auto *listMenu = new QMenu(this);
970 listMenu->setAttribute(Qt::WA_DeleteOnClose);
971 listMenu->setToolTipsVisible(true);
973 // Create actions
975 auto *actionStart = new QAction(UIThemeManager::instance()->getIcon(u"torrent-start"_s, u"media-playback-start"_s), tr("&Resume", "Resume/start the torrent"), listMenu);
976 connect(actionStart, &QAction::triggered, this, &TransferListWidget::startSelectedTorrents);
977 auto *actionPause = new QAction(UIThemeManager::instance()->getIcon(u"torrent-stop"_s, u"media-playback-pause"_s), tr("&Pause", "Pause the torrent"), listMenu);
978 connect(actionPause, &QAction::triggered, this, &TransferListWidget::pauseSelectedTorrents);
979 auto *actionForceStart = new QAction(UIThemeManager::instance()->getIcon(u"torrent-start-forced"_s, u"media-playback-start"_s), tr("Force Resu&me", "Force Resume/start the torrent"), listMenu);
980 connect(actionForceStart, &QAction::triggered, this, &TransferListWidget::forceStartSelectedTorrents);
981 auto *actionDelete = new QAction(UIThemeManager::instance()->getIcon(u"list-remove"_s), tr("&Remove", "Remove the torrent"), listMenu);
982 connect(actionDelete, &QAction::triggered, this, &TransferListWidget::softDeleteSelectedTorrents);
983 auto *actionPreviewFile = new QAction(UIThemeManager::instance()->getIcon(u"view-preview"_s), tr("Pre&view file..."), listMenu);
984 connect(actionPreviewFile, &QAction::triggered, this, &TransferListWidget::previewSelectedTorrents);
985 auto *actionTorrentOptions = new QAction(UIThemeManager::instance()->getIcon(u"configure"_s), tr("Torrent &options..."), listMenu);
986 connect(actionTorrentOptions, &QAction::triggered, this, &TransferListWidget::setTorrentOptions);
987 auto *actionOpenDestinationFolder = new QAction(UIThemeManager::instance()->getIcon(u"directory"_s), tr("Open destination &folder"), listMenu);
988 connect(actionOpenDestinationFolder, &QAction::triggered, this, &TransferListWidget::openSelectedTorrentsFolder);
989 auto *actionIncreaseQueuePos = new QAction(UIThemeManager::instance()->getIcon(u"go-up"_s), tr("Move &up", "i.e. move up in the queue"), listMenu);
990 connect(actionIncreaseQueuePos, &QAction::triggered, this, &TransferListWidget::increaseQueuePosSelectedTorrents);
991 auto *actionDecreaseQueuePos = new QAction(UIThemeManager::instance()->getIcon(u"go-down"_s), tr("Move &down", "i.e. Move down in the queue"), listMenu);
992 connect(actionDecreaseQueuePos, &QAction::triggered, this, &TransferListWidget::decreaseQueuePosSelectedTorrents);
993 auto *actionTopQueuePos = new QAction(UIThemeManager::instance()->getIcon(u"go-top"_s), tr("Move to &top", "i.e. Move to top of the queue"), listMenu);
994 connect(actionTopQueuePos, &QAction::triggered, this, &TransferListWidget::topQueuePosSelectedTorrents);
995 auto *actionBottomQueuePos = new QAction(UIThemeManager::instance()->getIcon(u"go-bottom"_s), tr("Move to &bottom", "i.e. Move to bottom of the queue"), listMenu);
996 connect(actionBottomQueuePos, &QAction::triggered, this, &TransferListWidget::bottomQueuePosSelectedTorrents);
997 auto *actionSetTorrentPath = new QAction(UIThemeManager::instance()->getIcon(u"set-location"_s, u"inode-directory"_s), tr("Set loc&ation..."), listMenu);
998 connect(actionSetTorrentPath, &QAction::triggered, this, &TransferListWidget::setSelectedTorrentsLocation);
999 auto *actionForceRecheck = new QAction(UIThemeManager::instance()->getIcon(u"force-recheck"_s, u"document-edit-verify"_s), tr("Force rec&heck"), listMenu);
1000 connect(actionForceRecheck, &QAction::triggered, this, &TransferListWidget::recheckSelectedTorrents);
1001 auto *actionForceReannounce = new QAction(UIThemeManager::instance()->getIcon(u"reannounce"_s, u"document-edit-verify"_s), tr("Force r&eannounce"), listMenu);
1002 connect(actionForceReannounce, &QAction::triggered, this, &TransferListWidget::reannounceSelectedTorrents);
1003 auto *actionCopyMagnetLink = new QAction(UIThemeManager::instance()->getIcon(u"torrent-magnet"_s, u"kt-magnet"_s), tr("&Magnet link"), listMenu);
1004 connect(actionCopyMagnetLink, &QAction::triggered, this, &TransferListWidget::copySelectedMagnetURIs);
1005 auto *actionCopyID = new QAction(UIThemeManager::instance()->getIcon(u"help-about"_s, u"edit-copy"_s), tr("Torrent &ID"), listMenu);
1006 connect(actionCopyID, &QAction::triggered, this, &TransferListWidget::copySelectedIDs);
1007 auto *actionCopyComment = new QAction(UIThemeManager::instance()->getIcon(u"edit-copy"_s), tr("&Comment"), listMenu);
1008 connect(actionCopyComment, &QAction::triggered, this, &TransferListWidget::copySelectedComments);
1009 auto *actionCopyName = new QAction(UIThemeManager::instance()->getIcon(u"name"_s, u"edit-copy"_s), tr("&Name"), listMenu);
1010 connect(actionCopyName, &QAction::triggered, this, &TransferListWidget::copySelectedNames);
1011 auto *actionCopyHash1 = new QAction(UIThemeManager::instance()->getIcon(u"hash"_s, u"edit-copy"_s), tr("Info &hash v1"), listMenu);
1012 connect(actionCopyHash1, &QAction::triggered, this, [this]() { copySelectedInfohashes(CopyInfohashPolicy::Version1); });
1013 auto *actionCopyHash2 = new QAction(UIThemeManager::instance()->getIcon(u"hash"_s, u"edit-copy"_s), tr("Info h&ash v2"), listMenu);
1014 connect(actionCopyHash2, &QAction::triggered, this, [this]() { copySelectedInfohashes(CopyInfohashPolicy::Version2); });
1015 auto *actionSuperSeedingMode = new TriStateAction(tr("Super seeding mode"), listMenu);
1016 connect(actionSuperSeedingMode, &QAction::triggered, this, &TransferListWidget::setSelectedTorrentsSuperSeeding);
1017 auto *actionRename = new QAction(UIThemeManager::instance()->getIcon(u"edit-rename"_s), tr("Re&name..."), listMenu);
1018 connect(actionRename, &QAction::triggered, this, &TransferListWidget::renameSelectedTorrent);
1019 auto *actionSequentialDownload = new TriStateAction(tr("Download in sequential order"), listMenu);
1020 connect(actionSequentialDownload, &QAction::triggered, this, &TransferListWidget::setSelectedTorrentsSequentialDownload);
1021 auto *actionFirstLastPiecePrio = new TriStateAction(tr("Download first and last pieces first"), listMenu);
1022 connect(actionFirstLastPiecePrio, &QAction::triggered, this, &TransferListWidget::setSelectedFirstLastPiecePrio);
1023 auto *actionAutoTMM = new TriStateAction(tr("Automatic Torrent Management"), listMenu);
1024 actionAutoTMM->setToolTip(tr("Automatic mode means that various torrent properties (e.g. save path) will be decided by the associated category"));
1025 connect(actionAutoTMM, &QAction::triggered, this, &TransferListWidget::setSelectedAutoTMMEnabled);
1026 auto *actionEditTracker = new QAction(UIThemeManager::instance()->getIcon(u"edit-rename"_s), tr("Edit trac&kers..."), listMenu);
1027 connect(actionEditTracker, &QAction::triggered, this, &TransferListWidget::editTorrentTrackers);
1028 auto *actionExportTorrent = new QAction(UIThemeManager::instance()->getIcon(u"edit-copy"_s), tr("E&xport .torrent..."), listMenu);
1029 connect(actionExportTorrent, &QAction::triggered, this, &TransferListWidget::exportTorrent);
1030 // End of actions
1032 // Enable/disable pause/start action given the DL state
1033 bool needsPause = false, needsStart = false, needsForce = false, needsPreview = false;
1034 bool allSameSuperSeeding = true;
1035 bool superSeedingMode = false;
1036 bool allSameSequentialDownloadMode = true, allSamePrioFirstlast = true;
1037 bool sequentialDownloadMode = false, prioritizeFirstLast = false;
1038 bool oneHasMetadata = false, oneNotFinished = false;
1039 bool allSameCategory = true;
1040 bool allSameAutoTMM = true;
1041 bool firstAutoTMM = false;
1042 QString firstCategory;
1043 bool first = true;
1044 TagSet tagsInAny;
1045 TagSet tagsInAll;
1046 bool hasInfohashV1 = false, hasInfohashV2 = false;
1047 bool oneCanForceReannounce = false;
1049 for (const QModelIndex &index : selectedIndexes)
1051 // Get the file name
1052 // Get handle and pause the torrent
1053 const BitTorrent::Torrent *torrent = m_listModel->torrentHandle(mapToSource(index));
1054 if (!torrent) continue;
1056 if (firstCategory.isEmpty() && first)
1057 firstCategory = torrent->category();
1058 if (firstCategory != torrent->category())
1059 allSameCategory = false;
1061 const TagSet torrentTags = torrent->tags();
1062 tagsInAny.unite(torrentTags);
1064 if (first)
1066 firstAutoTMM = torrent->isAutoTMMEnabled();
1067 tagsInAll = torrentTags;
1069 else
1071 tagsInAll.intersect(torrentTags);
1074 if (firstAutoTMM != torrent->isAutoTMMEnabled())
1075 allSameAutoTMM = false;
1077 if (torrent->hasMetadata())
1078 oneHasMetadata = true;
1079 if (!torrent->isFinished())
1081 oneNotFinished = true;
1082 if (first)
1084 sequentialDownloadMode = torrent->isSequentialDownload();
1085 prioritizeFirstLast = torrent->hasFirstLastPiecePriority();
1087 else
1089 if (sequentialDownloadMode != torrent->isSequentialDownload())
1090 allSameSequentialDownloadMode = false;
1091 if (prioritizeFirstLast != torrent->hasFirstLastPiecePriority())
1092 allSamePrioFirstlast = false;
1095 else
1097 if (!oneNotFinished && allSameSuperSeeding && torrent->hasMetadata())
1099 if (first)
1100 superSeedingMode = torrent->superSeeding();
1101 else if (superSeedingMode != torrent->superSeeding())
1102 allSameSuperSeeding = false;
1106 if (!torrent->isForced())
1107 needsForce = true;
1108 else
1109 needsStart = true;
1111 const bool isPaused = torrent->isPaused();
1112 if (isPaused)
1113 needsStart = true;
1114 else
1115 needsPause = true;
1117 if (torrent->isErrored() || torrent->hasMissingFiles())
1119 // If torrent is in "errored" or "missing files" state
1120 // it cannot keep further processing until you restart it.
1121 needsStart = true;
1122 needsForce = true;
1125 if (torrent->hasMetadata())
1126 needsPreview = true;
1128 if (!hasInfohashV1 && torrent->infoHash().v1().isValid())
1129 hasInfohashV1 = true;
1130 if (!hasInfohashV2 && torrent->infoHash().v2().isValid())
1131 hasInfohashV2 = true;
1133 first = false;
1135 const bool rechecking = torrent->isChecking();
1136 if (rechecking)
1138 needsStart = true;
1139 needsPause = true;
1142 const bool queued = (BitTorrent::Session::instance()->isQueueingSystemEnabled() && torrent->isQueued());
1144 if (!isPaused && !rechecking && !queued)
1145 oneCanForceReannounce = true;
1147 if (oneHasMetadata && oneNotFinished && !allSameSequentialDownloadMode
1148 && !allSamePrioFirstlast && !allSameSuperSeeding && !allSameCategory
1149 && needsStart && needsForce && needsPause && needsPreview && !allSameAutoTMM
1150 && hasInfohashV1 && hasInfohashV2 && oneCanForceReannounce)
1152 break;
1156 if (needsStart)
1157 listMenu->addAction(actionStart);
1158 if (needsPause)
1159 listMenu->addAction(actionPause);
1160 if (needsForce)
1161 listMenu->addAction(actionForceStart);
1162 listMenu->addSeparator();
1163 listMenu->addAction(actionDelete);
1164 listMenu->addSeparator();
1165 listMenu->addAction(actionSetTorrentPath);
1166 if (selectedIndexes.size() == 1)
1167 listMenu->addAction(actionRename);
1168 listMenu->addAction(actionEditTracker);
1170 // Category Menu
1171 QStringList categories = BitTorrent::Session::instance()->categories();
1172 std::sort(categories.begin(), categories.end(), Utils::Compare::NaturalLessThan<Qt::CaseInsensitive>());
1174 QMenu *categoryMenu = listMenu->addMenu(UIThemeManager::instance()->getIcon(u"view-categories"_s), tr("Categor&y"));
1176 categoryMenu->addAction(UIThemeManager::instance()->getIcon(u"list-add"_s), tr("&New...", "New category...")
1177 , this, &TransferListWidget::askNewCategoryForSelection);
1178 categoryMenu->addAction(UIThemeManager::instance()->getIcon(u"edit-clear"_s), tr("&Reset", "Reset category")
1179 , this, [this]() { setSelectionCategory(u""_s); });
1180 categoryMenu->addSeparator();
1182 for (const QString &category : asConst(categories))
1184 const QString escapedCategory = QString(category).replace(u'&', u"&&"_s); // avoid '&' becomes accelerator key
1185 QAction *categoryAction = categoryMenu->addAction(UIThemeManager::instance()->getIcon(u"view-categories"_s), escapedCategory
1186 , this, [this, category]() { setSelectionCategory(category); });
1188 if (allSameCategory && (category == firstCategory))
1190 categoryAction->setCheckable(true);
1191 categoryAction->setChecked(true);
1195 // Tag Menu
1196 QMenu *tagsMenu = listMenu->addMenu(UIThemeManager::instance()->getIcon(u"tags"_s, u"view-categories"_s), tr("Ta&gs"));
1198 tagsMenu->addAction(UIThemeManager::instance()->getIcon(u"list-add"_s), tr("&Add...", "Add / assign multiple tags...")
1199 , this, &TransferListWidget::askAddTagsForSelection);
1200 tagsMenu->addAction(UIThemeManager::instance()->getIcon(u"edit-clear"_s), tr("&Remove All", "Remove all tags")
1201 , this, [this]()
1203 if (Preferences::instance()->confirmRemoveAllTags())
1204 confirmRemoveAllTagsForSelection();
1205 else
1206 clearSelectionTags();
1208 tagsMenu->addSeparator();
1210 const TagSet tags = BitTorrent::Session::instance()->tags();
1211 for (const Tag &tag : asConst(tags))
1213 auto *action = new TriStateAction(tag.toString(), tagsMenu);
1214 action->setCloseOnInteraction(false);
1216 const Qt::CheckState initialState = tagsInAll.contains(tag) ? Qt::Checked
1217 : tagsInAny.contains(tag) ? Qt::PartiallyChecked : Qt::Unchecked;
1218 action->setCheckState(initialState);
1220 connect(action, &QAction::toggled, this, [this, tag](const bool checked)
1222 if (checked)
1223 addSelectionTag(tag);
1224 else
1225 removeSelectionTag(tag);
1228 tagsMenu->addAction(action);
1231 actionAutoTMM->setCheckState(allSameAutoTMM
1232 ? (firstAutoTMM ? Qt::Checked : Qt::Unchecked)
1233 : Qt::PartiallyChecked);
1234 listMenu->addAction(actionAutoTMM);
1236 listMenu->addSeparator();
1237 listMenu->addAction(actionTorrentOptions);
1238 if (!oneNotFinished && oneHasMetadata)
1240 actionSuperSeedingMode->setCheckState(allSameSuperSeeding
1241 ? (superSeedingMode ? Qt::Checked : Qt::Unchecked)
1242 : Qt::PartiallyChecked);
1243 listMenu->addAction(actionSuperSeedingMode);
1245 listMenu->addSeparator();
1246 bool addedPreviewAction = false;
1247 if (needsPreview)
1249 listMenu->addAction(actionPreviewFile);
1250 addedPreviewAction = true;
1252 if (oneNotFinished)
1254 actionSequentialDownload->setCheckState(allSameSequentialDownloadMode
1255 ? (sequentialDownloadMode ? Qt::Checked : Qt::Unchecked)
1256 : Qt::PartiallyChecked);
1257 listMenu->addAction(actionSequentialDownload);
1259 actionFirstLastPiecePrio->setCheckState(allSamePrioFirstlast
1260 ? (prioritizeFirstLast ? Qt::Checked : Qt::Unchecked)
1261 : Qt::PartiallyChecked);
1262 listMenu->addAction(actionFirstLastPiecePrio);
1264 addedPreviewAction = true;
1267 if (addedPreviewAction)
1268 listMenu->addSeparator();
1269 if (oneHasMetadata)
1270 listMenu->addAction(actionForceRecheck);
1271 // We can not force reannounce torrents that are paused/errored/checking/missing files/queued.
1272 // We may already have the tracker list from magnet url. So we can force reannounce torrents without metadata anyway.
1273 listMenu->addAction(actionForceReannounce);
1274 actionForceReannounce->setEnabled(oneCanForceReannounce);
1275 if (!oneCanForceReannounce)
1276 actionForceReannounce->setToolTip(tr("Can not force reannounce if torrent is Paused/Queued/Errored/Checking"));
1277 listMenu->addSeparator();
1278 listMenu->addAction(actionOpenDestinationFolder);
1279 if (BitTorrent::Session::instance()->isQueueingSystemEnabled() && oneNotFinished)
1281 listMenu->addSeparator();
1282 QMenu *queueMenu = listMenu->addMenu(
1283 UIThemeManager::instance()->getIcon(u"queued"_s), tr("&Queue"));
1284 queueMenu->addAction(actionTopQueuePos);
1285 queueMenu->addAction(actionIncreaseQueuePos);
1286 queueMenu->addAction(actionDecreaseQueuePos);
1287 queueMenu->addAction(actionBottomQueuePos);
1290 QMenu *copySubMenu = listMenu->addMenu(UIThemeManager::instance()->getIcon(u"edit-copy"_s), tr("&Copy"));
1291 copySubMenu->addAction(actionCopyName);
1292 copySubMenu->addAction(actionCopyHash1);
1293 actionCopyHash1->setEnabled(hasInfohashV1);
1294 copySubMenu->addAction(actionCopyHash2);
1295 actionCopyHash2->setEnabled(hasInfohashV2);
1296 copySubMenu->addAction(actionCopyMagnetLink);
1297 copySubMenu->addAction(actionCopyID);
1298 copySubMenu->addAction(actionCopyComment);
1300 actionExportTorrent->setToolTip(tr("Exported torrent is not necessarily the same as the imported"));
1301 listMenu->addAction(actionExportTorrent);
1303 listMenu->popup(QCursor::pos());
1306 void TransferListWidget::currentChanged(const QModelIndex &current, const QModelIndex&)
1308 qDebug("CURRENT CHANGED");
1309 BitTorrent::Torrent *torrent = nullptr;
1310 if (current.isValid())
1312 torrent = m_listModel->torrentHandle(mapToSource(current));
1313 // Fix scrolling to the lowermost visible torrent
1314 QMetaObject::invokeMethod(this, [this, current] { scrollTo(current); }, Qt::QueuedConnection);
1316 emit currentTorrentChanged(torrent);
1319 void TransferListWidget::applyCategoryFilter(const QString &category)
1321 if (category.isNull())
1322 m_sortFilterModel->disableCategoryFilter();
1323 else
1324 m_sortFilterModel->setCategoryFilter(category);
1327 void TransferListWidget::applyTagFilter(const std::optional<Tag> &tag)
1329 if (!tag)
1330 m_sortFilterModel->disableTagFilter();
1331 else
1332 m_sortFilterModel->setTagFilter(*tag);
1335 void TransferListWidget::applyTrackerFilterAll()
1337 m_sortFilterModel->disableTrackerFilter();
1340 void TransferListWidget::applyTrackerFilter(const QSet<BitTorrent::TorrentID> &torrentIDs)
1342 m_sortFilterModel->setTrackerFilter(torrentIDs);
1345 void TransferListWidget::applyFilter(const QString &name, const TransferListModel::Column &type)
1347 m_sortFilterModel->setFilterKeyColumn(type);
1348 const QString pattern = (Preferences::instance()->getRegexAsFilteringPatternForTransferList()
1349 ? name : Utils::String::wildcardToRegexPattern(name));
1350 m_sortFilterModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption));
1353 void TransferListWidget::applyStatusFilter(int f)
1355 m_sortFilterModel->setStatusFilter(static_cast<TorrentFilter::Type>(f));
1356 // Select first item if nothing is selected
1357 if (selectionModel()->selectedRows(0).empty() && (m_sortFilterModel->rowCount() > 0))
1359 qDebug("Nothing is selected, selecting first row: %s", qUtf8Printable(m_sortFilterModel->index(0, TransferListModel::TR_NAME).data().toString()));
1360 selectionModel()->setCurrentIndex(m_sortFilterModel->index(0, TransferListModel::TR_NAME), QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows);
1364 void TransferListWidget::saveSettings()
1366 Preferences::instance()->setTransHeaderState(header()->saveState());
1369 bool TransferListWidget::loadSettings()
1371 return header()->restoreState(Preferences::instance()->getTransHeaderState());
1374 void TransferListWidget::wheelEvent(QWheelEvent *event)
1376 if (event->modifiers() & Qt::ShiftModifier)
1378 // Shift + scroll = horizontal scroll
1379 event->accept();
1380 QWheelEvent scrollHEvent {event->position(), event->globalPosition()
1381 , event->pixelDelta(), event->angleDelta().transposed(), event->buttons()
1382 , event->modifiers(), event->phase(), event->inverted(), event->source()};
1383 QTreeView::wheelEvent(&scrollHEvent);
1384 return;
1387 QTreeView::wheelEvent(event); // event delegated to base class