Correctly handle "torrent finished" events
[qBittorrent.git] / src / gui / transferlistwidget.cpp
blob9435b05beada8ffbebfda9f92197cb70e14ab865
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 <QMimeData>
42 #include <QRegularExpression>
43 #include <QSet>
44 #include <QShortcut>
45 #include <QWheelEvent>
47 #include "base/bittorrent/session.h"
48 #include "base/bittorrent/torrent.h"
49 #include "base/bittorrent/trackerentrystatus.h"
50 #include "base/global.h"
51 #include "base/logger.h"
52 #include "base/path.h"
53 #include "base/preferences.h"
54 #include "base/torrentfilter.h"
55 #include "base/utils/compare.h"
56 #include "base/utils/fs.h"
57 #include "base/utils/misc.h"
58 #include "base/utils/string.h"
59 #include "autoexpandabledialog.h"
60 #include "deletionconfirmationdialog.h"
61 #include "interfaces/iguiapplication.h"
62 #include "mainwindow.h"
63 #include "optionsdialog.h"
64 #include "previewselectdialog.h"
65 #include "speedlimitdialog.h"
66 #include "torrentcategorydialog.h"
67 #include "torrentcreatordialog.h"
68 #include "torrentoptionsdialog.h"
69 #include "trackerentriesdialog.h"
70 #include "transferlistdelegate.h"
71 #include "transferlistsortmodel.h"
72 #include "tristateaction.h"
73 #include "uithememanager.h"
74 #include "utils.h"
76 #ifdef Q_OS_MACOS
77 #include "macutilities.h"
78 #endif
80 namespace
82 QList<BitTorrent::TorrentID> extractIDs(const QList<BitTorrent::Torrent *> &torrents)
84 QList<BitTorrent::TorrentID> torrentIDs;
85 torrentIDs.reserve(torrents.size());
86 for (const BitTorrent::Torrent *torrent : torrents)
87 torrentIDs << torrent->id();
88 return torrentIDs;
91 bool torrentContainsPreviewableFiles(const BitTorrent::Torrent *const torrent)
93 if (!torrent->hasMetadata())
94 return false;
96 for (const Path &filePath : asConst(torrent->filePaths()))
98 if (Utils::Misc::isPreviewable(filePath))
99 return true;
102 return false;
105 void openDestinationFolder(const BitTorrent::Torrent *const torrent)
107 const Path contentPath = torrent->contentPath();
108 const Path openedPath = (!contentPath.isEmpty() ? contentPath : torrent->savePath());
109 #ifdef Q_OS_MACOS
110 MacUtils::openFiles({openedPath});
111 #else
112 if (torrent->filesCount() == 1)
113 Utils::Gui::openFolderSelect(openedPath);
114 else
115 Utils::Gui::openPath(openedPath);
116 #endif
119 void removeTorrents(const QList<BitTorrent::Torrent *> &torrents, const bool isDeleteFileSelected)
121 auto *session = BitTorrent::Session::instance();
122 const BitTorrent::TorrentRemoveOption removeOption = isDeleteFileSelected
123 ? BitTorrent::TorrentRemoveOption::RemoveContent : BitTorrent::TorrentRemoveOption::KeepContent;
124 for (const BitTorrent::Torrent *torrent : torrents)
125 session->removeTorrent(torrent->id(), removeOption);
129 TransferListWidget::TransferListWidget(IGUIApplication *app, QWidget *parent)
130 : GUIApplicationComponent(app, parent)
131 , m_listModel {new TransferListModel {this}}
132 , m_sortFilterModel {new TransferListSortModel {this}}
134 // Load settings
135 const bool columnLoaded = loadSettings();
137 // Create and apply delegate
138 setItemDelegate(new TransferListDelegate {this});
140 m_sortFilterModel->setDynamicSortFilter(true);
141 m_sortFilterModel->setSourceModel(m_listModel);
142 m_sortFilterModel->setFilterKeyColumn(TransferListModel::TR_NAME);
143 m_sortFilterModel->setFilterRole(Qt::DisplayRole);
144 m_sortFilterModel->setSortCaseSensitivity(Qt::CaseInsensitive);
145 m_sortFilterModel->setSortRole(TransferListModel::UnderlyingDataRole);
146 setModel(m_sortFilterModel);
148 // Visual settings
149 setUniformRowHeights(true);
150 setRootIsDecorated(false);
151 setAllColumnsShowFocus(true);
152 setSortingEnabled(true);
153 setSelectionMode(QAbstractItemView::ExtendedSelection);
154 setItemsExpandable(false);
155 setAutoScroll(true);
156 setAcceptDrops(true);
157 setDragDropMode(QAbstractItemView::DropOnly);
158 setDropIndicatorShown(true);
159 #if defined(Q_OS_MACOS)
160 setAttribute(Qt::WA_MacShowFocusRect, false);
161 #endif
162 header()->setFirstSectionMovable(true);
163 header()->setStretchLastSection(false);
164 header()->setTextElideMode(Qt::ElideRight);
166 // Default hidden columns
167 if (!columnLoaded)
169 setColumnHidden(TransferListModel::TR_ADD_DATE, true);
170 setColumnHidden(TransferListModel::TR_SEED_DATE, true);
171 setColumnHidden(TransferListModel::TR_UPLIMIT, true);
172 setColumnHidden(TransferListModel::TR_DLLIMIT, true);
173 setColumnHidden(TransferListModel::TR_TRACKER, true);
174 setColumnHidden(TransferListModel::TR_AMOUNT_DOWNLOADED, true);
175 setColumnHidden(TransferListModel::TR_AMOUNT_UPLOADED, true);
176 setColumnHidden(TransferListModel::TR_AMOUNT_DOWNLOADED_SESSION, true);
177 setColumnHidden(TransferListModel::TR_AMOUNT_UPLOADED_SESSION, true);
178 setColumnHidden(TransferListModel::TR_AMOUNT_LEFT, true);
179 setColumnHidden(TransferListModel::TR_TIME_ELAPSED, true);
180 setColumnHidden(TransferListModel::TR_SAVE_PATH, true);
181 setColumnHidden(TransferListModel::TR_DOWNLOAD_PATH, true);
182 setColumnHidden(TransferListModel::TR_INFOHASH_V1, true);
183 setColumnHidden(TransferListModel::TR_INFOHASH_V2, true);
184 setColumnHidden(TransferListModel::TR_COMPLETED, true);
185 setColumnHidden(TransferListModel::TR_RATIO_LIMIT, true);
186 setColumnHidden(TransferListModel::TR_POPULARITY, true);
187 setColumnHidden(TransferListModel::TR_SEEN_COMPLETE_DATE, true);
188 setColumnHidden(TransferListModel::TR_LAST_ACTIVITY, true);
189 setColumnHidden(TransferListModel::TR_TOTAL_SIZE, true);
190 setColumnHidden(TransferListModel::TR_REANNOUNCE, true);
191 setColumnHidden(TransferListModel::TR_PRIVATE, true);
194 //Ensure that at least one column is visible at all times
195 bool atLeastOne = false;
196 for (int i = 0; i < TransferListModel::NB_COLUMNS; ++i)
198 if (!isColumnHidden(i))
200 atLeastOne = true;
201 break;
204 if (!atLeastOne)
205 setColumnHidden(TransferListModel::TR_NAME, false);
207 //When adding/removing columns between versions some may
208 //end up being size 0 when the new version is launched with
209 //a conf file from the previous version.
210 for (int i = 0; i < TransferListModel::NB_COLUMNS; ++i)
212 if ((columnWidth(i) <= 0) && (!isColumnHidden(i)))
213 resizeColumnToContents(i);
216 setContextMenuPolicy(Qt::CustomContextMenu);
218 // Listen for list events
219 connect(this, &QAbstractItemView::doubleClicked, this, &TransferListWidget::torrentDoubleClicked);
220 connect(this, &QWidget::customContextMenuRequested, this, &TransferListWidget::displayListMenu);
221 header()->setContextMenuPolicy(Qt::CustomContextMenu);
222 connect(header(), &QWidget::customContextMenuRequested, this, &TransferListWidget::displayColumnHeaderMenu);
223 connect(header(), &QHeaderView::sectionMoved, this, &TransferListWidget::saveSettings);
224 connect(header(), &QHeaderView::sectionResized, this, &TransferListWidget::saveSettings);
225 connect(header(), &QHeaderView::sortIndicatorChanged, this, &TransferListWidget::saveSettings);
227 const auto *editHotkey = new QShortcut(Qt::Key_F2, this, nullptr, nullptr, Qt::WidgetShortcut);
228 connect(editHotkey, &QShortcut::activated, this, &TransferListWidget::renameSelectedTorrent);
229 const auto *deleteHotkey = new QShortcut(QKeySequence::Delete, this, nullptr, nullptr, Qt::WidgetShortcut);
230 connect(deleteHotkey, &QShortcut::activated, this, &TransferListWidget::softDeleteSelectedTorrents);
231 const auto *permDeleteHotkey = new QShortcut((Qt::SHIFT | Qt::Key_Delete), this, nullptr, nullptr, Qt::WidgetShortcut);
232 connect(permDeleteHotkey, &QShortcut::activated, this, &TransferListWidget::permDeleteSelectedTorrents);
233 const auto *doubleClickHotkeyReturn = new QShortcut(Qt::Key_Return, this, nullptr, nullptr, Qt::WidgetShortcut);
234 connect(doubleClickHotkeyReturn, &QShortcut::activated, this, &TransferListWidget::torrentDoubleClicked);
235 const auto *doubleClickHotkeyEnter = new QShortcut(Qt::Key_Enter, this, nullptr, nullptr, Qt::WidgetShortcut);
236 connect(doubleClickHotkeyEnter, &QShortcut::activated, this, &TransferListWidget::torrentDoubleClicked);
237 const auto *recheckHotkey = new QShortcut((Qt::CTRL | Qt::Key_R), this, nullptr, nullptr, Qt::WidgetShortcut);
238 connect(recheckHotkey, &QShortcut::activated, this, &TransferListWidget::recheckSelectedTorrents);
239 const auto *forceStartHotkey = new QShortcut((Qt::CTRL | Qt::Key_M), this, nullptr, nullptr, Qt::WidgetShortcut);
240 connect(forceStartHotkey, &QShortcut::activated, this, &TransferListWidget::forceStartSelectedTorrents);
243 TransferListWidget::~TransferListWidget()
245 // Save settings
246 saveSettings();
249 TransferListModel *TransferListWidget::getSourceModel() const
251 return m_listModel;
254 void TransferListWidget::previewFile(const Path &filePath)
256 Utils::Gui::openPath(filePath);
259 QModelIndex TransferListWidget::mapToSource(const QModelIndex &index) const
261 Q_ASSERT(index.isValid());
262 if (index.model() == m_sortFilterModel)
263 return m_sortFilterModel->mapToSource(index);
264 return index;
267 QModelIndexList TransferListWidget::mapToSource(const QModelIndexList &indexes) const
269 QModelIndexList result;
270 result.reserve(indexes.size());
271 for (const QModelIndex &index : indexes)
272 result.append(mapToSource(index));
274 return result;
277 QModelIndex TransferListWidget::mapFromSource(const QModelIndex &index) const
279 Q_ASSERT(index.isValid());
280 Q_ASSERT(index.model() == m_sortFilterModel);
281 return m_sortFilterModel->mapFromSource(index);
284 void TransferListWidget::torrentDoubleClicked()
286 const QModelIndexList selectedIndexes = selectionModel()->selectedRows();
287 if ((selectedIndexes.size() != 1) || !selectedIndexes.first().isValid())
288 return;
290 const QModelIndex index = m_listModel->index(mapToSource(selectedIndexes.first()).row());
291 BitTorrent::Torrent *const torrent = m_listModel->torrentHandle(index);
292 if (!torrent)
293 return;
295 int action;
296 if (torrent->isFinished())
297 action = Preferences::instance()->getActionOnDblClOnTorrentFn();
298 else
299 action = Preferences::instance()->getActionOnDblClOnTorrentDl();
301 switch (action)
303 case TOGGLE_STOP:
304 if (torrent->isStopped())
305 torrent->start();
306 else
307 torrent->stop();
308 break;
309 case PREVIEW_FILE:
310 if (torrentContainsPreviewableFiles(torrent))
312 auto *dialog = new PreviewSelectDialog(this, torrent);
313 dialog->setAttribute(Qt::WA_DeleteOnClose);
314 connect(dialog, &PreviewSelectDialog::readyToPreviewFile, this, &TransferListWidget::previewFile);
315 dialog->show();
317 else
319 openDestinationFolder(torrent);
321 break;
322 case OPEN_DEST:
323 openDestinationFolder(torrent);
324 break;
325 case SHOW_OPTIONS:
326 setTorrentOptions();
327 break;
331 QList<BitTorrent::Torrent *> TransferListWidget::getSelectedTorrents() const
333 const QModelIndexList selectedRows = selectionModel()->selectedRows();
335 QList<BitTorrent::Torrent *> torrents;
336 torrents.reserve(selectedRows.size());
337 for (const QModelIndex &index : selectedRows)
338 torrents << m_listModel->torrentHandle(mapToSource(index));
339 return torrents;
342 QList<BitTorrent::Torrent *> TransferListWidget::getVisibleTorrents() const
344 const int visibleTorrentsCount = m_sortFilterModel->rowCount();
346 QList<BitTorrent::Torrent *> torrents;
347 torrents.reserve(visibleTorrentsCount);
348 for (int i = 0; i < visibleTorrentsCount; ++i)
349 torrents << m_listModel->torrentHandle(mapToSource(m_sortFilterModel->index(i, 0)));
350 return torrents;
353 void TransferListWidget::setSelectedTorrentsLocation()
355 const QList<BitTorrent::Torrent *> torrents = getSelectedTorrents();
356 if (torrents.isEmpty())
357 return;
359 const Path oldLocation = torrents[0]->savePath();
361 auto *fileDialog = new QFileDialog(this, tr("Choose save path"), oldLocation.data());
362 fileDialog->setAttribute(Qt::WA_DeleteOnClose);
363 fileDialog->setFileMode(QFileDialog::Directory);
364 fileDialog->setOptions(QFileDialog::DontConfirmOverwrite | QFileDialog::ShowDirsOnly | QFileDialog::HideNameFilterDetails);
365 connect(fileDialog, &QDialog::accepted, this, [this, fileDialog]()
367 const QList<BitTorrent::Torrent *> torrents = getSelectedTorrents();
368 if (torrents.isEmpty())
369 return;
371 const Path newLocation {fileDialog->selectedFiles().constFirst()};
372 if (!newLocation.exists())
373 return;
375 // Actually move storage
376 for (BitTorrent::Torrent *const torrent : torrents)
378 torrent->setAutoTMMEnabled(false);
379 torrent->setSavePath(newLocation);
383 fileDialog->open();
386 void TransferListWidget::pauseSession()
388 BitTorrent::Session::instance()->pause();
391 void TransferListWidget::resumeSession()
393 BitTorrent::Session::instance()->resume();
396 void TransferListWidget::startSelectedTorrents()
398 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
399 torrent->start();
402 void TransferListWidget::forceStartSelectedTorrents()
404 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
405 torrent->start(BitTorrent::TorrentOperatingMode::Forced);
408 void TransferListWidget::startVisibleTorrents()
410 for (BitTorrent::Torrent *const torrent : asConst(getVisibleTorrents()))
411 torrent->start();
414 void TransferListWidget::stopSelectedTorrents()
416 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
417 torrent->stop();
420 void TransferListWidget::stopVisibleTorrents()
422 for (BitTorrent::Torrent *const torrent : asConst(getVisibleTorrents()))
423 torrent->stop();
426 void TransferListWidget::softDeleteSelectedTorrents()
428 deleteSelectedTorrents(false);
431 void TransferListWidget::permDeleteSelectedTorrents()
433 deleteSelectedTorrents(true);
436 void TransferListWidget::deleteSelectedTorrents(const bool deleteLocalFiles)
438 if (app()->mainWindow()->currentTabWidget() != this) return;
440 const QList<BitTorrent::Torrent *> torrents = getSelectedTorrents();
441 if (torrents.empty()) return;
443 if (Preferences::instance()->confirmTorrentDeletion())
445 auto *dialog = new DeletionConfirmationDialog(this, torrents.size(), torrents[0]->name(), deleteLocalFiles);
446 dialog->setAttribute(Qt::WA_DeleteOnClose);
447 connect(dialog, &DeletionConfirmationDialog::accepted, this, [this, dialog]()
449 // Some torrents might be removed when waiting for user input, so refetch the torrent list
450 // NOTE: this will only work when dialog is modal
451 removeTorrents(getSelectedTorrents(), dialog->isRemoveContentSelected());
453 dialog->open();
455 else
457 removeTorrents(torrents, deleteLocalFiles);
461 void TransferListWidget::deleteVisibleTorrents()
463 const QList<BitTorrent::Torrent *> torrents = getVisibleTorrents();
464 if (torrents.empty()) return;
466 if (Preferences::instance()->confirmTorrentDeletion())
468 auto *dialog = new DeletionConfirmationDialog(this, torrents.size(), torrents[0]->name(), false);
469 dialog->setAttribute(Qt::WA_DeleteOnClose);
470 connect(dialog, &DeletionConfirmationDialog::accepted, this, [this, dialog]()
472 // Some torrents might be removed when waiting for user input, so refetch the torrent list
473 // NOTE: this will only work when dialog is modal
474 removeTorrents(getVisibleTorrents(), dialog->isRemoveContentSelected());
476 dialog->open();
478 else
480 removeTorrents(torrents, false);
484 void TransferListWidget::increaseQueuePosSelectedTorrents()
486 qDebug() << Q_FUNC_INFO;
487 if (app()->mainWindow()->currentTabWidget() == this)
488 BitTorrent::Session::instance()->increaseTorrentsQueuePos(extractIDs(getSelectedTorrents()));
491 void TransferListWidget::decreaseQueuePosSelectedTorrents()
493 qDebug() << Q_FUNC_INFO;
494 if (app()->mainWindow()->currentTabWidget() == this)
495 BitTorrent::Session::instance()->decreaseTorrentsQueuePos(extractIDs(getSelectedTorrents()));
498 void TransferListWidget::topQueuePosSelectedTorrents()
500 if (app()->mainWindow()->currentTabWidget() == this)
501 BitTorrent::Session::instance()->topTorrentsQueuePos(extractIDs(getSelectedTorrents()));
504 void TransferListWidget::bottomQueuePosSelectedTorrents()
506 if (app()->mainWindow()->currentTabWidget() == this)
507 BitTorrent::Session::instance()->bottomTorrentsQueuePos(extractIDs(getSelectedTorrents()));
510 void TransferListWidget::copySelectedMagnetURIs() const
512 QStringList magnetUris;
513 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
514 magnetUris << torrent->createMagnetURI();
516 qApp->clipboard()->setText(magnetUris.join(u'\n'));
519 void TransferListWidget::copySelectedNames() const
521 QStringList torrentNames;
522 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
523 torrentNames << torrent->name();
525 qApp->clipboard()->setText(torrentNames.join(u'\n'));
528 void TransferListWidget::copySelectedInfohashes(const CopyInfohashPolicy policy) const
530 const auto selectedTorrents = getSelectedTorrents();
531 QStringList infoHashes;
532 infoHashes.reserve(selectedTorrents.size());
533 switch (policy)
535 case CopyInfohashPolicy::Version1:
536 for (const BitTorrent::Torrent *torrent : selectedTorrents)
538 if (const auto infoHash = torrent->infoHash().v1(); infoHash.isValid())
539 infoHashes << infoHash.toString();
541 break;
542 case CopyInfohashPolicy::Version2:
543 for (const BitTorrent::Torrent *torrent : selectedTorrents)
545 if (const auto infoHash = torrent->infoHash().v2(); infoHash.isValid())
546 infoHashes << infoHash.toString();
548 break;
551 qApp->clipboard()->setText(infoHashes.join(u'\n'));
554 void TransferListWidget::copySelectedIDs() const
556 QStringList torrentIDs;
557 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
558 torrentIDs << torrent->id().toString();
560 qApp->clipboard()->setText(torrentIDs.join(u'\n'));
563 void TransferListWidget::copySelectedComments() const
565 QStringList torrentComments;
566 for (const BitTorrent::Torrent *torrent : asConst(getSelectedTorrents()))
568 if (!torrent->comment().isEmpty())
569 torrentComments << torrent->comment();
572 qApp->clipboard()->setText(torrentComments.join(u"\n---------\n"_s));
575 void TransferListWidget::hideQueuePosColumn(bool hide)
577 setColumnHidden(TransferListModel::TR_QUEUE_POSITION, hide);
578 if (!hide && (columnWidth(TransferListModel::TR_QUEUE_POSITION) == 0))
579 resizeColumnToContents(TransferListModel::TR_QUEUE_POSITION);
582 void TransferListWidget::openSelectedTorrentsFolder() const
584 QSet<Path> paths;
585 #ifdef Q_OS_MACOS
586 // On macOS you expect both the files and folders to be opened in their parent
587 // folders prehilighted for opening, so we use a custom method.
588 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
590 const Path contentPath = torrent->contentPath();
591 paths.insert(!contentPath.isEmpty() ? contentPath : torrent->savePath());
593 MacUtils::openFiles(PathList(paths.cbegin(), paths.cend()));
594 #else
595 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
597 const Path contentPath = torrent->contentPath();
598 const Path openedPath = (!contentPath.isEmpty() ? contentPath : torrent->savePath());
599 if (!paths.contains(openedPath))
601 if (torrent->filesCount() == 1)
602 Utils::Gui::openFolderSelect(openedPath);
603 else
604 Utils::Gui::openPath(openedPath);
606 paths.insert(openedPath);
608 #endif // Q_OS_MACOS
611 void TransferListWidget::previewSelectedTorrents()
613 for (const BitTorrent::Torrent *torrent : asConst(getSelectedTorrents()))
615 if (torrentContainsPreviewableFiles(torrent))
617 auto *dialog = new PreviewSelectDialog(this, torrent);
618 dialog->setAttribute(Qt::WA_DeleteOnClose);
619 connect(dialog, &PreviewSelectDialog::readyToPreviewFile, this, &TransferListWidget::previewFile);
620 dialog->show();
622 else
624 QMessageBox::critical(this, tr("Unable to preview"), tr("The selected torrent \"%1\" does not contain previewable files")
625 .arg(torrent->name()));
630 void TransferListWidget::setTorrentOptions()
632 const QList<BitTorrent::Torrent *> selectedTorrents = getSelectedTorrents();
633 if (selectedTorrents.empty()) return;
635 auto *dialog = new TorrentOptionsDialog {this, selectedTorrents};
636 dialog->setAttribute(Qt::WA_DeleteOnClose);
637 dialog->open();
640 void TransferListWidget::recheckSelectedTorrents()
642 if (Preferences::instance()->confirmTorrentRecheck())
644 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);
645 if (ret != QMessageBox::Yes) return;
648 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
649 torrent->forceRecheck();
652 void TransferListWidget::reannounceSelectedTorrents()
654 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
655 torrent->forceReannounce();
658 int TransferListWidget::visibleColumnsCount() const
660 int count = 0;
661 for (int i = 0, iMax = header()->count(); i < iMax; ++i)
663 if (!isColumnHidden(i))
664 ++count;
667 return count;
670 // hide/show columns menu
671 void TransferListWidget::displayColumnHeaderMenu()
673 auto *menu = new QMenu(this);
674 menu->setAttribute(Qt::WA_DeleteOnClose);
675 menu->setTitle(tr("Column visibility"));
676 menu->setToolTipsVisible(true);
678 for (int i = 0; i < TransferListModel::NB_COLUMNS; ++i)
680 if (!BitTorrent::Session::instance()->isQueueingSystemEnabled() && (i == TransferListModel::TR_QUEUE_POSITION))
681 continue;
683 const auto columnName = m_listModel->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString();
684 const QVariant columnToolTip = m_listModel->headerData(i, Qt::Horizontal, Qt::ToolTipRole);
685 QAction *action = menu->addAction(columnName, this, [this, i](const bool checked)
687 if (!checked && (visibleColumnsCount() <= 1))
688 return;
690 setColumnHidden(i, !checked);
692 if (checked && (columnWidth(i) <= 5))
693 resizeColumnToContents(i);
695 saveSettings();
697 action->setCheckable(true);
698 action->setChecked(!isColumnHidden(i));
699 if (!columnToolTip.isNull())
700 action->setToolTip(columnToolTip.toString());
703 menu->addSeparator();
704 QAction *resizeAction = menu->addAction(tr("Resize columns"), this, [this]()
706 for (int i = 0, count = header()->count(); i < count; ++i)
708 if (!isColumnHidden(i))
709 resizeColumnToContents(i);
711 saveSettings();
713 resizeAction->setToolTip(tr("Resize all non-hidden columns to the size of their contents"));
715 menu->popup(QCursor::pos());
718 void TransferListWidget::setSelectedTorrentsSuperSeeding(const bool enabled) const
720 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
722 if (torrent->hasMetadata())
723 torrent->setSuperSeeding(enabled);
727 void TransferListWidget::setSelectedTorrentsSequentialDownload(const bool enabled) const
729 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
730 torrent->setSequentialDownload(enabled);
733 void TransferListWidget::setSelectedFirstLastPiecePrio(const bool enabled) const
735 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
736 torrent->setFirstLastPiecePriority(enabled);
739 void TransferListWidget::setSelectedAutoTMMEnabled(const bool enabled)
741 if (enabled)
743 const QMessageBox::StandardButton btn = QMessageBox::question(this, tr("Enable automatic torrent management")
744 , tr("Are you sure you want to enable Automatic Torrent Management for the selected torrent(s)? They may be relocated.")
745 , (QMessageBox::Yes | QMessageBox::No), QMessageBox::Yes);
746 if (btn != QMessageBox::Yes) return;
749 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
750 torrent->setAutoTMMEnabled(enabled);
753 void TransferListWidget::askNewCategoryForSelection()
755 const QString newCategoryName = TorrentCategoryDialog::createCategory(this);
756 if (!newCategoryName.isEmpty())
757 setSelectionCategory(newCategoryName);
760 void TransferListWidget::askAddTagsForSelection()
762 const TagSet tags = askTagsForSelection(tr("Add tags"));
763 for (const Tag &tag : tags)
764 addSelectionTag(tag);
767 void TransferListWidget::editTorrentTrackers()
769 const QList<BitTorrent::Torrent *> torrents = getSelectedTorrents();
770 QList<BitTorrent::TrackerEntry> commonTrackers;
772 if (!torrents.empty())
774 for (const BitTorrent::TrackerEntryStatus &status : asConst(torrents[0]->trackers()))
775 commonTrackers.append({.url = status.url, .tier = status.tier});
777 for (const BitTorrent::Torrent *torrent : torrents)
779 QSet<BitTorrent::TrackerEntry> trackerSet;
780 for (const BitTorrent::TrackerEntryStatus &status : asConst(torrent->trackers()))
781 trackerSet.insert({.url = status.url, .tier = status.tier});
783 commonTrackers.erase(std::remove_if(commonTrackers.begin(), commonTrackers.end()
784 , [&trackerSet](const BitTorrent::TrackerEntry &entry) { return !trackerSet.contains(entry); })
785 , commonTrackers.end());
789 auto *trackerDialog = new TrackerEntriesDialog(this);
790 trackerDialog->setAttribute(Qt::WA_DeleteOnClose);
791 trackerDialog->setTrackers(commonTrackers);
793 connect(trackerDialog, &QDialog::accepted, this, [torrents, trackerDialog]()
795 for (BitTorrent::Torrent *torrent : torrents)
796 torrent->replaceTrackers(trackerDialog->trackers());
799 trackerDialog->open();
802 void TransferListWidget::exportTorrent()
804 if (getSelectedTorrents().isEmpty())
805 return;
807 auto *fileDialog = new QFileDialog(this, tr("Choose folder to save exported .torrent files"));
808 fileDialog->setAttribute(Qt::WA_DeleteOnClose);
809 fileDialog->setFileMode(QFileDialog::Directory);
810 fileDialog->setOptions(QFileDialog::ShowDirsOnly);
811 connect(fileDialog, &QFileDialog::fileSelected, this, [this](const QString &dir)
813 const QList<BitTorrent::Torrent *> torrents = getSelectedTorrents();
814 if (torrents.isEmpty())
815 return;
817 const Path savePath {dir};
818 if (!savePath.exists())
819 return;
821 const QString errorMsg = tr("Export .torrent file failed. Torrent: \"%1\". Save path: \"%2\". Reason: \"%3\"");
823 bool hasError = false;
824 for (const BitTorrent::Torrent *torrent : torrents)
826 const QString validName = Utils::Fs::toValidFileName(torrent->name(), u"_"_s);
827 const Path filePath = savePath / Path(validName + u".torrent");
828 if (filePath.exists())
830 LogMsg(errorMsg.arg(torrent->name(), filePath.toString(), tr("A file with the same name already exists")) , Log::WARNING);
831 hasError = true;
832 continue;
835 const nonstd::expected<void, QString> result = torrent->exportToFile(filePath);
836 if (!result)
838 LogMsg(errorMsg.arg(torrent->name(), filePath.toString(), result.error()) , Log::WARNING);
839 hasError = true;
840 continue;
844 if (hasError)
846 QMessageBox::warning(this, tr("Export .torrent file error")
847 , tr("Errors occurred when exporting .torrent files. Check execution log for details."));
851 fileDialog->open();
854 void TransferListWidget::confirmRemoveAllTagsForSelection()
856 QMessageBox::StandardButton response = QMessageBox::question(
857 this, tr("Remove All Tags"), tr("Remove all tags from selected torrents?"),
858 QMessageBox::Yes | QMessageBox::No);
859 if (response == QMessageBox::Yes)
860 clearSelectionTags();
863 TagSet TransferListWidget::askTagsForSelection(const QString &dialogTitle)
865 TagSet tags;
866 bool invalid = true;
867 while (invalid)
869 bool ok = false;
870 invalid = false;
871 const QString tagsInput = AutoExpandableDialog::getText(
872 this, dialogTitle, tr("Comma-separated tags:"), QLineEdit::Normal, {}, &ok).trimmed();
873 if (!ok || tagsInput.isEmpty())
874 return {};
876 const QStringList tagStrings = tagsInput.split(u',', Qt::SkipEmptyParts);
877 tags.clear();
878 for (const QString &tagStr : tagStrings)
880 const Tag tag {tagStr};
881 if (!tag.isValid())
883 QMessageBox::warning(this, tr("Invalid tag"), tr("Tag name: '%1' is invalid").arg(tag.toString()));
884 invalid = true;
887 if (!invalid)
888 tags.insert(tag);
892 return tags;
895 void TransferListWidget::applyToSelectedTorrents(const std::function<void (BitTorrent::Torrent *const)> &fn)
897 // Changing the data may affect the layout of the sort/filter model, which in turn may invalidate
898 // the indexes previously obtained from selection model before we process them all.
899 // Therefore, we must map all the selected indexes to source before start processing them.
900 const QModelIndexList sourceRows = mapToSource(selectionModel()->selectedRows());
901 for (const QModelIndex &index : sourceRows)
903 BitTorrent::Torrent *const torrent = m_listModel->torrentHandle(index);
904 Q_ASSERT(torrent);
905 fn(torrent);
909 void TransferListWidget::renameSelectedTorrent()
911 const QModelIndexList selectedIndexes = selectionModel()->selectedRows();
912 if ((selectedIndexes.size() != 1) || !selectedIndexes.first().isValid())
913 return;
915 const QModelIndex mi = m_listModel->index(mapToSource(selectedIndexes.first()).row(), TransferListModel::TR_NAME);
916 BitTorrent::Torrent *const torrent = m_listModel->torrentHandle(mi);
917 if (!torrent)
918 return;
920 // Ask for a new Name
921 bool ok = false;
922 QString name = AutoExpandableDialog::getText(this, tr("Rename"), tr("New name:"), QLineEdit::Normal, torrent->name(), &ok);
923 if (ok && !name.isEmpty())
925 name.replace(QRegularExpression(u"\r?\n|\r"_s), u" "_s);
926 // Rename the torrent
927 m_listModel->setData(mi, name, Qt::DisplayRole);
931 void TransferListWidget::setSelectionCategory(const QString &category)
933 applyToSelectedTorrents([&category](BitTorrent::Torrent *torrent) { torrent->setCategory(category); });
936 void TransferListWidget::addSelectionTag(const Tag &tag)
938 applyToSelectedTorrents([&tag](BitTorrent::Torrent *const torrent) { torrent->addTag(tag); });
941 void TransferListWidget::removeSelectionTag(const Tag &tag)
943 applyToSelectedTorrents([&tag](BitTorrent::Torrent *const torrent) { torrent->removeTag(tag); });
946 void TransferListWidget::clearSelectionTags()
948 applyToSelectedTorrents([](BitTorrent::Torrent *const torrent) { torrent->removeAllTags(); });
951 void TransferListWidget::displayListMenu()
953 const QModelIndexList selectedIndexes = selectionModel()->selectedRows();
954 if (selectedIndexes.isEmpty())
955 return;
957 auto *listMenu = new QMenu(this);
958 listMenu->setAttribute(Qt::WA_DeleteOnClose);
959 listMenu->setToolTipsVisible(true);
961 // Create actions
963 auto *actionStart = new QAction(UIThemeManager::instance()->getIcon(u"torrent-start"_s, u"media-playback-start"_s), tr("&Start", "Resume/start the torrent"), listMenu);
964 connect(actionStart, &QAction::triggered, this, &TransferListWidget::startSelectedTorrents);
965 auto *actionStop = new QAction(UIThemeManager::instance()->getIcon(u"torrent-stop"_s, u"media-playback-pause"_s), tr("Sto&p", "Stop the torrent"), listMenu);
966 connect(actionStop, &QAction::triggered, this, &TransferListWidget::stopSelectedTorrents);
967 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);
968 connect(actionForceStart, &QAction::triggered, this, &TransferListWidget::forceStartSelectedTorrents);
969 auto *actionDelete = new QAction(UIThemeManager::instance()->getIcon(u"list-remove"_s), tr("&Remove", "Remove the torrent"), listMenu);
970 connect(actionDelete, &QAction::triggered, this, &TransferListWidget::softDeleteSelectedTorrents);
971 auto *actionPreviewFile = new QAction(UIThemeManager::instance()->getIcon(u"view-preview"_s), tr("Pre&view file..."), listMenu);
972 connect(actionPreviewFile, &QAction::triggered, this, &TransferListWidget::previewSelectedTorrents);
973 auto *actionTorrentOptions = new QAction(UIThemeManager::instance()->getIcon(u"configure"_s), tr("Torrent &options..."), listMenu);
974 connect(actionTorrentOptions, &QAction::triggered, this, &TransferListWidget::setTorrentOptions);
975 auto *actionOpenDestinationFolder = new QAction(UIThemeManager::instance()->getIcon(u"directory"_s), tr("Open destination &folder"), listMenu);
976 connect(actionOpenDestinationFolder, &QAction::triggered, this, &TransferListWidget::openSelectedTorrentsFolder);
977 auto *actionIncreaseQueuePos = new QAction(UIThemeManager::instance()->getIcon(u"go-up"_s), tr("Move &up", "i.e. move up in the queue"), listMenu);
978 connect(actionIncreaseQueuePos, &QAction::triggered, this, &TransferListWidget::increaseQueuePosSelectedTorrents);
979 auto *actionDecreaseQueuePos = new QAction(UIThemeManager::instance()->getIcon(u"go-down"_s), tr("Move &down", "i.e. Move down in the queue"), listMenu);
980 connect(actionDecreaseQueuePos, &QAction::triggered, this, &TransferListWidget::decreaseQueuePosSelectedTorrents);
981 auto *actionTopQueuePos = new QAction(UIThemeManager::instance()->getIcon(u"go-top"_s), tr("Move to &top", "i.e. Move to top of the queue"), listMenu);
982 connect(actionTopQueuePos, &QAction::triggered, this, &TransferListWidget::topQueuePosSelectedTorrents);
983 auto *actionBottomQueuePos = new QAction(UIThemeManager::instance()->getIcon(u"go-bottom"_s), tr("Move to &bottom", "i.e. Move to bottom of the queue"), listMenu);
984 connect(actionBottomQueuePos, &QAction::triggered, this, &TransferListWidget::bottomQueuePosSelectedTorrents);
985 auto *actionSetTorrentPath = new QAction(UIThemeManager::instance()->getIcon(u"set-location"_s, u"inode-directory"_s), tr("Set loc&ation..."), listMenu);
986 connect(actionSetTorrentPath, &QAction::triggered, this, &TransferListWidget::setSelectedTorrentsLocation);
987 auto *actionForceRecheck = new QAction(UIThemeManager::instance()->getIcon(u"force-recheck"_s, u"document-edit-verify"_s), tr("Force rec&heck"), listMenu);
988 connect(actionForceRecheck, &QAction::triggered, this, &TransferListWidget::recheckSelectedTorrents);
989 auto *actionForceReannounce = new QAction(UIThemeManager::instance()->getIcon(u"reannounce"_s, u"document-edit-verify"_s), tr("Force r&eannounce"), listMenu);
990 connect(actionForceReannounce, &QAction::triggered, this, &TransferListWidget::reannounceSelectedTorrents);
991 auto *actionCopyMagnetLink = new QAction(UIThemeManager::instance()->getIcon(u"torrent-magnet"_s, u"kt-magnet"_s), tr("&Magnet link"), listMenu);
992 connect(actionCopyMagnetLink, &QAction::triggered, this, &TransferListWidget::copySelectedMagnetURIs);
993 auto *actionCopyID = new QAction(UIThemeManager::instance()->getIcon(u"help-about"_s, u"edit-copy"_s), tr("Torrent &ID"), listMenu);
994 connect(actionCopyID, &QAction::triggered, this, &TransferListWidget::copySelectedIDs);
995 auto *actionCopyComment = new QAction(UIThemeManager::instance()->getIcon(u"edit-copy"_s), tr("&Comment"), listMenu);
996 connect(actionCopyComment, &QAction::triggered, this, &TransferListWidget::copySelectedComments);
997 auto *actionCopyName = new QAction(UIThemeManager::instance()->getIcon(u"name"_s, u"edit-copy"_s), tr("&Name"), listMenu);
998 connect(actionCopyName, &QAction::triggered, this, &TransferListWidget::copySelectedNames);
999 auto *actionCopyHash1 = new QAction(UIThemeManager::instance()->getIcon(u"hash"_s, u"edit-copy"_s), tr("Info &hash v1"), listMenu);
1000 connect(actionCopyHash1, &QAction::triggered, this, [this]() { copySelectedInfohashes(CopyInfohashPolicy::Version1); });
1001 auto *actionCopyHash2 = new QAction(UIThemeManager::instance()->getIcon(u"hash"_s, u"edit-copy"_s), tr("Info h&ash v2"), listMenu);
1002 connect(actionCopyHash2, &QAction::triggered, this, [this]() { copySelectedInfohashes(CopyInfohashPolicy::Version2); });
1003 auto *actionSuperSeedingMode = new TriStateAction(tr("Super seeding mode"), listMenu);
1004 connect(actionSuperSeedingMode, &QAction::triggered, this, &TransferListWidget::setSelectedTorrentsSuperSeeding);
1005 auto *actionRename = new QAction(UIThemeManager::instance()->getIcon(u"edit-rename"_s), tr("Re&name..."), listMenu);
1006 connect(actionRename, &QAction::triggered, this, &TransferListWidget::renameSelectedTorrent);
1007 auto *actionSequentialDownload = new TriStateAction(tr("Download in sequential order"), listMenu);
1008 connect(actionSequentialDownload, &QAction::triggered, this, &TransferListWidget::setSelectedTorrentsSequentialDownload);
1009 auto *actionFirstLastPiecePrio = new TriStateAction(tr("Download first and last pieces first"), listMenu);
1010 connect(actionFirstLastPiecePrio, &QAction::triggered, this, &TransferListWidget::setSelectedFirstLastPiecePrio);
1011 auto *actionAutoTMM = new TriStateAction(tr("Automatic Torrent Management"), listMenu);
1012 actionAutoTMM->setToolTip(tr("Automatic mode means that various torrent properties (e.g. save path) will be decided by the associated category"));
1013 connect(actionAutoTMM, &QAction::triggered, this, &TransferListWidget::setSelectedAutoTMMEnabled);
1014 auto *actionEditTracker = new QAction(UIThemeManager::instance()->getIcon(u"edit-rename"_s), tr("Edit trac&kers..."), listMenu);
1015 connect(actionEditTracker, &QAction::triggered, this, &TransferListWidget::editTorrentTrackers);
1016 auto *actionExportTorrent = new QAction(UIThemeManager::instance()->getIcon(u"edit-copy"_s), tr("E&xport .torrent..."), listMenu);
1017 connect(actionExportTorrent, &QAction::triggered, this, &TransferListWidget::exportTorrent);
1018 // End of actions
1020 // Enable/disable stop/start action given the DL state
1021 bool needsStop = false, needsStart = false, needsForce = false, needsPreview = false;
1022 bool allSameSuperSeeding = true;
1023 bool superSeedingMode = false;
1024 bool allSameSequentialDownloadMode = true, allSamePrioFirstlast = true;
1025 bool sequentialDownloadMode = false, prioritizeFirstLast = false;
1026 bool oneHasMetadata = false, oneNotFinished = false;
1027 bool allSameCategory = true;
1028 bool allSameAutoTMM = true;
1029 bool firstAutoTMM = false;
1030 QString firstCategory;
1031 bool first = true;
1032 TagSet tagsInAny;
1033 TagSet tagsInAll;
1034 bool hasInfohashV1 = false, hasInfohashV2 = false;
1035 bool oneCanForceReannounce = false;
1037 for (const QModelIndex &index : selectedIndexes)
1039 const BitTorrent::Torrent *torrent = m_listModel->torrentHandle(mapToSource(index));
1040 if (!torrent)
1041 continue;
1043 if (firstCategory.isEmpty() && first)
1044 firstCategory = torrent->category();
1045 if (firstCategory != torrent->category())
1046 allSameCategory = false;
1048 const TagSet torrentTags = torrent->tags();
1049 tagsInAny.unite(torrentTags);
1051 if (first)
1053 firstAutoTMM = torrent->isAutoTMMEnabled();
1054 tagsInAll = torrentTags;
1056 else
1058 tagsInAll.intersect(torrentTags);
1061 if (firstAutoTMM != torrent->isAutoTMMEnabled())
1062 allSameAutoTMM = false;
1064 if (torrent->hasMetadata())
1065 oneHasMetadata = true;
1066 if (!torrent->isFinished())
1068 oneNotFinished = true;
1069 if (first)
1071 sequentialDownloadMode = torrent->isSequentialDownload();
1072 prioritizeFirstLast = torrent->hasFirstLastPiecePriority();
1074 else
1076 if (sequentialDownloadMode != torrent->isSequentialDownload())
1077 allSameSequentialDownloadMode = false;
1078 if (prioritizeFirstLast != torrent->hasFirstLastPiecePriority())
1079 allSamePrioFirstlast = false;
1082 else
1084 if (!oneNotFinished && allSameSuperSeeding && torrent->hasMetadata())
1086 if (first)
1087 superSeedingMode = torrent->superSeeding();
1088 else if (superSeedingMode != torrent->superSeeding())
1089 allSameSuperSeeding = false;
1093 if (!torrent->isForced())
1094 needsForce = true;
1095 else
1096 needsStart = true;
1098 const bool isStopped = torrent->isStopped();
1099 if (isStopped)
1100 needsStart = true;
1101 else
1102 needsStop = true;
1104 if (torrent->isErrored() || torrent->hasMissingFiles())
1106 // If torrent is in "errored" or "missing files" state
1107 // it cannot keep further processing until you restart it.
1108 needsStart = true;
1109 needsForce = true;
1112 if (torrent->hasMetadata())
1113 needsPreview = true;
1115 if (!hasInfohashV1 && torrent->infoHash().v1().isValid())
1116 hasInfohashV1 = true;
1117 if (!hasInfohashV2 && torrent->infoHash().v2().isValid())
1118 hasInfohashV2 = true;
1120 first = false;
1122 const bool rechecking = torrent->isChecking();
1123 if (rechecking)
1125 needsStart = true;
1126 needsStop = true;
1129 const bool queued = torrent->isQueued();
1130 if (!isStopped && !rechecking && !queued)
1131 oneCanForceReannounce = true;
1133 if (oneHasMetadata && oneNotFinished && !allSameSequentialDownloadMode
1134 && !allSamePrioFirstlast && !allSameSuperSeeding && !allSameCategory
1135 && needsStart && needsForce && needsStop && needsPreview && !allSameAutoTMM
1136 && hasInfohashV1 && hasInfohashV2 && oneCanForceReannounce)
1138 break;
1142 if (needsStart)
1143 listMenu->addAction(actionStart);
1144 if (needsStop)
1145 listMenu->addAction(actionStop);
1146 if (needsForce)
1147 listMenu->addAction(actionForceStart);
1148 listMenu->addSeparator();
1149 listMenu->addAction(actionDelete);
1150 listMenu->addSeparator();
1151 listMenu->addAction(actionSetTorrentPath);
1152 if (selectedIndexes.size() == 1)
1153 listMenu->addAction(actionRename);
1154 listMenu->addAction(actionEditTracker);
1156 // Category Menu
1157 QStringList categories = BitTorrent::Session::instance()->categories();
1158 std::sort(categories.begin(), categories.end(), Utils::Compare::NaturalLessThan<Qt::CaseInsensitive>());
1160 QMenu *categoryMenu = listMenu->addMenu(UIThemeManager::instance()->getIcon(u"view-categories"_s), tr("Categor&y"));
1162 categoryMenu->addAction(UIThemeManager::instance()->getIcon(u"list-add"_s), tr("&New...", "New category...")
1163 , this, &TransferListWidget::askNewCategoryForSelection);
1164 categoryMenu->addAction(UIThemeManager::instance()->getIcon(u"edit-clear"_s), tr("&Reset", "Reset category")
1165 , this, [this]() { setSelectionCategory(u""_s); });
1166 categoryMenu->addSeparator();
1168 for (const QString &category : asConst(categories))
1170 const QString escapedCategory = QString(category).replace(u'&', u"&&"_s); // avoid '&' becomes accelerator key
1171 QAction *categoryAction = categoryMenu->addAction(UIThemeManager::instance()->getIcon(u"view-categories"_s), escapedCategory
1172 , this, [this, category]() { setSelectionCategory(category); });
1174 if (allSameCategory && (category == firstCategory))
1176 categoryAction->setCheckable(true);
1177 categoryAction->setChecked(true);
1181 // Tag Menu
1182 QMenu *tagsMenu = listMenu->addMenu(UIThemeManager::instance()->getIcon(u"tags"_s, u"view-categories"_s), tr("Ta&gs"));
1184 tagsMenu->addAction(UIThemeManager::instance()->getIcon(u"list-add"_s), tr("&Add...", "Add / assign multiple tags...")
1185 , this, &TransferListWidget::askAddTagsForSelection);
1186 tagsMenu->addAction(UIThemeManager::instance()->getIcon(u"edit-clear"_s), tr("&Remove All", "Remove all tags")
1187 , this, [this]()
1189 if (Preferences::instance()->confirmRemoveAllTags())
1190 confirmRemoveAllTagsForSelection();
1191 else
1192 clearSelectionTags();
1194 tagsMenu->addSeparator();
1196 const TagSet tags = BitTorrent::Session::instance()->tags();
1197 for (const Tag &tag : asConst(tags))
1199 auto *action = new TriStateAction(Utils::Gui::tagToWidgetText(tag), tagsMenu);
1200 action->setCloseOnInteraction(false);
1202 const Qt::CheckState initialState = tagsInAll.contains(tag) ? Qt::Checked
1203 : tagsInAny.contains(tag) ? Qt::PartiallyChecked : Qt::Unchecked;
1204 action->setCheckState(initialState);
1206 connect(action, &QAction::toggled, this, [this, tag](const bool checked)
1208 if (checked)
1209 addSelectionTag(tag);
1210 else
1211 removeSelectionTag(tag);
1214 tagsMenu->addAction(action);
1217 actionAutoTMM->setCheckState(allSameAutoTMM
1218 ? (firstAutoTMM ? Qt::Checked : Qt::Unchecked)
1219 : Qt::PartiallyChecked);
1220 listMenu->addAction(actionAutoTMM);
1222 listMenu->addSeparator();
1223 listMenu->addAction(actionTorrentOptions);
1224 if (!oneNotFinished && oneHasMetadata)
1226 actionSuperSeedingMode->setCheckState(allSameSuperSeeding
1227 ? (superSeedingMode ? Qt::Checked : Qt::Unchecked)
1228 : Qt::PartiallyChecked);
1229 listMenu->addAction(actionSuperSeedingMode);
1231 listMenu->addSeparator();
1232 bool addedPreviewAction = false;
1233 if (needsPreview)
1235 listMenu->addAction(actionPreviewFile);
1236 addedPreviewAction = true;
1238 if (oneNotFinished)
1240 actionSequentialDownload->setCheckState(allSameSequentialDownloadMode
1241 ? (sequentialDownloadMode ? Qt::Checked : Qt::Unchecked)
1242 : Qt::PartiallyChecked);
1243 listMenu->addAction(actionSequentialDownload);
1245 actionFirstLastPiecePrio->setCheckState(allSamePrioFirstlast
1246 ? (prioritizeFirstLast ? Qt::Checked : Qt::Unchecked)
1247 : Qt::PartiallyChecked);
1248 listMenu->addAction(actionFirstLastPiecePrio);
1250 addedPreviewAction = true;
1253 if (addedPreviewAction)
1254 listMenu->addSeparator();
1255 if (oneHasMetadata)
1256 listMenu->addAction(actionForceRecheck);
1257 // We can not force reannounce torrents that are stopped/errored/checking/missing files/queued.
1258 // We may already have the tracker list from magnet url. So we can force reannounce torrents without metadata anyway.
1259 listMenu->addAction(actionForceReannounce);
1260 actionForceReannounce->setEnabled(oneCanForceReannounce);
1261 if (!oneCanForceReannounce)
1262 actionForceReannounce->setToolTip(tr("Can not force reannounce if torrent is Stopped/Queued/Errored/Checking"));
1263 listMenu->addSeparator();
1264 listMenu->addAction(actionOpenDestinationFolder);
1265 if (BitTorrent::Session::instance()->isQueueingSystemEnabled() && oneNotFinished)
1267 listMenu->addSeparator();
1268 QMenu *queueMenu = listMenu->addMenu(
1269 UIThemeManager::instance()->getIcon(u"queued"_s), tr("&Queue"));
1270 queueMenu->addAction(actionTopQueuePos);
1271 queueMenu->addAction(actionIncreaseQueuePos);
1272 queueMenu->addAction(actionDecreaseQueuePos);
1273 queueMenu->addAction(actionBottomQueuePos);
1276 QMenu *copySubMenu = listMenu->addMenu(UIThemeManager::instance()->getIcon(u"edit-copy"_s), tr("&Copy"));
1277 copySubMenu->addAction(actionCopyName);
1278 copySubMenu->addAction(actionCopyHash1);
1279 actionCopyHash1->setEnabled(hasInfohashV1);
1280 copySubMenu->addAction(actionCopyHash2);
1281 actionCopyHash2->setEnabled(hasInfohashV2);
1282 copySubMenu->addAction(actionCopyMagnetLink);
1283 copySubMenu->addAction(actionCopyID);
1284 copySubMenu->addAction(actionCopyComment);
1286 actionExportTorrent->setToolTip(tr("Exported torrent is not necessarily the same as the imported"));
1287 listMenu->addAction(actionExportTorrent);
1289 listMenu->popup(QCursor::pos());
1292 void TransferListWidget::currentChanged(const QModelIndex &current, const QModelIndex&)
1294 qDebug("CURRENT CHANGED");
1295 BitTorrent::Torrent *torrent = nullptr;
1296 if (current.isValid())
1298 torrent = m_listModel->torrentHandle(mapToSource(current));
1299 // Fix scrolling to the lowermost visible torrent
1300 QMetaObject::invokeMethod(this, [this, current] { scrollTo(current); }, Qt::QueuedConnection);
1302 emit currentTorrentChanged(torrent);
1305 void TransferListWidget::applyCategoryFilter(const QString &category)
1307 if (category.isNull())
1308 m_sortFilterModel->disableCategoryFilter();
1309 else
1310 m_sortFilterModel->setCategoryFilter(category);
1313 void TransferListWidget::applyTagFilter(const std::optional<Tag> &tag)
1315 if (!tag)
1316 m_sortFilterModel->disableTagFilter();
1317 else
1318 m_sortFilterModel->setTagFilter(*tag);
1321 void TransferListWidget::applyTrackerFilterAll()
1323 m_sortFilterModel->disableTrackerFilter();
1326 void TransferListWidget::applyTrackerFilter(const QSet<BitTorrent::TorrentID> &torrentIDs)
1328 m_sortFilterModel->setTrackerFilter(torrentIDs);
1331 void TransferListWidget::applyFilter(const QString &name, const TransferListModel::Column &type)
1333 m_sortFilterModel->setFilterKeyColumn(type);
1334 const QString pattern = (Preferences::instance()->getRegexAsFilteringPatternForTransferList()
1335 ? name : Utils::String::wildcardToRegexPattern(name));
1336 m_sortFilterModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption));
1339 void TransferListWidget::applyStatusFilter(const int filterIndex)
1341 const auto filterType = static_cast<TorrentFilter::Type>(filterIndex);
1342 m_sortFilterModel->setStatusFilter(((filterType >= TorrentFilter::All) && (filterType < TorrentFilter::_Count)) ? filterType : TorrentFilter::All);
1343 // Select first item if nothing is selected
1344 if (selectionModel()->selectedRows(0).empty() && (m_sortFilterModel->rowCount() > 0))
1346 qDebug("Nothing is selected, selecting first row: %s", qUtf8Printable(m_sortFilterModel->index(0, TransferListModel::TR_NAME).data().toString()));
1347 selectionModel()->setCurrentIndex(m_sortFilterModel->index(0, TransferListModel::TR_NAME), QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows);
1351 void TransferListWidget::saveSettings()
1353 Preferences::instance()->setTransHeaderState(header()->saveState());
1356 bool TransferListWidget::loadSettings()
1358 return header()->restoreState(Preferences::instance()->getTransHeaderState());
1361 void TransferListWidget::dragEnterEvent(QDragEnterEvent *event)
1363 if (const QMimeData *data = event->mimeData(); data->hasText() || data->hasUrls())
1365 event->setDropAction(Qt::CopyAction);
1366 event->accept();
1370 void TransferListWidget::dragMoveEvent(QDragMoveEvent *event)
1372 event->acceptProposedAction(); // required, otherwise we won't get `dropEvent`
1375 void TransferListWidget::dropEvent(QDropEvent *event)
1377 event->acceptProposedAction();
1378 // remove scheme
1379 QStringList files;
1380 if (const QMimeData *data = event->mimeData(); data->hasUrls())
1382 const QList<QUrl> urls = data->urls();
1383 files.reserve(urls.size());
1385 for (const QUrl &url : urls)
1387 if (url.isEmpty())
1388 continue;
1390 files.append(url.isLocalFile()
1391 ? url.toLocalFile()
1392 : url.toString());
1395 else
1397 files = data->text().split(u'\n', Qt::SkipEmptyParts);
1400 // differentiate ".torrent" files/links & magnet links from others
1401 QStringList torrentFiles, otherFiles;
1402 torrentFiles.reserve(files.size());
1403 otherFiles.reserve(files.size());
1404 for (const QString &file : asConst(files))
1406 if (Utils::Misc::isTorrentLink(file))
1407 torrentFiles << file;
1408 else
1409 otherFiles << file;
1412 // Download torrents
1413 if (!torrentFiles.isEmpty())
1415 for (const QString &file : asConst(torrentFiles))
1416 app()->addTorrentManager()->addTorrent(file);
1418 return;
1421 // Create torrent
1422 for (const QString &file : asConst(otherFiles))
1424 auto torrentCreator = new TorrentCreatorDialog(this, Path(file));
1425 torrentCreator->setAttribute(Qt::WA_DeleteOnClose);
1426 torrentCreator->show();
1428 // currently only handle the first entry
1429 // this is a stub that can be expanded later to create many torrents at once
1430 break;
1434 void TransferListWidget::wheelEvent(QWheelEvent *event)
1436 if (event->modifiers() & Qt::ShiftModifier)
1438 // Shift + scroll = horizontal scroll
1439 event->accept();
1440 QWheelEvent scrollHEvent {event->position(), event->globalPosition()
1441 , event->pixelDelta(), event->angleDelta().transposed(), event->buttons()
1442 , event->modifiers(), event->phase(), event->inverted(), event->source()};
1443 QTreeView::wheelEvent(&scrollHEvent);
1444 return;
1447 QTreeView::wheelEvent(event); // event delegated to base class