Enable customizing the save statistics time interval
[qBittorrent.git] / src / gui / transferlistwidget.cpp
blobe765b9e38e65251f9d8634017befbb4b78b0b556
1 /*
2 * Bittorrent Client using Qt and libtorrent.
3 * Copyright (C) 2023-2024 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 <QList>
39 #include <QMenu>
40 #include <QMessageBox>
41 #include <QRegularExpression>
42 #include <QSet>
43 #include <QShortcut>
44 #include <QWheelEvent>
46 #include "base/bittorrent/session.h"
47 #include "base/bittorrent/torrent.h"
48 #include "base/bittorrent/trackerentrystatus.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 QList<BitTorrent::TorrentID> extractIDs(const QList<BitTorrent::Torrent *> &torrents)
81 QList<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 QList<BitTorrent::Torrent *> &torrents, const bool isDeleteFileSelected)
118 auto *session = BitTorrent::Session::instance();
119 const BitTorrent::TorrentRemoveOption removeOption = isDeleteFileSelected
120 ? BitTorrent::TorrentRemoveOption::RemoveContent : BitTorrent::TorrentRemoveOption::KeepContent;
121 for (const BitTorrent::Torrent *torrent : torrents)
122 session->removeTorrent(torrent->id(), removeOption);
126 TransferListWidget::TransferListWidget(QWidget *parent, MainWindow *mainWindow)
127 : QTreeView {parent}
128 , m_listModel {new TransferListModel {this}}
129 , m_sortFilterModel {new TransferListSortModel {this}}
130 , m_mainWindow {mainWindow}
132 // Load settings
133 const bool columnLoaded = loadSettings();
135 // Create and apply delegate
136 setItemDelegate(new TransferListDelegate {this});
138 m_sortFilterModel->setDynamicSortFilter(true);
139 m_sortFilterModel->setSourceModel(m_listModel);
140 m_sortFilterModel->setFilterKeyColumn(TransferListModel::TR_NAME);
141 m_sortFilterModel->setFilterRole(Qt::DisplayRole);
142 m_sortFilterModel->setSortCaseSensitivity(Qt::CaseInsensitive);
143 m_sortFilterModel->setSortRole(TransferListModel::UnderlyingDataRole);
144 setModel(m_sortFilterModel);
146 // Visual settings
147 setUniformRowHeights(true);
148 setRootIsDecorated(false);
149 setAllColumnsShowFocus(true);
150 setSortingEnabled(true);
151 setSelectionMode(QAbstractItemView::ExtendedSelection);
152 setItemsExpandable(false);
153 setAutoScroll(true);
154 setDragDropMode(QAbstractItemView::DragOnly);
155 #if defined(Q_OS_MACOS)
156 setAttribute(Qt::WA_MacShowFocusRect, false);
157 #endif
158 header()->setFirstSectionMovable(true);
159 header()->setStretchLastSection(false);
160 header()->setTextElideMode(Qt::ElideRight);
162 // Default hidden columns
163 if (!columnLoaded)
165 setColumnHidden(TransferListModel::TR_ADD_DATE, true);
166 setColumnHidden(TransferListModel::TR_SEED_DATE, true);
167 setColumnHidden(TransferListModel::TR_UPLIMIT, true);
168 setColumnHidden(TransferListModel::TR_DLLIMIT, true);
169 setColumnHidden(TransferListModel::TR_TRACKER, true);
170 setColumnHidden(TransferListModel::TR_AMOUNT_DOWNLOADED, true);
171 setColumnHidden(TransferListModel::TR_AMOUNT_UPLOADED, true);
172 setColumnHidden(TransferListModel::TR_AMOUNT_DOWNLOADED_SESSION, true);
173 setColumnHidden(TransferListModel::TR_AMOUNT_UPLOADED_SESSION, true);
174 setColumnHidden(TransferListModel::TR_AMOUNT_LEFT, true);
175 setColumnHidden(TransferListModel::TR_TIME_ELAPSED, true);
176 setColumnHidden(TransferListModel::TR_SAVE_PATH, true);
177 setColumnHidden(TransferListModel::TR_DOWNLOAD_PATH, true);
178 setColumnHidden(TransferListModel::TR_INFOHASH_V1, true);
179 setColumnHidden(TransferListModel::TR_INFOHASH_V2, true);
180 setColumnHidden(TransferListModel::TR_COMPLETED, true);
181 setColumnHidden(TransferListModel::TR_RATIO_LIMIT, true);
182 setColumnHidden(TransferListModel::TR_POPULARITY, true);
183 setColumnHidden(TransferListModel::TR_SEEN_COMPLETE_DATE, true);
184 setColumnHidden(TransferListModel::TR_LAST_ACTIVITY, true);
185 setColumnHidden(TransferListModel::TR_TOTAL_SIZE, true);
186 setColumnHidden(TransferListModel::TR_REANNOUNCE, true);
187 setColumnHidden(TransferListModel::TR_PRIVATE, true);
190 //Ensure that at least one column is visible at all times
191 bool atLeastOne = false;
192 for (int i = 0; i < TransferListModel::NB_COLUMNS; ++i)
194 if (!isColumnHidden(i))
196 atLeastOne = true;
197 break;
200 if (!atLeastOne)
201 setColumnHidden(TransferListModel::TR_NAME, false);
203 //When adding/removing columns between versions some may
204 //end up being size 0 when the new version is launched with
205 //a conf file from the previous version.
206 for (int i = 0; i < TransferListModel::NB_COLUMNS; ++i)
208 if ((columnWidth(i) <= 0) && (!isColumnHidden(i)))
209 resizeColumnToContents(i);
212 setContextMenuPolicy(Qt::CustomContextMenu);
214 // Listen for list events
215 connect(this, &QAbstractItemView::doubleClicked, this, &TransferListWidget::torrentDoubleClicked);
216 connect(this, &QWidget::customContextMenuRequested, this, &TransferListWidget::displayListMenu);
217 header()->setContextMenuPolicy(Qt::CustomContextMenu);
218 connect(header(), &QWidget::customContextMenuRequested, this, &TransferListWidget::displayColumnHeaderMenu);
219 connect(header(), &QHeaderView::sectionMoved, this, &TransferListWidget::saveSettings);
220 connect(header(), &QHeaderView::sectionResized, this, &TransferListWidget::saveSettings);
221 connect(header(), &QHeaderView::sortIndicatorChanged, this, &TransferListWidget::saveSettings);
223 const auto *editHotkey = new QShortcut(Qt::Key_F2, this, nullptr, nullptr, Qt::WidgetShortcut);
224 connect(editHotkey, &QShortcut::activated, this, &TransferListWidget::renameSelectedTorrent);
225 const auto *deleteHotkey = new QShortcut(QKeySequence::Delete, this, nullptr, nullptr, Qt::WidgetShortcut);
226 connect(deleteHotkey, &QShortcut::activated, this, &TransferListWidget::softDeleteSelectedTorrents);
227 const auto *permDeleteHotkey = new QShortcut((Qt::SHIFT | Qt::Key_Delete), this, nullptr, nullptr, Qt::WidgetShortcut);
228 connect(permDeleteHotkey, &QShortcut::activated, this, &TransferListWidget::permDeleteSelectedTorrents);
229 const auto *doubleClickHotkeyReturn = new QShortcut(Qt::Key_Return, this, nullptr, nullptr, Qt::WidgetShortcut);
230 connect(doubleClickHotkeyReturn, &QShortcut::activated, this, &TransferListWidget::torrentDoubleClicked);
231 const auto *doubleClickHotkeyEnter = new QShortcut(Qt::Key_Enter, this, nullptr, nullptr, Qt::WidgetShortcut);
232 connect(doubleClickHotkeyEnter, &QShortcut::activated, this, &TransferListWidget::torrentDoubleClicked);
233 const auto *recheckHotkey = new QShortcut((Qt::CTRL | Qt::Key_R), this, nullptr, nullptr, Qt::WidgetShortcut);
234 connect(recheckHotkey, &QShortcut::activated, this, &TransferListWidget::recheckSelectedTorrents);
235 const auto *forceStartHotkey = new QShortcut((Qt::CTRL | Qt::Key_M), this, nullptr, nullptr, Qt::WidgetShortcut);
236 connect(forceStartHotkey, &QShortcut::activated, this, &TransferListWidget::forceStartSelectedTorrents);
239 TransferListWidget::~TransferListWidget()
241 // Save settings
242 saveSettings();
245 TransferListModel *TransferListWidget::getSourceModel() const
247 return m_listModel;
250 void TransferListWidget::previewFile(const Path &filePath)
252 Utils::Gui::openPath(filePath);
255 QModelIndex TransferListWidget::mapToSource(const QModelIndex &index) const
257 Q_ASSERT(index.isValid());
258 if (index.model() == m_sortFilterModel)
259 return m_sortFilterModel->mapToSource(index);
260 return index;
263 QModelIndexList TransferListWidget::mapToSource(const QModelIndexList &indexes) const
265 QModelIndexList result;
266 result.reserve(indexes.size());
267 for (const QModelIndex &index : indexes)
268 result.append(mapToSource(index));
270 return result;
273 QModelIndex TransferListWidget::mapFromSource(const QModelIndex &index) const
275 Q_ASSERT(index.isValid());
276 Q_ASSERT(index.model() == m_sortFilterModel);
277 return m_sortFilterModel->mapFromSource(index);
280 void TransferListWidget::torrentDoubleClicked()
282 const QModelIndexList selectedIndexes = selectionModel()->selectedRows();
283 if ((selectedIndexes.size() != 1) || !selectedIndexes.first().isValid())
284 return;
286 const QModelIndex index = m_listModel->index(mapToSource(selectedIndexes.first()).row());
287 BitTorrent::Torrent *const torrent = m_listModel->torrentHandle(index);
288 if (!torrent)
289 return;
291 int action;
292 if (torrent->isFinished())
293 action = Preferences::instance()->getActionOnDblClOnTorrentFn();
294 else
295 action = Preferences::instance()->getActionOnDblClOnTorrentDl();
297 switch (action)
299 case TOGGLE_STOP:
300 if (torrent->isStopped())
301 torrent->start();
302 else
303 torrent->stop();
304 break;
305 case PREVIEW_FILE:
306 if (torrentContainsPreviewableFiles(torrent))
308 auto *dialog = new PreviewSelectDialog(this, torrent);
309 dialog->setAttribute(Qt::WA_DeleteOnClose);
310 connect(dialog, &PreviewSelectDialog::readyToPreviewFile, this, &TransferListWidget::previewFile);
311 dialog->show();
313 else
315 openDestinationFolder(torrent);
317 break;
318 case OPEN_DEST:
319 openDestinationFolder(torrent);
320 break;
321 case SHOW_OPTIONS:
322 setTorrentOptions();
323 break;
327 QList<BitTorrent::Torrent *> TransferListWidget::getSelectedTorrents() const
329 const QModelIndexList selectedRows = selectionModel()->selectedRows();
331 QList<BitTorrent::Torrent *> torrents;
332 torrents.reserve(selectedRows.size());
333 for (const QModelIndex &index : selectedRows)
334 torrents << m_listModel->torrentHandle(mapToSource(index));
335 return torrents;
338 QList<BitTorrent::Torrent *> TransferListWidget::getVisibleTorrents() const
340 const int visibleTorrentsCount = m_sortFilterModel->rowCount();
342 QList<BitTorrent::Torrent *> torrents;
343 torrents.reserve(visibleTorrentsCount);
344 for (int i = 0; i < visibleTorrentsCount; ++i)
345 torrents << m_listModel->torrentHandle(mapToSource(m_sortFilterModel->index(i, 0)));
346 return torrents;
349 void TransferListWidget::setSelectedTorrentsLocation()
351 const QList<BitTorrent::Torrent *> torrents = getSelectedTorrents();
352 if (torrents.isEmpty())
353 return;
355 const Path oldLocation = torrents[0]->savePath();
357 auto *fileDialog = new QFileDialog(this, tr("Choose save path"), oldLocation.data());
358 fileDialog->setAttribute(Qt::WA_DeleteOnClose);
359 fileDialog->setFileMode(QFileDialog::Directory);
360 fileDialog->setOptions(QFileDialog::DontConfirmOverwrite | QFileDialog::ShowDirsOnly | QFileDialog::HideNameFilterDetails);
361 connect(fileDialog, &QDialog::accepted, this, [this, fileDialog]()
363 const QList<BitTorrent::Torrent *> torrents = getSelectedTorrents();
364 if (torrents.isEmpty())
365 return;
367 const Path newLocation {fileDialog->selectedFiles().constFirst()};
368 if (!newLocation.exists())
369 return;
371 // Actually move storage
372 for (BitTorrent::Torrent *const torrent : torrents)
374 torrent->setAutoTMMEnabled(false);
375 torrent->setSavePath(newLocation);
379 fileDialog->open();
382 void TransferListWidget::pauseSession()
384 BitTorrent::Session::instance()->pause();
387 void TransferListWidget::resumeSession()
389 BitTorrent::Session::instance()->resume();
392 void TransferListWidget::startSelectedTorrents()
394 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
395 torrent->start();
398 void TransferListWidget::forceStartSelectedTorrents()
400 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
401 torrent->start(BitTorrent::TorrentOperatingMode::Forced);
404 void TransferListWidget::startVisibleTorrents()
406 for (BitTorrent::Torrent *const torrent : asConst(getVisibleTorrents()))
407 torrent->start();
410 void TransferListWidget::stopSelectedTorrents()
412 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
413 torrent->stop();
416 void TransferListWidget::stopVisibleTorrents()
418 for (BitTorrent::Torrent *const torrent : asConst(getVisibleTorrents()))
419 torrent->stop();
422 void TransferListWidget::softDeleteSelectedTorrents()
424 deleteSelectedTorrents(false);
427 void TransferListWidget::permDeleteSelectedTorrents()
429 deleteSelectedTorrents(true);
432 void TransferListWidget::deleteSelectedTorrents(const bool deleteLocalFiles)
434 if (m_mainWindow->currentTabWidget() != this) return;
436 const QList<BitTorrent::Torrent *> torrents = getSelectedTorrents();
437 if (torrents.empty()) return;
439 if (Preferences::instance()->confirmTorrentDeletion())
441 auto *dialog = new DeletionConfirmationDialog(this, torrents.size(), torrents[0]->name(), deleteLocalFiles);
442 dialog->setAttribute(Qt::WA_DeleteOnClose);
443 connect(dialog, &DeletionConfirmationDialog::accepted, this, [this, dialog]()
445 // Some torrents might be removed when waiting for user input, so refetch the torrent list
446 // NOTE: this will only work when dialog is modal
447 removeTorrents(getSelectedTorrents(), dialog->isRemoveContentSelected());
449 dialog->open();
451 else
453 removeTorrents(torrents, deleteLocalFiles);
457 void TransferListWidget::deleteVisibleTorrents()
459 const QList<BitTorrent::Torrent *> torrents = getVisibleTorrents();
460 if (torrents.empty()) return;
462 if (Preferences::instance()->confirmTorrentDeletion())
464 auto *dialog = new DeletionConfirmationDialog(this, torrents.size(), torrents[0]->name(), false);
465 dialog->setAttribute(Qt::WA_DeleteOnClose);
466 connect(dialog, &DeletionConfirmationDialog::accepted, this, [this, dialog]()
468 // Some torrents might be removed when waiting for user input, so refetch the torrent list
469 // NOTE: this will only work when dialog is modal
470 removeTorrents(getVisibleTorrents(), dialog->isRemoveContentSelected());
472 dialog->open();
474 else
476 removeTorrents(torrents, false);
480 void TransferListWidget::increaseQueuePosSelectedTorrents()
482 qDebug() << Q_FUNC_INFO;
483 if (m_mainWindow->currentTabWidget() == this)
484 BitTorrent::Session::instance()->increaseTorrentsQueuePos(extractIDs(getSelectedTorrents()));
487 void TransferListWidget::decreaseQueuePosSelectedTorrents()
489 qDebug() << Q_FUNC_INFO;
490 if (m_mainWindow->currentTabWidget() == this)
491 BitTorrent::Session::instance()->decreaseTorrentsQueuePos(extractIDs(getSelectedTorrents()));
494 void TransferListWidget::topQueuePosSelectedTorrents()
496 if (m_mainWindow->currentTabWidget() == this)
497 BitTorrent::Session::instance()->topTorrentsQueuePos(extractIDs(getSelectedTorrents()));
500 void TransferListWidget::bottomQueuePosSelectedTorrents()
502 if (m_mainWindow->currentTabWidget() == this)
503 BitTorrent::Session::instance()->bottomTorrentsQueuePos(extractIDs(getSelectedTorrents()));
506 void TransferListWidget::copySelectedMagnetURIs() const
508 QStringList magnetUris;
509 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
510 magnetUris << torrent->createMagnetURI();
512 qApp->clipboard()->setText(magnetUris.join(u'\n'));
515 void TransferListWidget::copySelectedNames() const
517 QStringList torrentNames;
518 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
519 torrentNames << torrent->name();
521 qApp->clipboard()->setText(torrentNames.join(u'\n'));
524 void TransferListWidget::copySelectedInfohashes(const CopyInfohashPolicy policy) const
526 const auto selectedTorrents = getSelectedTorrents();
527 QStringList infoHashes;
528 infoHashes.reserve(selectedTorrents.size());
529 switch (policy)
531 case CopyInfohashPolicy::Version1:
532 for (const BitTorrent::Torrent *torrent : selectedTorrents)
534 if (const auto infoHash = torrent->infoHash().v1(); infoHash.isValid())
535 infoHashes << infoHash.toString();
537 break;
538 case CopyInfohashPolicy::Version2:
539 for (const BitTorrent::Torrent *torrent : selectedTorrents)
541 if (const auto infoHash = torrent->infoHash().v2(); infoHash.isValid())
542 infoHashes << infoHash.toString();
544 break;
547 qApp->clipboard()->setText(infoHashes.join(u'\n'));
550 void TransferListWidget::copySelectedIDs() const
552 QStringList torrentIDs;
553 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
554 torrentIDs << torrent->id().toString();
556 qApp->clipboard()->setText(torrentIDs.join(u'\n'));
559 void TransferListWidget::copySelectedComments() const
561 QStringList torrentComments;
562 for (const BitTorrent::Torrent *torrent : asConst(getSelectedTorrents()))
564 if (!torrent->comment().isEmpty())
565 torrentComments << torrent->comment();
568 qApp->clipboard()->setText(torrentComments.join(u"\n---------\n"_s));
571 void TransferListWidget::hideQueuePosColumn(bool hide)
573 setColumnHidden(TransferListModel::TR_QUEUE_POSITION, hide);
574 if (!hide && (columnWidth(TransferListModel::TR_QUEUE_POSITION) == 0))
575 resizeColumnToContents(TransferListModel::TR_QUEUE_POSITION);
578 void TransferListWidget::openSelectedTorrentsFolder() const
580 QSet<Path> paths;
581 #ifdef Q_OS_MACOS
582 // On macOS you expect both the files and folders to be opened in their parent
583 // folders prehilighted for opening, so we use a custom method.
584 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
586 const Path contentPath = torrent->contentPath();
587 paths.insert(!contentPath.isEmpty() ? contentPath : torrent->savePath());
589 MacUtils::openFiles(PathList(paths.cbegin(), paths.cend()));
590 #else
591 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
593 const Path contentPath = torrent->contentPath();
594 const Path openedPath = (!contentPath.isEmpty() ? contentPath : torrent->savePath());
595 if (!paths.contains(openedPath))
597 if (torrent->filesCount() == 1)
598 Utils::Gui::openFolderSelect(openedPath);
599 else
600 Utils::Gui::openPath(openedPath);
602 paths.insert(openedPath);
604 #endif // Q_OS_MACOS
607 void TransferListWidget::previewSelectedTorrents()
609 for (const BitTorrent::Torrent *torrent : asConst(getSelectedTorrents()))
611 if (torrentContainsPreviewableFiles(torrent))
613 auto *dialog = new PreviewSelectDialog(this, torrent);
614 dialog->setAttribute(Qt::WA_DeleteOnClose);
615 connect(dialog, &PreviewSelectDialog::readyToPreviewFile, this, &TransferListWidget::previewFile);
616 dialog->show();
618 else
620 QMessageBox::critical(this, tr("Unable to preview"), tr("The selected torrent \"%1\" does not contain previewable files")
621 .arg(torrent->name()));
626 void TransferListWidget::setTorrentOptions()
628 const QList<BitTorrent::Torrent *> selectedTorrents = getSelectedTorrents();
629 if (selectedTorrents.empty()) return;
631 auto *dialog = new TorrentOptionsDialog {this, selectedTorrents};
632 dialog->setAttribute(Qt::WA_DeleteOnClose);
633 dialog->open();
636 void TransferListWidget::recheckSelectedTorrents()
638 if (Preferences::instance()->confirmTorrentRecheck())
640 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);
641 if (ret != QMessageBox::Yes) return;
644 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
645 torrent->forceRecheck();
648 void TransferListWidget::reannounceSelectedTorrents()
650 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
651 torrent->forceReannounce();
654 int TransferListWidget::visibleColumnsCount() const
656 int count = 0;
657 for (int i = 0, iMax = header()->count(); i < iMax; ++i)
659 if (!isColumnHidden(i))
660 ++count;
663 return count;
666 // hide/show columns menu
667 void TransferListWidget::displayColumnHeaderMenu()
669 auto *menu = new QMenu(this);
670 menu->setAttribute(Qt::WA_DeleteOnClose);
671 menu->setTitle(tr("Column visibility"));
672 menu->setToolTipsVisible(true);
674 for (int i = 0; i < TransferListModel::NB_COLUMNS; ++i)
676 if (!BitTorrent::Session::instance()->isQueueingSystemEnabled() && (i == TransferListModel::TR_QUEUE_POSITION))
677 continue;
679 const auto columnName = m_listModel->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString();
680 const QVariant columnToolTip = m_listModel->headerData(i, Qt::Horizontal, Qt::ToolTipRole);
681 QAction *action = menu->addAction(columnName, this, [this, i](const bool checked)
683 if (!checked && (visibleColumnsCount() <= 1))
684 return;
686 setColumnHidden(i, !checked);
688 if (checked && (columnWidth(i) <= 5))
689 resizeColumnToContents(i);
691 saveSettings();
693 action->setCheckable(true);
694 action->setChecked(!isColumnHidden(i));
695 if (!columnToolTip.isNull())
696 action->setToolTip(columnToolTip.toString());
699 menu->addSeparator();
700 QAction *resizeAction = menu->addAction(tr("Resize columns"), this, [this]()
702 for (int i = 0, count = header()->count(); i < count; ++i)
704 if (!isColumnHidden(i))
705 resizeColumnToContents(i);
707 saveSettings();
709 resizeAction->setToolTip(tr("Resize all non-hidden columns to the size of their contents"));
711 menu->popup(QCursor::pos());
714 void TransferListWidget::setSelectedTorrentsSuperSeeding(const bool enabled) const
716 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
718 if (torrent->hasMetadata())
719 torrent->setSuperSeeding(enabled);
723 void TransferListWidget::setSelectedTorrentsSequentialDownload(const bool enabled) const
725 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
726 torrent->setSequentialDownload(enabled);
729 void TransferListWidget::setSelectedFirstLastPiecePrio(const bool enabled) const
731 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
732 torrent->setFirstLastPiecePriority(enabled);
735 void TransferListWidget::setSelectedAutoTMMEnabled(const bool enabled)
737 if (enabled)
739 const QMessageBox::StandardButton btn = QMessageBox::question(this, tr("Enable automatic torrent management")
740 , tr("Are you sure you want to enable Automatic Torrent Management for the selected torrent(s)? They may be relocated.")
741 , (QMessageBox::Yes | QMessageBox::No), QMessageBox::Yes);
742 if (btn != QMessageBox::Yes) return;
745 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
746 torrent->setAutoTMMEnabled(enabled);
749 void TransferListWidget::askNewCategoryForSelection()
751 const QString newCategoryName = TorrentCategoryDialog::createCategory(this);
752 if (!newCategoryName.isEmpty())
753 setSelectionCategory(newCategoryName);
756 void TransferListWidget::askAddTagsForSelection()
758 const TagSet tags = askTagsForSelection(tr("Add tags"));
759 for (const Tag &tag : tags)
760 addSelectionTag(tag);
763 void TransferListWidget::editTorrentTrackers()
765 const QList<BitTorrent::Torrent *> torrents = getSelectedTorrents();
766 QList<BitTorrent::TrackerEntry> commonTrackers;
768 if (!torrents.empty())
770 for (const BitTorrent::TrackerEntryStatus &status : asConst(torrents[0]->trackers()))
771 commonTrackers.append({.url = status.url, .tier = status.tier});
773 for (const BitTorrent::Torrent *torrent : torrents)
775 QSet<BitTorrent::TrackerEntry> trackerSet;
776 for (const BitTorrent::TrackerEntryStatus &status : asConst(torrent->trackers()))
777 trackerSet.insert({.url = status.url, .tier = status.tier});
779 commonTrackers.erase(std::remove_if(commonTrackers.begin(), commonTrackers.end()
780 , [&trackerSet](const BitTorrent::TrackerEntry &entry) { return !trackerSet.contains(entry); })
781 , commonTrackers.end());
785 auto *trackerDialog = new TrackerEntriesDialog(this);
786 trackerDialog->setAttribute(Qt::WA_DeleteOnClose);
787 trackerDialog->setTrackers(commonTrackers);
789 connect(trackerDialog, &QDialog::accepted, this, [torrents, trackerDialog]()
791 for (BitTorrent::Torrent *torrent : torrents)
792 torrent->replaceTrackers(trackerDialog->trackers());
795 trackerDialog->open();
798 void TransferListWidget::exportTorrent()
800 if (getSelectedTorrents().isEmpty())
801 return;
803 auto *fileDialog = new QFileDialog(this, tr("Choose folder to save exported .torrent files"));
804 fileDialog->setAttribute(Qt::WA_DeleteOnClose);
805 fileDialog->setFileMode(QFileDialog::Directory);
806 fileDialog->setOptions(QFileDialog::ShowDirsOnly);
807 connect(fileDialog, &QFileDialog::fileSelected, this, [this](const QString &dir)
809 const QList<BitTorrent::Torrent *> torrents = getSelectedTorrents();
810 if (torrents.isEmpty())
811 return;
813 const Path savePath {dir};
814 if (!savePath.exists())
815 return;
817 const QString errorMsg = tr("Export .torrent file failed. Torrent: \"%1\". Save path: \"%2\". Reason: \"%3\"");
819 bool hasError = false;
820 for (const BitTorrent::Torrent *torrent : torrents)
822 const QString validName = Utils::Fs::toValidFileName(torrent->name(), u"_"_s);
823 const Path filePath = savePath / Path(validName + u".torrent");
824 if (filePath.exists())
826 LogMsg(errorMsg.arg(torrent->name(), filePath.toString(), tr("A file with the same name already exists")) , Log::WARNING);
827 hasError = true;
828 continue;
831 const nonstd::expected<void, QString> result = torrent->exportToFile(filePath);
832 if (!result)
834 LogMsg(errorMsg.arg(torrent->name(), filePath.toString(), result.error()) , Log::WARNING);
835 hasError = true;
836 continue;
840 if (hasError)
842 QMessageBox::warning(this, tr("Export .torrent file error")
843 , tr("Errors occurred when exporting .torrent files. Check execution log for details."));
847 fileDialog->open();
850 void TransferListWidget::confirmRemoveAllTagsForSelection()
852 QMessageBox::StandardButton response = QMessageBox::question(
853 this, tr("Remove All Tags"), tr("Remove all tags from selected torrents?"),
854 QMessageBox::Yes | QMessageBox::No);
855 if (response == QMessageBox::Yes)
856 clearSelectionTags();
859 TagSet TransferListWidget::askTagsForSelection(const QString &dialogTitle)
861 TagSet tags;
862 bool invalid = true;
863 while (invalid)
865 bool ok = false;
866 invalid = false;
867 const QString tagsInput = AutoExpandableDialog::getText(
868 this, dialogTitle, tr("Comma-separated tags:"), QLineEdit::Normal, {}, &ok).trimmed();
869 if (!ok || tagsInput.isEmpty())
870 return {};
872 const QStringList tagStrings = tagsInput.split(u',', Qt::SkipEmptyParts);
873 tags.clear();
874 for (const QString &tagStr : tagStrings)
876 const Tag tag {tagStr};
877 if (!tag.isValid())
879 QMessageBox::warning(this, tr("Invalid tag"), tr("Tag name: '%1' is invalid").arg(tag.toString()));
880 invalid = true;
883 if (!invalid)
884 tags.insert(tag);
888 return tags;
891 void TransferListWidget::applyToSelectedTorrents(const std::function<void (BitTorrent::Torrent *const)> &fn)
893 // Changing the data may affect the layout of the sort/filter model, which in turn may invalidate
894 // the indexes previously obtained from selection model before we process them all.
895 // Therefore, we must map all the selected indexes to source before start processing them.
896 const QModelIndexList sourceRows = mapToSource(selectionModel()->selectedRows());
897 for (const QModelIndex &index : sourceRows)
899 BitTorrent::Torrent *const torrent = m_listModel->torrentHandle(index);
900 Q_ASSERT(torrent);
901 fn(torrent);
905 void TransferListWidget::renameSelectedTorrent()
907 const QModelIndexList selectedIndexes = selectionModel()->selectedRows();
908 if ((selectedIndexes.size() != 1) || !selectedIndexes.first().isValid())
909 return;
911 const QModelIndex mi = m_listModel->index(mapToSource(selectedIndexes.first()).row(), TransferListModel::TR_NAME);
912 BitTorrent::Torrent *const torrent = m_listModel->torrentHandle(mi);
913 if (!torrent)
914 return;
916 // Ask for a new Name
917 bool ok = false;
918 QString name = AutoExpandableDialog::getText(this, tr("Rename"), tr("New name:"), QLineEdit::Normal, torrent->name(), &ok);
919 if (ok && !name.isEmpty())
921 name.replace(QRegularExpression(u"\r?\n|\r"_s), u" "_s);
922 // Rename the torrent
923 m_listModel->setData(mi, name, Qt::DisplayRole);
927 void TransferListWidget::setSelectionCategory(const QString &category)
929 applyToSelectedTorrents([&category](BitTorrent::Torrent *torrent) { torrent->setCategory(category); });
932 void TransferListWidget::addSelectionTag(const Tag &tag)
934 applyToSelectedTorrents([&tag](BitTorrent::Torrent *const torrent) { torrent->addTag(tag); });
937 void TransferListWidget::removeSelectionTag(const Tag &tag)
939 applyToSelectedTorrents([&tag](BitTorrent::Torrent *const torrent) { torrent->removeTag(tag); });
942 void TransferListWidget::clearSelectionTags()
944 applyToSelectedTorrents([](BitTorrent::Torrent *const torrent) { torrent->removeAllTags(); });
947 void TransferListWidget::displayListMenu()
949 const QModelIndexList selectedIndexes = selectionModel()->selectedRows();
950 if (selectedIndexes.isEmpty())
951 return;
953 auto *listMenu = new QMenu(this);
954 listMenu->setAttribute(Qt::WA_DeleteOnClose);
955 listMenu->setToolTipsVisible(true);
957 // Create actions
959 auto *actionStart = new QAction(UIThemeManager::instance()->getIcon(u"torrent-start"_s, u"media-playback-start"_s), tr("&Start", "Resume/start the torrent"), listMenu);
960 connect(actionStart, &QAction::triggered, this, &TransferListWidget::startSelectedTorrents);
961 auto *actionStop = new QAction(UIThemeManager::instance()->getIcon(u"torrent-stop"_s, u"media-playback-pause"_s), tr("Sto&p", "Stop the torrent"), listMenu);
962 connect(actionStop, &QAction::triggered, this, &TransferListWidget::stopSelectedTorrents);
963 auto *actionForceStart = new QAction(UIThemeManager::instance()->getIcon(u"torrent-start-forced"_s, u"media-playback-start"_s), tr("Force Star&t", "Force Resume/start the torrent"), listMenu);
964 connect(actionForceStart, &QAction::triggered, this, &TransferListWidget::forceStartSelectedTorrents);
965 auto *actionDelete = new QAction(UIThemeManager::instance()->getIcon(u"list-remove"_s), tr("&Remove", "Remove the torrent"), listMenu);
966 connect(actionDelete, &QAction::triggered, this, &TransferListWidget::softDeleteSelectedTorrents);
967 auto *actionPreviewFile = new QAction(UIThemeManager::instance()->getIcon(u"view-preview"_s), tr("Pre&view file..."), listMenu);
968 connect(actionPreviewFile, &QAction::triggered, this, &TransferListWidget::previewSelectedTorrents);
969 auto *actionTorrentOptions = new QAction(UIThemeManager::instance()->getIcon(u"configure"_s), tr("Torrent &options..."), listMenu);
970 connect(actionTorrentOptions, &QAction::triggered, this, &TransferListWidget::setTorrentOptions);
971 auto *actionOpenDestinationFolder = new QAction(UIThemeManager::instance()->getIcon(u"directory"_s), tr("Open destination &folder"), listMenu);
972 connect(actionOpenDestinationFolder, &QAction::triggered, this, &TransferListWidget::openSelectedTorrentsFolder);
973 auto *actionIncreaseQueuePos = new QAction(UIThemeManager::instance()->getIcon(u"go-up"_s), tr("Move &up", "i.e. move up in the queue"), listMenu);
974 connect(actionIncreaseQueuePos, &QAction::triggered, this, &TransferListWidget::increaseQueuePosSelectedTorrents);
975 auto *actionDecreaseQueuePos = new QAction(UIThemeManager::instance()->getIcon(u"go-down"_s), tr("Move &down", "i.e. Move down in the queue"), listMenu);
976 connect(actionDecreaseQueuePos, &QAction::triggered, this, &TransferListWidget::decreaseQueuePosSelectedTorrents);
977 auto *actionTopQueuePos = new QAction(UIThemeManager::instance()->getIcon(u"go-top"_s), tr("Move to &top", "i.e. Move to top of the queue"), listMenu);
978 connect(actionTopQueuePos, &QAction::triggered, this, &TransferListWidget::topQueuePosSelectedTorrents);
979 auto *actionBottomQueuePos = new QAction(UIThemeManager::instance()->getIcon(u"go-bottom"_s), tr("Move to &bottom", "i.e. Move to bottom of the queue"), listMenu);
980 connect(actionBottomQueuePos, &QAction::triggered, this, &TransferListWidget::bottomQueuePosSelectedTorrents);
981 auto *actionSetTorrentPath = new QAction(UIThemeManager::instance()->getIcon(u"set-location"_s, u"inode-directory"_s), tr("Set loc&ation..."), listMenu);
982 connect(actionSetTorrentPath, &QAction::triggered, this, &TransferListWidget::setSelectedTorrentsLocation);
983 auto *actionForceRecheck = new QAction(UIThemeManager::instance()->getIcon(u"force-recheck"_s, u"document-edit-verify"_s), tr("Force rec&heck"), listMenu);
984 connect(actionForceRecheck, &QAction::triggered, this, &TransferListWidget::recheckSelectedTorrents);
985 auto *actionForceReannounce = new QAction(UIThemeManager::instance()->getIcon(u"reannounce"_s, u"document-edit-verify"_s), tr("Force r&eannounce"), listMenu);
986 connect(actionForceReannounce, &QAction::triggered, this, &TransferListWidget::reannounceSelectedTorrents);
987 auto *actionCopyMagnetLink = new QAction(UIThemeManager::instance()->getIcon(u"torrent-magnet"_s, u"kt-magnet"_s), tr("&Magnet link"), listMenu);
988 connect(actionCopyMagnetLink, &QAction::triggered, this, &TransferListWidget::copySelectedMagnetURIs);
989 auto *actionCopyID = new QAction(UIThemeManager::instance()->getIcon(u"help-about"_s, u"edit-copy"_s), tr("Torrent &ID"), listMenu);
990 connect(actionCopyID, &QAction::triggered, this, &TransferListWidget::copySelectedIDs);
991 auto *actionCopyComment = new QAction(UIThemeManager::instance()->getIcon(u"edit-copy"_s), tr("&Comment"), listMenu);
992 connect(actionCopyComment, &QAction::triggered, this, &TransferListWidget::copySelectedComments);
993 auto *actionCopyName = new QAction(UIThemeManager::instance()->getIcon(u"name"_s, u"edit-copy"_s), tr("&Name"), listMenu);
994 connect(actionCopyName, &QAction::triggered, this, &TransferListWidget::copySelectedNames);
995 auto *actionCopyHash1 = new QAction(UIThemeManager::instance()->getIcon(u"hash"_s, u"edit-copy"_s), tr("Info &hash v1"), listMenu);
996 connect(actionCopyHash1, &QAction::triggered, this, [this]() { copySelectedInfohashes(CopyInfohashPolicy::Version1); });
997 auto *actionCopyHash2 = new QAction(UIThemeManager::instance()->getIcon(u"hash"_s, u"edit-copy"_s), tr("Info h&ash v2"), listMenu);
998 connect(actionCopyHash2, &QAction::triggered, this, [this]() { copySelectedInfohashes(CopyInfohashPolicy::Version2); });
999 auto *actionSuperSeedingMode = new TriStateAction(tr("Super seeding mode"), listMenu);
1000 connect(actionSuperSeedingMode, &QAction::triggered, this, &TransferListWidget::setSelectedTorrentsSuperSeeding);
1001 auto *actionRename = new QAction(UIThemeManager::instance()->getIcon(u"edit-rename"_s), tr("Re&name..."), listMenu);
1002 connect(actionRename, &QAction::triggered, this, &TransferListWidget::renameSelectedTorrent);
1003 auto *actionSequentialDownload = new TriStateAction(tr("Download in sequential order"), listMenu);
1004 connect(actionSequentialDownload, &QAction::triggered, this, &TransferListWidget::setSelectedTorrentsSequentialDownload);
1005 auto *actionFirstLastPiecePrio = new TriStateAction(tr("Download first and last pieces first"), listMenu);
1006 connect(actionFirstLastPiecePrio, &QAction::triggered, this, &TransferListWidget::setSelectedFirstLastPiecePrio);
1007 auto *actionAutoTMM = new TriStateAction(tr("Automatic Torrent Management"), listMenu);
1008 actionAutoTMM->setToolTip(tr("Automatic mode means that various torrent properties (e.g. save path) will be decided by the associated category"));
1009 connect(actionAutoTMM, &QAction::triggered, this, &TransferListWidget::setSelectedAutoTMMEnabled);
1010 auto *actionEditTracker = new QAction(UIThemeManager::instance()->getIcon(u"edit-rename"_s), tr("Edit trac&kers..."), listMenu);
1011 connect(actionEditTracker, &QAction::triggered, this, &TransferListWidget::editTorrentTrackers);
1012 auto *actionExportTorrent = new QAction(UIThemeManager::instance()->getIcon(u"edit-copy"_s), tr("E&xport .torrent..."), listMenu);
1013 connect(actionExportTorrent, &QAction::triggered, this, &TransferListWidget::exportTorrent);
1014 // End of actions
1016 // Enable/disable stop/start action given the DL state
1017 bool needsStop = false, needsStart = false, needsForce = false, needsPreview = false;
1018 bool allSameSuperSeeding = true;
1019 bool superSeedingMode = false;
1020 bool allSameSequentialDownloadMode = true, allSamePrioFirstlast = true;
1021 bool sequentialDownloadMode = false, prioritizeFirstLast = false;
1022 bool oneHasMetadata = false, oneNotFinished = false;
1023 bool allSameCategory = true;
1024 bool allSameAutoTMM = true;
1025 bool firstAutoTMM = false;
1026 QString firstCategory;
1027 bool first = true;
1028 TagSet tagsInAny;
1029 TagSet tagsInAll;
1030 bool hasInfohashV1 = false, hasInfohashV2 = false;
1031 bool oneCanForceReannounce = false;
1033 for (const QModelIndex &index : selectedIndexes)
1035 const BitTorrent::Torrent *torrent = m_listModel->torrentHandle(mapToSource(index));
1036 if (!torrent)
1037 continue;
1039 if (firstCategory.isEmpty() && first)
1040 firstCategory = torrent->category();
1041 if (firstCategory != torrent->category())
1042 allSameCategory = false;
1044 const TagSet torrentTags = torrent->tags();
1045 tagsInAny.unite(torrentTags);
1047 if (first)
1049 firstAutoTMM = torrent->isAutoTMMEnabled();
1050 tagsInAll = torrentTags;
1052 else
1054 tagsInAll.intersect(torrentTags);
1057 if (firstAutoTMM != torrent->isAutoTMMEnabled())
1058 allSameAutoTMM = false;
1060 if (torrent->hasMetadata())
1061 oneHasMetadata = true;
1062 if (!torrent->isFinished())
1064 oneNotFinished = true;
1065 if (first)
1067 sequentialDownloadMode = torrent->isSequentialDownload();
1068 prioritizeFirstLast = torrent->hasFirstLastPiecePriority();
1070 else
1072 if (sequentialDownloadMode != torrent->isSequentialDownload())
1073 allSameSequentialDownloadMode = false;
1074 if (prioritizeFirstLast != torrent->hasFirstLastPiecePriority())
1075 allSamePrioFirstlast = false;
1078 else
1080 if (!oneNotFinished && allSameSuperSeeding && torrent->hasMetadata())
1082 if (first)
1083 superSeedingMode = torrent->superSeeding();
1084 else if (superSeedingMode != torrent->superSeeding())
1085 allSameSuperSeeding = false;
1089 if (!torrent->isForced())
1090 needsForce = true;
1091 else
1092 needsStart = true;
1094 const bool isStopped = torrent->isStopped();
1095 if (isStopped)
1096 needsStart = true;
1097 else
1098 needsStop = true;
1100 if (torrent->isErrored() || torrent->hasMissingFiles())
1102 // If torrent is in "errored" or "missing files" state
1103 // it cannot keep further processing until you restart it.
1104 needsStart = true;
1105 needsForce = true;
1108 if (torrent->hasMetadata())
1109 needsPreview = true;
1111 if (!hasInfohashV1 && torrent->infoHash().v1().isValid())
1112 hasInfohashV1 = true;
1113 if (!hasInfohashV2 && torrent->infoHash().v2().isValid())
1114 hasInfohashV2 = true;
1116 first = false;
1118 const bool rechecking = torrent->isChecking();
1119 if (rechecking)
1121 needsStart = true;
1122 needsStop = true;
1125 const bool queued = torrent->isQueued();
1126 if (!isStopped && !rechecking && !queued)
1127 oneCanForceReannounce = true;
1129 if (oneHasMetadata && oneNotFinished && !allSameSequentialDownloadMode
1130 && !allSamePrioFirstlast && !allSameSuperSeeding && !allSameCategory
1131 && needsStart && needsForce && needsStop && needsPreview && !allSameAutoTMM
1132 && hasInfohashV1 && hasInfohashV2 && oneCanForceReannounce)
1134 break;
1138 if (needsStart)
1139 listMenu->addAction(actionStart);
1140 if (needsStop)
1141 listMenu->addAction(actionStop);
1142 if (needsForce)
1143 listMenu->addAction(actionForceStart);
1144 listMenu->addSeparator();
1145 listMenu->addAction(actionDelete);
1146 listMenu->addSeparator();
1147 listMenu->addAction(actionSetTorrentPath);
1148 if (selectedIndexes.size() == 1)
1149 listMenu->addAction(actionRename);
1150 listMenu->addAction(actionEditTracker);
1152 // Category Menu
1153 QStringList categories = BitTorrent::Session::instance()->categories();
1154 std::sort(categories.begin(), categories.end(), Utils::Compare::NaturalLessThan<Qt::CaseInsensitive>());
1156 QMenu *categoryMenu = listMenu->addMenu(UIThemeManager::instance()->getIcon(u"view-categories"_s), tr("Categor&y"));
1158 categoryMenu->addAction(UIThemeManager::instance()->getIcon(u"list-add"_s), tr("&New...", "New category...")
1159 , this, &TransferListWidget::askNewCategoryForSelection);
1160 categoryMenu->addAction(UIThemeManager::instance()->getIcon(u"edit-clear"_s), tr("&Reset", "Reset category")
1161 , this, [this]() { setSelectionCategory(u""_s); });
1162 categoryMenu->addSeparator();
1164 for (const QString &category : asConst(categories))
1166 const QString escapedCategory = QString(category).replace(u'&', u"&&"_s); // avoid '&' becomes accelerator key
1167 QAction *categoryAction = categoryMenu->addAction(UIThemeManager::instance()->getIcon(u"view-categories"_s), escapedCategory
1168 , this, [this, category]() { setSelectionCategory(category); });
1170 if (allSameCategory && (category == firstCategory))
1172 categoryAction->setCheckable(true);
1173 categoryAction->setChecked(true);
1177 // Tag Menu
1178 QMenu *tagsMenu = listMenu->addMenu(UIThemeManager::instance()->getIcon(u"tags"_s, u"view-categories"_s), tr("Ta&gs"));
1180 tagsMenu->addAction(UIThemeManager::instance()->getIcon(u"list-add"_s), tr("&Add...", "Add / assign multiple tags...")
1181 , this, &TransferListWidget::askAddTagsForSelection);
1182 tagsMenu->addAction(UIThemeManager::instance()->getIcon(u"edit-clear"_s), tr("&Remove All", "Remove all tags")
1183 , this, [this]()
1185 if (Preferences::instance()->confirmRemoveAllTags())
1186 confirmRemoveAllTagsForSelection();
1187 else
1188 clearSelectionTags();
1190 tagsMenu->addSeparator();
1192 const TagSet tags = BitTorrent::Session::instance()->tags();
1193 for (const Tag &tag : asConst(tags))
1195 auto *action = new TriStateAction(Utils::Gui::tagToWidgetText(tag), tagsMenu);
1196 action->setCloseOnInteraction(false);
1198 const Qt::CheckState initialState = tagsInAll.contains(tag) ? Qt::Checked
1199 : tagsInAny.contains(tag) ? Qt::PartiallyChecked : Qt::Unchecked;
1200 action->setCheckState(initialState);
1202 connect(action, &QAction::toggled, this, [this, tag](const bool checked)
1204 if (checked)
1205 addSelectionTag(tag);
1206 else
1207 removeSelectionTag(tag);
1210 tagsMenu->addAction(action);
1213 actionAutoTMM->setCheckState(allSameAutoTMM
1214 ? (firstAutoTMM ? Qt::Checked : Qt::Unchecked)
1215 : Qt::PartiallyChecked);
1216 listMenu->addAction(actionAutoTMM);
1218 listMenu->addSeparator();
1219 listMenu->addAction(actionTorrentOptions);
1220 if (!oneNotFinished && oneHasMetadata)
1222 actionSuperSeedingMode->setCheckState(allSameSuperSeeding
1223 ? (superSeedingMode ? Qt::Checked : Qt::Unchecked)
1224 : Qt::PartiallyChecked);
1225 listMenu->addAction(actionSuperSeedingMode);
1227 listMenu->addSeparator();
1228 bool addedPreviewAction = false;
1229 if (needsPreview)
1231 listMenu->addAction(actionPreviewFile);
1232 addedPreviewAction = true;
1234 if (oneNotFinished)
1236 actionSequentialDownload->setCheckState(allSameSequentialDownloadMode
1237 ? (sequentialDownloadMode ? Qt::Checked : Qt::Unchecked)
1238 : Qt::PartiallyChecked);
1239 listMenu->addAction(actionSequentialDownload);
1241 actionFirstLastPiecePrio->setCheckState(allSamePrioFirstlast
1242 ? (prioritizeFirstLast ? Qt::Checked : Qt::Unchecked)
1243 : Qt::PartiallyChecked);
1244 listMenu->addAction(actionFirstLastPiecePrio);
1246 addedPreviewAction = true;
1249 if (addedPreviewAction)
1250 listMenu->addSeparator();
1251 if (oneHasMetadata)
1252 listMenu->addAction(actionForceRecheck);
1253 // We can not force reannounce torrents that are stopped/errored/checking/missing files/queued.
1254 // We may already have the tracker list from magnet url. So we can force reannounce torrents without metadata anyway.
1255 listMenu->addAction(actionForceReannounce);
1256 actionForceReannounce->setEnabled(oneCanForceReannounce);
1257 if (!oneCanForceReannounce)
1258 actionForceReannounce->setToolTip(tr("Can not force reannounce if torrent is Stopped/Queued/Errored/Checking"));
1259 listMenu->addSeparator();
1260 listMenu->addAction(actionOpenDestinationFolder);
1261 if (BitTorrent::Session::instance()->isQueueingSystemEnabled() && oneNotFinished)
1263 listMenu->addSeparator();
1264 QMenu *queueMenu = listMenu->addMenu(
1265 UIThemeManager::instance()->getIcon(u"queued"_s), tr("&Queue"));
1266 queueMenu->addAction(actionTopQueuePos);
1267 queueMenu->addAction(actionIncreaseQueuePos);
1268 queueMenu->addAction(actionDecreaseQueuePos);
1269 queueMenu->addAction(actionBottomQueuePos);
1272 QMenu *copySubMenu = listMenu->addMenu(UIThemeManager::instance()->getIcon(u"edit-copy"_s), tr("&Copy"));
1273 copySubMenu->addAction(actionCopyName);
1274 copySubMenu->addAction(actionCopyHash1);
1275 actionCopyHash1->setEnabled(hasInfohashV1);
1276 copySubMenu->addAction(actionCopyHash2);
1277 actionCopyHash2->setEnabled(hasInfohashV2);
1278 copySubMenu->addAction(actionCopyMagnetLink);
1279 copySubMenu->addAction(actionCopyID);
1280 copySubMenu->addAction(actionCopyComment);
1282 actionExportTorrent->setToolTip(tr("Exported torrent is not necessarily the same as the imported"));
1283 listMenu->addAction(actionExportTorrent);
1285 listMenu->popup(QCursor::pos());
1288 void TransferListWidget::currentChanged(const QModelIndex &current, const QModelIndex&)
1290 qDebug("CURRENT CHANGED");
1291 BitTorrent::Torrent *torrent = nullptr;
1292 if (current.isValid())
1294 torrent = m_listModel->torrentHandle(mapToSource(current));
1295 // Fix scrolling to the lowermost visible torrent
1296 QMetaObject::invokeMethod(this, [this, current] { scrollTo(current); }, Qt::QueuedConnection);
1298 emit currentTorrentChanged(torrent);
1301 void TransferListWidget::applyCategoryFilter(const QString &category)
1303 if (category.isNull())
1304 m_sortFilterModel->disableCategoryFilter();
1305 else
1306 m_sortFilterModel->setCategoryFilter(category);
1309 void TransferListWidget::applyTagFilter(const std::optional<Tag> &tag)
1311 if (!tag)
1312 m_sortFilterModel->disableTagFilter();
1313 else
1314 m_sortFilterModel->setTagFilter(*tag);
1317 void TransferListWidget::applyTrackerFilterAll()
1319 m_sortFilterModel->disableTrackerFilter();
1322 void TransferListWidget::applyTrackerFilter(const QSet<BitTorrent::TorrentID> &torrentIDs)
1324 m_sortFilterModel->setTrackerFilter(torrentIDs);
1327 void TransferListWidget::applyFilter(const QString &name, const TransferListModel::Column &type)
1329 m_sortFilterModel->setFilterKeyColumn(type);
1330 const QString pattern = (Preferences::instance()->getRegexAsFilteringPatternForTransferList()
1331 ? name : Utils::String::wildcardToRegexPattern(name));
1332 m_sortFilterModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption));
1335 void TransferListWidget::applyStatusFilter(const int filterIndex)
1337 const auto filterType = static_cast<TorrentFilter::Type>(filterIndex);
1338 m_sortFilterModel->setStatusFilter(((filterType >= TorrentFilter::All) && (filterType < TorrentFilter::_Count)) ? filterType : TorrentFilter::All);
1339 // Select first item if nothing is selected
1340 if (selectionModel()->selectedRows(0).empty() && (m_sortFilterModel->rowCount() > 0))
1342 qDebug("Nothing is selected, selecting first row: %s", qUtf8Printable(m_sortFilterModel->index(0, TransferListModel::TR_NAME).data().toString()));
1343 selectionModel()->setCurrentIndex(m_sortFilterModel->index(0, TransferListModel::TR_NAME), QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows);
1347 void TransferListWidget::saveSettings()
1349 Preferences::instance()->setTransHeaderState(header()->saveState());
1352 bool TransferListWidget::loadSettings()
1354 return header()->restoreState(Preferences::instance()->getTransHeaderState());
1357 void TransferListWidget::wheelEvent(QWheelEvent *event)
1359 if (event->modifiers() & Qt::ShiftModifier)
1361 // Shift + scroll = horizontal scroll
1362 event->accept();
1363 QWheelEvent scrollHEvent {event->position(), event->globalPosition()
1364 , event->pixelDelta(), event->angleDelta().transposed(), event->buttons()
1365 , event->modifiers(), event->phase(), event->inverted(), event->source()};
1366 QTreeView::wheelEvent(&scrollHEvent);
1367 return;
1370 QTreeView::wheelEvent(event); // event delegated to base class