Fix handling of tags containing '&' character
[qBittorrent.git] / src / gui / transferlistwidget.cpp
blobb08e6b8b33aa195898898c45081e1ae947ded6a4
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);
189 //Ensure that at least one column is visible at all times
190 bool atLeastOne = false;
191 for (int i = 0; i < TransferListModel::NB_COLUMNS; ++i)
193 if (!isColumnHidden(i))
195 atLeastOne = true;
196 break;
199 if (!atLeastOne)
200 setColumnHidden(TransferListModel::TR_NAME, false);
202 //When adding/removing columns between versions some may
203 //end up being size 0 when the new version is launched with
204 //a conf file from the previous version.
205 for (int i = 0; i < TransferListModel::NB_COLUMNS; ++i)
207 if ((columnWidth(i) <= 0) && (!isColumnHidden(i)))
208 resizeColumnToContents(i);
211 setContextMenuPolicy(Qt::CustomContextMenu);
213 // Listen for list events
214 connect(this, &QAbstractItemView::doubleClicked, this, &TransferListWidget::torrentDoubleClicked);
215 connect(this, &QWidget::customContextMenuRequested, this, &TransferListWidget::displayListMenu);
216 header()->setContextMenuPolicy(Qt::CustomContextMenu);
217 connect(header(), &QWidget::customContextMenuRequested, this, &TransferListWidget::displayColumnHeaderMenu);
218 connect(header(), &QHeaderView::sectionMoved, this, &TransferListWidget::saveSettings);
219 connect(header(), &QHeaderView::sectionResized, this, &TransferListWidget::saveSettings);
220 connect(header(), &QHeaderView::sortIndicatorChanged, this, &TransferListWidget::saveSettings);
222 const auto *editHotkey = new QShortcut(Qt::Key_F2, this, nullptr, nullptr, Qt::WidgetShortcut);
223 connect(editHotkey, &QShortcut::activated, this, &TransferListWidget::renameSelectedTorrent);
224 const auto *deleteHotkey = new QShortcut(QKeySequence::Delete, this, nullptr, nullptr, Qt::WidgetShortcut);
225 connect(deleteHotkey, &QShortcut::activated, this, &TransferListWidget::softDeleteSelectedTorrents);
226 const auto *permDeleteHotkey = new QShortcut((Qt::SHIFT | Qt::Key_Delete), this, nullptr, nullptr, Qt::WidgetShortcut);
227 connect(permDeleteHotkey, &QShortcut::activated, this, &TransferListWidget::permDeleteSelectedTorrents);
228 const auto *doubleClickHotkeyReturn = new QShortcut(Qt::Key_Return, this, nullptr, nullptr, Qt::WidgetShortcut);
229 connect(doubleClickHotkeyReturn, &QShortcut::activated, this, &TransferListWidget::torrentDoubleClicked);
230 const auto *doubleClickHotkeyEnter = new QShortcut(Qt::Key_Enter, this, nullptr, nullptr, Qt::WidgetShortcut);
231 connect(doubleClickHotkeyEnter, &QShortcut::activated, this, &TransferListWidget::torrentDoubleClicked);
232 const auto *recheckHotkey = new QShortcut((Qt::CTRL | Qt::Key_R), this, nullptr, nullptr, Qt::WidgetShortcut);
233 connect(recheckHotkey, &QShortcut::activated, this, &TransferListWidget::recheckSelectedTorrents);
234 const auto *forceStartHotkey = new QShortcut((Qt::CTRL | Qt::Key_M), this, nullptr, nullptr, Qt::WidgetShortcut);
235 connect(forceStartHotkey, &QShortcut::activated, this, &TransferListWidget::forceStartSelectedTorrents);
238 TransferListWidget::~TransferListWidget()
240 // Save settings
241 saveSettings();
244 TransferListModel *TransferListWidget::getSourceModel() const
246 return m_listModel;
249 void TransferListWidget::previewFile(const Path &filePath)
251 Utils::Gui::openPath(filePath);
254 QModelIndex TransferListWidget::mapToSource(const QModelIndex &index) const
256 Q_ASSERT(index.isValid());
257 if (index.model() == m_sortFilterModel)
258 return m_sortFilterModel->mapToSource(index);
259 return index;
262 QModelIndexList TransferListWidget::mapToSource(const QModelIndexList &indexes) const
264 QModelIndexList result;
265 result.reserve(indexes.size());
266 for (const QModelIndex &index : indexes)
267 result.append(mapToSource(index));
269 return result;
272 QModelIndex TransferListWidget::mapFromSource(const QModelIndex &index) const
274 Q_ASSERT(index.isValid());
275 Q_ASSERT(index.model() == m_sortFilterModel);
276 return m_sortFilterModel->mapFromSource(index);
279 void TransferListWidget::torrentDoubleClicked()
281 const QModelIndexList selectedIndexes = selectionModel()->selectedRows();
282 if ((selectedIndexes.size() != 1) || !selectedIndexes.first().isValid())
283 return;
285 const QModelIndex index = m_listModel->index(mapToSource(selectedIndexes.first()).row());
286 BitTorrent::Torrent *const torrent = m_listModel->torrentHandle(index);
287 if (!torrent)
288 return;
290 int action;
291 if (torrent->isFinished())
292 action = Preferences::instance()->getActionOnDblClOnTorrentFn();
293 else
294 action = Preferences::instance()->getActionOnDblClOnTorrentDl();
296 switch (action)
298 case TOGGLE_STOP:
299 if (torrent->isStopped())
300 torrent->start();
301 else
302 torrent->stop();
303 break;
304 case PREVIEW_FILE:
305 if (torrentContainsPreviewableFiles(torrent))
307 auto *dialog = new PreviewSelectDialog(this, torrent);
308 dialog->setAttribute(Qt::WA_DeleteOnClose);
309 connect(dialog, &PreviewSelectDialog::readyToPreviewFile, this, &TransferListWidget::previewFile);
310 dialog->show();
312 else
314 openDestinationFolder(torrent);
316 break;
317 case OPEN_DEST:
318 openDestinationFolder(torrent);
319 break;
320 case SHOW_OPTIONS:
321 setTorrentOptions();
322 break;
326 QList<BitTorrent::Torrent *> TransferListWidget::getSelectedTorrents() const
328 const QModelIndexList selectedRows = selectionModel()->selectedRows();
330 QList<BitTorrent::Torrent *> torrents;
331 torrents.reserve(selectedRows.size());
332 for (const QModelIndex &index : selectedRows)
333 torrents << m_listModel->torrentHandle(mapToSource(index));
334 return torrents;
337 QList<BitTorrent::Torrent *> TransferListWidget::getVisibleTorrents() const
339 const int visibleTorrentsCount = m_sortFilterModel->rowCount();
341 QList<BitTorrent::Torrent *> torrents;
342 torrents.reserve(visibleTorrentsCount);
343 for (int i = 0; i < visibleTorrentsCount; ++i)
344 torrents << m_listModel->torrentHandle(mapToSource(m_sortFilterModel->index(i, 0)));
345 return torrents;
348 void TransferListWidget::setSelectedTorrentsLocation()
350 const QList<BitTorrent::Torrent *> torrents = getSelectedTorrents();
351 if (torrents.isEmpty())
352 return;
354 const Path oldLocation = torrents[0]->savePath();
356 auto *fileDialog = new QFileDialog(this, tr("Choose save path"), oldLocation.data());
357 fileDialog->setAttribute(Qt::WA_DeleteOnClose);
358 fileDialog->setFileMode(QFileDialog::Directory);
359 fileDialog->setOptions(QFileDialog::DontConfirmOverwrite | QFileDialog::ShowDirsOnly | QFileDialog::HideNameFilterDetails);
360 connect(fileDialog, &QDialog::accepted, this, [this, fileDialog]()
362 const QList<BitTorrent::Torrent *> torrents = getSelectedTorrents();
363 if (torrents.isEmpty())
364 return;
366 const Path newLocation {fileDialog->selectedFiles().constFirst()};
367 if (!newLocation.exists())
368 return;
370 // Actually move storage
371 for (BitTorrent::Torrent *const torrent : torrents)
373 torrent->setAutoTMMEnabled(false);
374 torrent->setSavePath(newLocation);
378 fileDialog->open();
381 void TransferListWidget::pauseSession()
383 BitTorrent::Session::instance()->pause();
386 void TransferListWidget::resumeSession()
388 BitTorrent::Session::instance()->resume();
391 void TransferListWidget::startSelectedTorrents()
393 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
394 torrent->start();
397 void TransferListWidget::forceStartSelectedTorrents()
399 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
400 torrent->start(BitTorrent::TorrentOperatingMode::Forced);
403 void TransferListWidget::startVisibleTorrents()
405 for (BitTorrent::Torrent *const torrent : asConst(getVisibleTorrents()))
406 torrent->start();
409 void TransferListWidget::stopSelectedTorrents()
411 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
412 torrent->stop();
415 void TransferListWidget::stopVisibleTorrents()
417 for (BitTorrent::Torrent *const torrent : asConst(getVisibleTorrents()))
418 torrent->stop();
421 void TransferListWidget::softDeleteSelectedTorrents()
423 deleteSelectedTorrents(false);
426 void TransferListWidget::permDeleteSelectedTorrents()
428 deleteSelectedTorrents(true);
431 void TransferListWidget::deleteSelectedTorrents(const bool deleteLocalFiles)
433 if (m_mainWindow->currentTabWidget() != this) return;
435 const QList<BitTorrent::Torrent *> torrents = getSelectedTorrents();
436 if (torrents.empty()) return;
438 if (Preferences::instance()->confirmTorrentDeletion())
440 auto *dialog = new DeletionConfirmationDialog(this, torrents.size(), torrents[0]->name(), deleteLocalFiles);
441 dialog->setAttribute(Qt::WA_DeleteOnClose);
442 connect(dialog, &DeletionConfirmationDialog::accepted, this, [this, dialog]()
444 // Some torrents might be removed when waiting for user input, so refetch the torrent list
445 // NOTE: this will only work when dialog is modal
446 removeTorrents(getSelectedTorrents(), dialog->isRemoveContentSelected());
448 dialog->open();
450 else
452 removeTorrents(torrents, deleteLocalFiles);
456 void TransferListWidget::deleteVisibleTorrents()
458 const QList<BitTorrent::Torrent *> torrents = getVisibleTorrents();
459 if (torrents.empty()) return;
461 if (Preferences::instance()->confirmTorrentDeletion())
463 auto *dialog = new DeletionConfirmationDialog(this, torrents.size(), torrents[0]->name(), false);
464 dialog->setAttribute(Qt::WA_DeleteOnClose);
465 connect(dialog, &DeletionConfirmationDialog::accepted, this, [this, dialog]()
467 // Some torrents might be removed when waiting for user input, so refetch the torrent list
468 // NOTE: this will only work when dialog is modal
469 removeTorrents(getVisibleTorrents(), dialog->isRemoveContentSelected());
471 dialog->open();
473 else
475 removeTorrents(torrents, false);
479 void TransferListWidget::increaseQueuePosSelectedTorrents()
481 qDebug() << Q_FUNC_INFO;
482 if (m_mainWindow->currentTabWidget() == this)
483 BitTorrent::Session::instance()->increaseTorrentsQueuePos(extractIDs(getSelectedTorrents()));
486 void TransferListWidget::decreaseQueuePosSelectedTorrents()
488 qDebug() << Q_FUNC_INFO;
489 if (m_mainWindow->currentTabWidget() == this)
490 BitTorrent::Session::instance()->decreaseTorrentsQueuePos(extractIDs(getSelectedTorrents()));
493 void TransferListWidget::topQueuePosSelectedTorrents()
495 if (m_mainWindow->currentTabWidget() == this)
496 BitTorrent::Session::instance()->topTorrentsQueuePos(extractIDs(getSelectedTorrents()));
499 void TransferListWidget::bottomQueuePosSelectedTorrents()
501 if (m_mainWindow->currentTabWidget() == this)
502 BitTorrent::Session::instance()->bottomTorrentsQueuePos(extractIDs(getSelectedTorrents()));
505 void TransferListWidget::copySelectedMagnetURIs() const
507 QStringList magnetUris;
508 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
509 magnetUris << torrent->createMagnetURI();
511 qApp->clipboard()->setText(magnetUris.join(u'\n'));
514 void TransferListWidget::copySelectedNames() const
516 QStringList torrentNames;
517 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
518 torrentNames << torrent->name();
520 qApp->clipboard()->setText(torrentNames.join(u'\n'));
523 void TransferListWidget::copySelectedInfohashes(const CopyInfohashPolicy policy) const
525 const auto selectedTorrents = getSelectedTorrents();
526 QStringList infoHashes;
527 infoHashes.reserve(selectedTorrents.size());
528 switch (policy)
530 case CopyInfohashPolicy::Version1:
531 for (const BitTorrent::Torrent *torrent : selectedTorrents)
533 if (const auto infoHash = torrent->infoHash().v1(); infoHash.isValid())
534 infoHashes << infoHash.toString();
536 break;
537 case CopyInfohashPolicy::Version2:
538 for (const BitTorrent::Torrent *torrent : selectedTorrents)
540 if (const auto infoHash = torrent->infoHash().v2(); infoHash.isValid())
541 infoHashes << infoHash.toString();
543 break;
546 qApp->clipboard()->setText(infoHashes.join(u'\n'));
549 void TransferListWidget::copySelectedIDs() const
551 QStringList torrentIDs;
552 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
553 torrentIDs << torrent->id().toString();
555 qApp->clipboard()->setText(torrentIDs.join(u'\n'));
558 void TransferListWidget::copySelectedComments() const
560 QStringList torrentComments;
561 for (const BitTorrent::Torrent *torrent : asConst(getSelectedTorrents()))
563 if (!torrent->comment().isEmpty())
564 torrentComments << torrent->comment();
567 qApp->clipboard()->setText(torrentComments.join(u"\n---------\n"_s));
570 void TransferListWidget::hideQueuePosColumn(bool hide)
572 setColumnHidden(TransferListModel::TR_QUEUE_POSITION, hide);
573 if (!hide && (columnWidth(TransferListModel::TR_QUEUE_POSITION) == 0))
574 resizeColumnToContents(TransferListModel::TR_QUEUE_POSITION);
577 void TransferListWidget::openSelectedTorrentsFolder() const
579 QSet<Path> paths;
580 #ifdef Q_OS_MACOS
581 // On macOS you expect both the files and folders to be opened in their parent
582 // folders prehilighted for opening, so we use a custom method.
583 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
585 const Path contentPath = torrent->contentPath();
586 paths.insert(!contentPath.isEmpty() ? contentPath : torrent->savePath());
588 MacUtils::openFiles(PathList(paths.cbegin(), paths.cend()));
589 #else
590 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
592 const Path contentPath = torrent->contentPath();
593 const Path openedPath = (!contentPath.isEmpty() ? contentPath : torrent->savePath());
594 if (!paths.contains(openedPath))
596 if (torrent->filesCount() == 1)
597 Utils::Gui::openFolderSelect(openedPath);
598 else
599 Utils::Gui::openPath(openedPath);
601 paths.insert(openedPath);
603 #endif // Q_OS_MACOS
606 void TransferListWidget::previewSelectedTorrents()
608 for (const BitTorrent::Torrent *torrent : asConst(getSelectedTorrents()))
610 if (torrentContainsPreviewableFiles(torrent))
612 auto *dialog = new PreviewSelectDialog(this, torrent);
613 dialog->setAttribute(Qt::WA_DeleteOnClose);
614 connect(dialog, &PreviewSelectDialog::readyToPreviewFile, this, &TransferListWidget::previewFile);
615 dialog->show();
617 else
619 QMessageBox::critical(this, tr("Unable to preview"), tr("The selected torrent \"%1\" does not contain previewable files")
620 .arg(torrent->name()));
625 void TransferListWidget::setTorrentOptions()
627 const QList<BitTorrent::Torrent *> selectedTorrents = getSelectedTorrents();
628 if (selectedTorrents.empty()) return;
630 auto *dialog = new TorrentOptionsDialog {this, selectedTorrents};
631 dialog->setAttribute(Qt::WA_DeleteOnClose);
632 dialog->open();
635 void TransferListWidget::recheckSelectedTorrents()
637 if (Preferences::instance()->confirmTorrentRecheck())
639 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);
640 if (ret != QMessageBox::Yes) return;
643 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
644 torrent->forceRecheck();
647 void TransferListWidget::reannounceSelectedTorrents()
649 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
650 torrent->forceReannounce();
653 int TransferListWidget::visibleColumnsCount() const
655 int count = 0;
656 for (int i = 0, iMax = header()->count(); i < iMax; ++i)
658 if (!isColumnHidden(i))
659 ++count;
662 return count;
665 // hide/show columns menu
666 void TransferListWidget::displayColumnHeaderMenu()
668 auto *menu = new QMenu(this);
669 menu->setAttribute(Qt::WA_DeleteOnClose);
670 menu->setTitle(tr("Column visibility"));
671 menu->setToolTipsVisible(true);
673 for (int i = 0; i < TransferListModel::NB_COLUMNS; ++i)
675 if (!BitTorrent::Session::instance()->isQueueingSystemEnabled() && (i == TransferListModel::TR_QUEUE_POSITION))
676 continue;
678 const auto columnName = m_listModel->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString();
679 const QVariant columnToolTip = m_listModel->headerData(i, Qt::Horizontal, Qt::ToolTipRole);
680 QAction *action = menu->addAction(columnName, this, [this, i](const bool checked)
682 if (!checked && (visibleColumnsCount() <= 1))
683 return;
685 setColumnHidden(i, !checked);
687 if (checked && (columnWidth(i) <= 5))
688 resizeColumnToContents(i);
690 saveSettings();
692 action->setCheckable(true);
693 action->setChecked(!isColumnHidden(i));
694 if (!columnToolTip.isNull())
695 action->setToolTip(columnToolTip.toString());
698 menu->addSeparator();
699 QAction *resizeAction = menu->addAction(tr("Resize columns"), this, [this]()
701 for (int i = 0, count = header()->count(); i < count; ++i)
703 if (!isColumnHidden(i))
704 resizeColumnToContents(i);
706 saveSettings();
708 resizeAction->setToolTip(tr("Resize all non-hidden columns to the size of their contents"));
710 menu->popup(QCursor::pos());
713 void TransferListWidget::setSelectedTorrentsSuperSeeding(const bool enabled) const
715 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
717 if (torrent->hasMetadata())
718 torrent->setSuperSeeding(enabled);
722 void TransferListWidget::setSelectedTorrentsSequentialDownload(const bool enabled) const
724 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
725 torrent->setSequentialDownload(enabled);
728 void TransferListWidget::setSelectedFirstLastPiecePrio(const bool enabled) const
730 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
731 torrent->setFirstLastPiecePriority(enabled);
734 void TransferListWidget::setSelectedAutoTMMEnabled(const bool enabled)
736 if (enabled)
738 const QMessageBox::StandardButton btn = QMessageBox::question(this, tr("Enable automatic torrent management")
739 , tr("Are you sure you want to enable Automatic Torrent Management for the selected torrent(s)? They may be relocated.")
740 , (QMessageBox::Yes | QMessageBox::No), QMessageBox::Yes);
741 if (btn != QMessageBox::Yes) return;
744 for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
745 torrent->setAutoTMMEnabled(enabled);
748 void TransferListWidget::askNewCategoryForSelection()
750 const QString newCategoryName = TorrentCategoryDialog::createCategory(this);
751 if (!newCategoryName.isEmpty())
752 setSelectionCategory(newCategoryName);
755 void TransferListWidget::askAddTagsForSelection()
757 const TagSet tags = askTagsForSelection(tr("Add Tags"));
758 for (const Tag &tag : tags)
759 addSelectionTag(tag);
762 void TransferListWidget::editTorrentTrackers()
764 const QList<BitTorrent::Torrent *> torrents = getSelectedTorrents();
765 QList<BitTorrent::TrackerEntry> commonTrackers;
767 if (!torrents.empty())
769 for (const BitTorrent::TrackerEntryStatus &status : asConst(torrents[0]->trackers()))
770 commonTrackers.append({.url = status.url, .tier = status.tier});
772 for (const BitTorrent::Torrent *torrent : torrents)
774 QSet<BitTorrent::TrackerEntry> trackerSet;
775 for (const BitTorrent::TrackerEntryStatus &status : asConst(torrent->trackers()))
776 trackerSet.insert({.url = status.url, .tier = status.tier});
778 commonTrackers.erase(std::remove_if(commonTrackers.begin(), commonTrackers.end()
779 , [&trackerSet](const BitTorrent::TrackerEntry &entry) { return !trackerSet.contains(entry); })
780 , commonTrackers.end());
784 auto *trackerDialog = new TrackerEntriesDialog(this);
785 trackerDialog->setAttribute(Qt::WA_DeleteOnClose);
786 trackerDialog->setTrackers(commonTrackers);
788 connect(trackerDialog, &QDialog::accepted, this, [torrents, trackerDialog]()
790 for (BitTorrent::Torrent *torrent : torrents)
791 torrent->replaceTrackers(trackerDialog->trackers());
794 trackerDialog->open();
797 void TransferListWidget::exportTorrent()
799 if (getSelectedTorrents().isEmpty())
800 return;
802 auto *fileDialog = new QFileDialog(this, tr("Choose folder to save exported .torrent files"));
803 fileDialog->setAttribute(Qt::WA_DeleteOnClose);
804 fileDialog->setFileMode(QFileDialog::Directory);
805 fileDialog->setOptions(QFileDialog::ShowDirsOnly);
806 connect(fileDialog, &QFileDialog::fileSelected, this, [this](const QString &dir)
808 const QList<BitTorrent::Torrent *> torrents = getSelectedTorrents();
809 if (torrents.isEmpty())
810 return;
812 const Path savePath {dir};
813 if (!savePath.exists())
814 return;
816 const QString errorMsg = tr("Export .torrent file failed. Torrent: \"%1\". Save path: \"%2\". Reason: \"%3\"");
818 bool hasError = false;
819 for (const BitTorrent::Torrent *torrent : torrents)
821 const QString validName = Utils::Fs::toValidFileName(torrent->name(), u"_"_s);
822 const Path filePath = savePath / Path(validName + u".torrent");
823 if (filePath.exists())
825 LogMsg(errorMsg.arg(torrent->name(), filePath.toString(), tr("A file with the same name already exists")) , Log::WARNING);
826 hasError = true;
827 continue;
830 const nonstd::expected<void, QString> result = torrent->exportToFile(filePath);
831 if (!result)
833 LogMsg(errorMsg.arg(torrent->name(), filePath.toString(), result.error()) , Log::WARNING);
834 hasError = true;
835 continue;
839 if (hasError)
841 QMessageBox::warning(this, tr("Export .torrent file error")
842 , tr("Errors occurred when exporting .torrent files. Check execution log for details."));
846 fileDialog->open();
849 void TransferListWidget::confirmRemoveAllTagsForSelection()
851 QMessageBox::StandardButton response = QMessageBox::question(
852 this, tr("Remove All Tags"), tr("Remove all tags from selected torrents?"),
853 QMessageBox::Yes | QMessageBox::No);
854 if (response == QMessageBox::Yes)
855 clearSelectionTags();
858 TagSet TransferListWidget::askTagsForSelection(const QString &dialogTitle)
860 TagSet tags;
861 bool invalid = true;
862 while (invalid)
864 bool ok = false;
865 invalid = false;
866 const QString tagsInput = AutoExpandableDialog::getText(
867 this, dialogTitle, tr("Comma-separated tags:"), QLineEdit::Normal, {}, &ok).trimmed();
868 if (!ok || tagsInput.isEmpty())
869 return {};
871 const QStringList tagStrings = tagsInput.split(u',', Qt::SkipEmptyParts);
872 tags.clear();
873 for (const QString &tagStr : tagStrings)
875 const Tag tag {tagStr};
876 if (!tag.isValid())
878 QMessageBox::warning(this, tr("Invalid tag"), tr("Tag name: '%1' is invalid").arg(tag.toString()));
879 invalid = true;
882 if (!invalid)
883 tags.insert(tag);
887 return tags;
890 void TransferListWidget::applyToSelectedTorrents(const std::function<void (BitTorrent::Torrent *const)> &fn)
892 // Changing the data may affect the layout of the sort/filter model, which in turn may invalidate
893 // the indexes previously obtained from selection model before we process them all.
894 // Therefore, we must map all the selected indexes to source before start processing them.
895 const QModelIndexList sourceRows = mapToSource(selectionModel()->selectedRows());
896 for (const QModelIndex &index : sourceRows)
898 BitTorrent::Torrent *const torrent = m_listModel->torrentHandle(index);
899 Q_ASSERT(torrent);
900 fn(torrent);
904 void TransferListWidget::renameSelectedTorrent()
906 const QModelIndexList selectedIndexes = selectionModel()->selectedRows();
907 if ((selectedIndexes.size() != 1) || !selectedIndexes.first().isValid())
908 return;
910 const QModelIndex mi = m_listModel->index(mapToSource(selectedIndexes.first()).row(), TransferListModel::TR_NAME);
911 BitTorrent::Torrent *const torrent = m_listModel->torrentHandle(mi);
912 if (!torrent)
913 return;
915 // Ask for a new Name
916 bool ok = false;
917 QString name = AutoExpandableDialog::getText(this, tr("Rename"), tr("New name:"), QLineEdit::Normal, torrent->name(), &ok);
918 if (ok && !name.isEmpty())
920 name.replace(QRegularExpression(u"\r?\n|\r"_s), u" "_s);
921 // Rename the torrent
922 m_listModel->setData(mi, name, Qt::DisplayRole);
926 void TransferListWidget::setSelectionCategory(const QString &category)
928 applyToSelectedTorrents([&category](BitTorrent::Torrent *torrent) { torrent->setCategory(category); });
931 void TransferListWidget::addSelectionTag(const Tag &tag)
933 applyToSelectedTorrents([&tag](BitTorrent::Torrent *const torrent) { torrent->addTag(tag); });
936 void TransferListWidget::removeSelectionTag(const Tag &tag)
938 applyToSelectedTorrents([&tag](BitTorrent::Torrent *const torrent) { torrent->removeTag(tag); });
941 void TransferListWidget::clearSelectionTags()
943 applyToSelectedTorrents([](BitTorrent::Torrent *const torrent) { torrent->removeAllTags(); });
946 void TransferListWidget::displayListMenu()
948 const QModelIndexList selectedIndexes = selectionModel()->selectedRows();
949 if (selectedIndexes.isEmpty())
950 return;
952 auto *listMenu = new QMenu(this);
953 listMenu->setAttribute(Qt::WA_DeleteOnClose);
954 listMenu->setToolTipsVisible(true);
956 // Create actions
958 auto *actionStart = new QAction(UIThemeManager::instance()->getIcon(u"torrent-start"_s, u"media-playback-start"_s), tr("&Start", "Resume/start the torrent"), listMenu);
959 connect(actionStart, &QAction::triggered, this, &TransferListWidget::startSelectedTorrents);
960 auto *actionStop = new QAction(UIThemeManager::instance()->getIcon(u"torrent-stop"_s, u"media-playback-pause"_s), tr("Sto&p", "Stop the torrent"), listMenu);
961 connect(actionStop, &QAction::triggered, this, &TransferListWidget::stopSelectedTorrents);
962 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);
963 connect(actionForceStart, &QAction::triggered, this, &TransferListWidget::forceStartSelectedTorrents);
964 auto *actionDelete = new QAction(UIThemeManager::instance()->getIcon(u"list-remove"_s), tr("&Remove", "Remove the torrent"), listMenu);
965 connect(actionDelete, &QAction::triggered, this, &TransferListWidget::softDeleteSelectedTorrents);
966 auto *actionPreviewFile = new QAction(UIThemeManager::instance()->getIcon(u"view-preview"_s), tr("Pre&view file..."), listMenu);
967 connect(actionPreviewFile, &QAction::triggered, this, &TransferListWidget::previewSelectedTorrents);
968 auto *actionTorrentOptions = new QAction(UIThemeManager::instance()->getIcon(u"configure"_s), tr("Torrent &options..."), listMenu);
969 connect(actionTorrentOptions, &QAction::triggered, this, &TransferListWidget::setTorrentOptions);
970 auto *actionOpenDestinationFolder = new QAction(UIThemeManager::instance()->getIcon(u"directory"_s), tr("Open destination &folder"), listMenu);
971 connect(actionOpenDestinationFolder, &QAction::triggered, this, &TransferListWidget::openSelectedTorrentsFolder);
972 auto *actionIncreaseQueuePos = new QAction(UIThemeManager::instance()->getIcon(u"go-up"_s), tr("Move &up", "i.e. move up in the queue"), listMenu);
973 connect(actionIncreaseQueuePos, &QAction::triggered, this, &TransferListWidget::increaseQueuePosSelectedTorrents);
974 auto *actionDecreaseQueuePos = new QAction(UIThemeManager::instance()->getIcon(u"go-down"_s), tr("Move &down", "i.e. Move down in the queue"), listMenu);
975 connect(actionDecreaseQueuePos, &QAction::triggered, this, &TransferListWidget::decreaseQueuePosSelectedTorrents);
976 auto *actionTopQueuePos = new QAction(UIThemeManager::instance()->getIcon(u"go-top"_s), tr("Move to &top", "i.e. Move to top of the queue"), listMenu);
977 connect(actionTopQueuePos, &QAction::triggered, this, &TransferListWidget::topQueuePosSelectedTorrents);
978 auto *actionBottomQueuePos = new QAction(UIThemeManager::instance()->getIcon(u"go-bottom"_s), tr("Move to &bottom", "i.e. Move to bottom of the queue"), listMenu);
979 connect(actionBottomQueuePos, &QAction::triggered, this, &TransferListWidget::bottomQueuePosSelectedTorrents);
980 auto *actionSetTorrentPath = new QAction(UIThemeManager::instance()->getIcon(u"set-location"_s, u"inode-directory"_s), tr("Set loc&ation..."), listMenu);
981 connect(actionSetTorrentPath, &QAction::triggered, this, &TransferListWidget::setSelectedTorrentsLocation);
982 auto *actionForceRecheck = new QAction(UIThemeManager::instance()->getIcon(u"force-recheck"_s, u"document-edit-verify"_s), tr("Force rec&heck"), listMenu);
983 connect(actionForceRecheck, &QAction::triggered, this, &TransferListWidget::recheckSelectedTorrents);
984 auto *actionForceReannounce = new QAction(UIThemeManager::instance()->getIcon(u"reannounce"_s, u"document-edit-verify"_s), tr("Force r&eannounce"), listMenu);
985 connect(actionForceReannounce, &QAction::triggered, this, &TransferListWidget::reannounceSelectedTorrents);
986 auto *actionCopyMagnetLink = new QAction(UIThemeManager::instance()->getIcon(u"torrent-magnet"_s, u"kt-magnet"_s), tr("&Magnet link"), listMenu);
987 connect(actionCopyMagnetLink, &QAction::triggered, this, &TransferListWidget::copySelectedMagnetURIs);
988 auto *actionCopyID = new QAction(UIThemeManager::instance()->getIcon(u"help-about"_s, u"edit-copy"_s), tr("Torrent &ID"), listMenu);
989 connect(actionCopyID, &QAction::triggered, this, &TransferListWidget::copySelectedIDs);
990 auto *actionCopyComment = new QAction(UIThemeManager::instance()->getIcon(u"edit-copy"_s), tr("&Comment"), listMenu);
991 connect(actionCopyComment, &QAction::triggered, this, &TransferListWidget::copySelectedComments);
992 auto *actionCopyName = new QAction(UIThemeManager::instance()->getIcon(u"name"_s, u"edit-copy"_s), tr("&Name"), listMenu);
993 connect(actionCopyName, &QAction::triggered, this, &TransferListWidget::copySelectedNames);
994 auto *actionCopyHash1 = new QAction(UIThemeManager::instance()->getIcon(u"hash"_s, u"edit-copy"_s), tr("Info &hash v1"), listMenu);
995 connect(actionCopyHash1, &QAction::triggered, this, [this]() { copySelectedInfohashes(CopyInfohashPolicy::Version1); });
996 auto *actionCopyHash2 = new QAction(UIThemeManager::instance()->getIcon(u"hash"_s, u"edit-copy"_s), tr("Info h&ash v2"), listMenu);
997 connect(actionCopyHash2, &QAction::triggered, this, [this]() { copySelectedInfohashes(CopyInfohashPolicy::Version2); });
998 auto *actionSuperSeedingMode = new TriStateAction(tr("Super seeding mode"), listMenu);
999 connect(actionSuperSeedingMode, &QAction::triggered, this, &TransferListWidget::setSelectedTorrentsSuperSeeding);
1000 auto *actionRename = new QAction(UIThemeManager::instance()->getIcon(u"edit-rename"_s), tr("Re&name..."), listMenu);
1001 connect(actionRename, &QAction::triggered, this, &TransferListWidget::renameSelectedTorrent);
1002 auto *actionSequentialDownload = new TriStateAction(tr("Download in sequential order"), listMenu);
1003 connect(actionSequentialDownload, &QAction::triggered, this, &TransferListWidget::setSelectedTorrentsSequentialDownload);
1004 auto *actionFirstLastPiecePrio = new TriStateAction(tr("Download first and last pieces first"), listMenu);
1005 connect(actionFirstLastPiecePrio, &QAction::triggered, this, &TransferListWidget::setSelectedFirstLastPiecePrio);
1006 auto *actionAutoTMM = new TriStateAction(tr("Automatic Torrent Management"), listMenu);
1007 actionAutoTMM->setToolTip(tr("Automatic mode means that various torrent properties (e.g. save path) will be decided by the associated category"));
1008 connect(actionAutoTMM, &QAction::triggered, this, &TransferListWidget::setSelectedAutoTMMEnabled);
1009 auto *actionEditTracker = new QAction(UIThemeManager::instance()->getIcon(u"edit-rename"_s), tr("Edit trac&kers..."), listMenu);
1010 connect(actionEditTracker, &QAction::triggered, this, &TransferListWidget::editTorrentTrackers);
1011 auto *actionExportTorrent = new QAction(UIThemeManager::instance()->getIcon(u"edit-copy"_s), tr("E&xport .torrent..."), listMenu);
1012 connect(actionExportTorrent, &QAction::triggered, this, &TransferListWidget::exportTorrent);
1013 // End of actions
1015 // Enable/disable stop/start action given the DL state
1016 bool needsStop = false, needsStart = false, needsForce = false, needsPreview = false;
1017 bool allSameSuperSeeding = true;
1018 bool superSeedingMode = false;
1019 bool allSameSequentialDownloadMode = true, allSamePrioFirstlast = true;
1020 bool sequentialDownloadMode = false, prioritizeFirstLast = false;
1021 bool oneHasMetadata = false, oneNotFinished = false;
1022 bool allSameCategory = true;
1023 bool allSameAutoTMM = true;
1024 bool firstAutoTMM = false;
1025 QString firstCategory;
1026 bool first = true;
1027 TagSet tagsInAny;
1028 TagSet tagsInAll;
1029 bool hasInfohashV1 = false, hasInfohashV2 = false;
1030 bool oneCanForceReannounce = false;
1032 for (const QModelIndex &index : selectedIndexes)
1034 const BitTorrent::Torrent *torrent = m_listModel->torrentHandle(mapToSource(index));
1035 if (!torrent)
1036 continue;
1038 if (firstCategory.isEmpty() && first)
1039 firstCategory = torrent->category();
1040 if (firstCategory != torrent->category())
1041 allSameCategory = false;
1043 const TagSet torrentTags = torrent->tags();
1044 tagsInAny.unite(torrentTags);
1046 if (first)
1048 firstAutoTMM = torrent->isAutoTMMEnabled();
1049 tagsInAll = torrentTags;
1051 else
1053 tagsInAll.intersect(torrentTags);
1056 if (firstAutoTMM != torrent->isAutoTMMEnabled())
1057 allSameAutoTMM = false;
1059 if (torrent->hasMetadata())
1060 oneHasMetadata = true;
1061 if (!torrent->isFinished())
1063 oneNotFinished = true;
1064 if (first)
1066 sequentialDownloadMode = torrent->isSequentialDownload();
1067 prioritizeFirstLast = torrent->hasFirstLastPiecePriority();
1069 else
1071 if (sequentialDownloadMode != torrent->isSequentialDownload())
1072 allSameSequentialDownloadMode = false;
1073 if (prioritizeFirstLast != torrent->hasFirstLastPiecePriority())
1074 allSamePrioFirstlast = false;
1077 else
1079 if (!oneNotFinished && allSameSuperSeeding && torrent->hasMetadata())
1081 if (first)
1082 superSeedingMode = torrent->superSeeding();
1083 else if (superSeedingMode != torrent->superSeeding())
1084 allSameSuperSeeding = false;
1088 if (!torrent->isForced())
1089 needsForce = true;
1090 else
1091 needsStart = true;
1093 const bool isStopped = torrent->isStopped();
1094 if (isStopped)
1095 needsStart = true;
1096 else
1097 needsStop = true;
1099 if (torrent->isErrored() || torrent->hasMissingFiles())
1101 // If torrent is in "errored" or "missing files" state
1102 // it cannot keep further processing until you restart it.
1103 needsStart = true;
1104 needsForce = true;
1107 if (torrent->hasMetadata())
1108 needsPreview = true;
1110 if (!hasInfohashV1 && torrent->infoHash().v1().isValid())
1111 hasInfohashV1 = true;
1112 if (!hasInfohashV2 && torrent->infoHash().v2().isValid())
1113 hasInfohashV2 = true;
1115 first = false;
1117 const bool rechecking = torrent->isChecking();
1118 if (rechecking)
1120 needsStart = true;
1121 needsStop = true;
1124 const bool queued = torrent->isQueued();
1125 if (!isStopped && !rechecking && !queued)
1126 oneCanForceReannounce = true;
1128 if (oneHasMetadata && oneNotFinished && !allSameSequentialDownloadMode
1129 && !allSamePrioFirstlast && !allSameSuperSeeding && !allSameCategory
1130 && needsStart && needsForce && needsStop && needsPreview && !allSameAutoTMM
1131 && hasInfohashV1 && hasInfohashV2 && oneCanForceReannounce)
1133 break;
1137 if (needsStart)
1138 listMenu->addAction(actionStart);
1139 if (needsStop)
1140 listMenu->addAction(actionStop);
1141 if (needsForce)
1142 listMenu->addAction(actionForceStart);
1143 listMenu->addSeparator();
1144 listMenu->addAction(actionDelete);
1145 listMenu->addSeparator();
1146 listMenu->addAction(actionSetTorrentPath);
1147 if (selectedIndexes.size() == 1)
1148 listMenu->addAction(actionRename);
1149 listMenu->addAction(actionEditTracker);
1151 // Category Menu
1152 QStringList categories = BitTorrent::Session::instance()->categories();
1153 std::sort(categories.begin(), categories.end(), Utils::Compare::NaturalLessThan<Qt::CaseInsensitive>());
1155 QMenu *categoryMenu = listMenu->addMenu(UIThemeManager::instance()->getIcon(u"view-categories"_s), tr("Categor&y"));
1157 categoryMenu->addAction(UIThemeManager::instance()->getIcon(u"list-add"_s), tr("&New...", "New category...")
1158 , this, &TransferListWidget::askNewCategoryForSelection);
1159 categoryMenu->addAction(UIThemeManager::instance()->getIcon(u"edit-clear"_s), tr("&Reset", "Reset category")
1160 , this, [this]() { setSelectionCategory(u""_s); });
1161 categoryMenu->addSeparator();
1163 for (const QString &category : asConst(categories))
1165 const QString escapedCategory = QString(category).replace(u'&', u"&&"_s); // avoid '&' becomes accelerator key
1166 QAction *categoryAction = categoryMenu->addAction(UIThemeManager::instance()->getIcon(u"view-categories"_s), escapedCategory
1167 , this, [this, category]() { setSelectionCategory(category); });
1169 if (allSameCategory && (category == firstCategory))
1171 categoryAction->setCheckable(true);
1172 categoryAction->setChecked(true);
1176 // Tag Menu
1177 QMenu *tagsMenu = listMenu->addMenu(UIThemeManager::instance()->getIcon(u"tags"_s, u"view-categories"_s), tr("Ta&gs"));
1179 tagsMenu->addAction(UIThemeManager::instance()->getIcon(u"list-add"_s), tr("&Add...", "Add / assign multiple tags...")
1180 , this, &TransferListWidget::askAddTagsForSelection);
1181 tagsMenu->addAction(UIThemeManager::instance()->getIcon(u"edit-clear"_s), tr("&Remove All", "Remove all tags")
1182 , this, [this]()
1184 if (Preferences::instance()->confirmRemoveAllTags())
1185 confirmRemoveAllTagsForSelection();
1186 else
1187 clearSelectionTags();
1189 tagsMenu->addSeparator();
1191 const TagSet tags = BitTorrent::Session::instance()->tags();
1192 for (const Tag &tag : asConst(tags))
1194 auto *action = new TriStateAction(Utils::Gui::tagToWidgetText(tag), tagsMenu);
1195 action->setCloseOnInteraction(false);
1197 const Qt::CheckState initialState = tagsInAll.contains(tag) ? Qt::Checked
1198 : tagsInAny.contains(tag) ? Qt::PartiallyChecked : Qt::Unchecked;
1199 action->setCheckState(initialState);
1201 connect(action, &QAction::toggled, this, [this, tag](const bool checked)
1203 if (checked)
1204 addSelectionTag(tag);
1205 else
1206 removeSelectionTag(tag);
1209 tagsMenu->addAction(action);
1212 actionAutoTMM->setCheckState(allSameAutoTMM
1213 ? (firstAutoTMM ? Qt::Checked : Qt::Unchecked)
1214 : Qt::PartiallyChecked);
1215 listMenu->addAction(actionAutoTMM);
1217 listMenu->addSeparator();
1218 listMenu->addAction(actionTorrentOptions);
1219 if (!oneNotFinished && oneHasMetadata)
1221 actionSuperSeedingMode->setCheckState(allSameSuperSeeding
1222 ? (superSeedingMode ? Qt::Checked : Qt::Unchecked)
1223 : Qt::PartiallyChecked);
1224 listMenu->addAction(actionSuperSeedingMode);
1226 listMenu->addSeparator();
1227 bool addedPreviewAction = false;
1228 if (needsPreview)
1230 listMenu->addAction(actionPreviewFile);
1231 addedPreviewAction = true;
1233 if (oneNotFinished)
1235 actionSequentialDownload->setCheckState(allSameSequentialDownloadMode
1236 ? (sequentialDownloadMode ? Qt::Checked : Qt::Unchecked)
1237 : Qt::PartiallyChecked);
1238 listMenu->addAction(actionSequentialDownload);
1240 actionFirstLastPiecePrio->setCheckState(allSamePrioFirstlast
1241 ? (prioritizeFirstLast ? Qt::Checked : Qt::Unchecked)
1242 : Qt::PartiallyChecked);
1243 listMenu->addAction(actionFirstLastPiecePrio);
1245 addedPreviewAction = true;
1248 if (addedPreviewAction)
1249 listMenu->addSeparator();
1250 if (oneHasMetadata)
1251 listMenu->addAction(actionForceRecheck);
1252 // We can not force reannounce torrents that are stopped/errored/checking/missing files/queued.
1253 // We may already have the tracker list from magnet url. So we can force reannounce torrents without metadata anyway.
1254 listMenu->addAction(actionForceReannounce);
1255 actionForceReannounce->setEnabled(oneCanForceReannounce);
1256 if (!oneCanForceReannounce)
1257 actionForceReannounce->setToolTip(tr("Can not force reannounce if torrent is Stopped/Queued/Errored/Checking"));
1258 listMenu->addSeparator();
1259 listMenu->addAction(actionOpenDestinationFolder);
1260 if (BitTorrent::Session::instance()->isQueueingSystemEnabled() && oneNotFinished)
1262 listMenu->addSeparator();
1263 QMenu *queueMenu = listMenu->addMenu(
1264 UIThemeManager::instance()->getIcon(u"queued"_s), tr("&Queue"));
1265 queueMenu->addAction(actionTopQueuePos);
1266 queueMenu->addAction(actionIncreaseQueuePos);
1267 queueMenu->addAction(actionDecreaseQueuePos);
1268 queueMenu->addAction(actionBottomQueuePos);
1271 QMenu *copySubMenu = listMenu->addMenu(UIThemeManager::instance()->getIcon(u"edit-copy"_s), tr("&Copy"));
1272 copySubMenu->addAction(actionCopyName);
1273 copySubMenu->addAction(actionCopyHash1);
1274 actionCopyHash1->setEnabled(hasInfohashV1);
1275 copySubMenu->addAction(actionCopyHash2);
1276 actionCopyHash2->setEnabled(hasInfohashV2);
1277 copySubMenu->addAction(actionCopyMagnetLink);
1278 copySubMenu->addAction(actionCopyID);
1279 copySubMenu->addAction(actionCopyComment);
1281 actionExportTorrent->setToolTip(tr("Exported torrent is not necessarily the same as the imported"));
1282 listMenu->addAction(actionExportTorrent);
1284 listMenu->popup(QCursor::pos());
1287 void TransferListWidget::currentChanged(const QModelIndex &current, const QModelIndex&)
1289 qDebug("CURRENT CHANGED");
1290 BitTorrent::Torrent *torrent = nullptr;
1291 if (current.isValid())
1293 torrent = m_listModel->torrentHandle(mapToSource(current));
1294 // Fix scrolling to the lowermost visible torrent
1295 QMetaObject::invokeMethod(this, [this, current] { scrollTo(current); }, Qt::QueuedConnection);
1297 emit currentTorrentChanged(torrent);
1300 void TransferListWidget::applyCategoryFilter(const QString &category)
1302 if (category.isNull())
1303 m_sortFilterModel->disableCategoryFilter();
1304 else
1305 m_sortFilterModel->setCategoryFilter(category);
1308 void TransferListWidget::applyTagFilter(const std::optional<Tag> &tag)
1310 if (!tag)
1311 m_sortFilterModel->disableTagFilter();
1312 else
1313 m_sortFilterModel->setTagFilter(*tag);
1316 void TransferListWidget::applyTrackerFilterAll()
1318 m_sortFilterModel->disableTrackerFilter();
1321 void TransferListWidget::applyTrackerFilter(const QSet<BitTorrent::TorrentID> &torrentIDs)
1323 m_sortFilterModel->setTrackerFilter(torrentIDs);
1326 void TransferListWidget::applyFilter(const QString &name, const TransferListModel::Column &type)
1328 m_sortFilterModel->setFilterKeyColumn(type);
1329 const QString pattern = (Preferences::instance()->getRegexAsFilteringPatternForTransferList()
1330 ? name : Utils::String::wildcardToRegexPattern(name));
1331 m_sortFilterModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption));
1334 void TransferListWidget::applyStatusFilter(const int filterIndex)
1336 const auto filterType = static_cast<TorrentFilter::Type>(filterIndex);
1337 m_sortFilterModel->setStatusFilter(((filterType >= TorrentFilter::All) && (filterType < TorrentFilter::_Count)) ? filterType : TorrentFilter::All);
1338 // Select first item if nothing is selected
1339 if (selectionModel()->selectedRows(0).empty() && (m_sortFilterModel->rowCount() > 0))
1341 qDebug("Nothing is selected, selecting first row: %s", qUtf8Printable(m_sortFilterModel->index(0, TransferListModel::TR_NAME).data().toString()));
1342 selectionModel()->setCurrentIndex(m_sortFilterModel->index(0, TransferListModel::TR_NAME), QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows);
1346 void TransferListWidget::saveSettings()
1348 Preferences::instance()->setTransHeaderState(header()->saveState());
1351 bool TransferListWidget::loadSettings()
1353 return header()->restoreState(Preferences::instance()->getTransHeaderState());
1356 void TransferListWidget::wheelEvent(QWheelEvent *event)
1358 if (event->modifiers() & Qt::ShiftModifier)
1360 // Shift + scroll = horizontal scroll
1361 event->accept();
1362 QWheelEvent scrollHEvent {event->position(), event->globalPosition()
1363 , event->pixelDelta(), event->angleDelta().transposed(), event->buttons()
1364 , event->modifiers(), event->phase(), event->inverted(), event->source()};
1365 QTreeView::wheelEvent(&scrollHEvent);
1366 return;
1369 QTreeView::wheelEvent(event); // event delegated to base class