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"
36 #include <QFileDialog>
37 #include <QHeaderView>
40 #include <QMessageBox>
42 #include <QRegularExpression>
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"
77 #include "macutilities.h"
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();
91 bool torrentContainsPreviewableFiles(const BitTorrent::Torrent
*const torrent
)
93 if (!torrent
->hasMetadata())
96 for (const Path
&filePath
: asConst(torrent
->filePaths()))
98 if (Utils::Misc::isPreviewable(filePath
))
105 void openDestinationFolder(const BitTorrent::Torrent
*const torrent
)
107 const Path contentPath
= torrent
->contentPath();
108 const Path openedPath
= (!contentPath
.isEmpty() ? contentPath
: torrent
->savePath());
110 MacUtils::openFiles({openedPath
});
112 if (torrent
->filesCount() == 1)
113 Utils::Gui::openFolderSelect(openedPath
);
115 Utils::Gui::openPath(openedPath
);
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}}
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
);
149 setUniformRowHeights(true);
150 setRootIsDecorated(false);
151 setAllColumnsShowFocus(true);
152 setSortingEnabled(true);
153 setSelectionMode(QAbstractItemView::ExtendedSelection
);
154 setItemsExpandable(false);
156 setAcceptDrops(true);
157 setDragDropMode(QAbstractItemView::DropOnly
);
158 setDropIndicatorShown(true);
159 #if defined(Q_OS_MACOS)
160 setAttribute(Qt::WA_MacShowFocusRect
, false);
162 header()->setFirstSectionMovable(true);
163 header()->setStretchLastSection(false);
164 header()->setTextElideMode(Qt::ElideRight
);
166 // Default hidden columns
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
))
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()
249 TransferListModel
*TransferListWidget::getSourceModel() const
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
);
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
));
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())
290 const QModelIndex index
= m_listModel
->index(mapToSource(selectedIndexes
.first()).row());
291 BitTorrent::Torrent
*const torrent
= m_listModel
->torrentHandle(index
);
296 if (torrent
->isFinished())
297 action
= Preferences::instance()->getActionOnDblClOnTorrentFn();
299 action
= Preferences::instance()->getActionOnDblClOnTorrentDl();
304 if (torrent
->isStopped())
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
);
319 openDestinationFolder(torrent
);
323 openDestinationFolder(torrent
);
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
));
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)));
353 void TransferListWidget::setSelectedTorrentsLocation()
355 const QList
<BitTorrent::Torrent
*> torrents
= getSelectedTorrents();
356 if (torrents
.isEmpty())
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())
371 const Path newLocation
{fileDialog
->selectedFiles().constFirst()};
372 if (!newLocation
.exists())
375 // Actually move storage
376 for (BitTorrent::Torrent
*const torrent
: torrents
)
378 torrent
->setAutoTMMEnabled(false);
379 torrent
->setSavePath(newLocation
);
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()))
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()))
414 void TransferListWidget::stopSelectedTorrents()
416 for (BitTorrent::Torrent
*const torrent
: asConst(getSelectedTorrents()))
420 void TransferListWidget::stopVisibleTorrents()
422 for (BitTorrent::Torrent
*const torrent
: asConst(getVisibleTorrents()))
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());
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());
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());
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();
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();
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
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()));
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
);
604 Utils::Gui::openPath(openedPath
);
606 paths
.insert(openedPath
);
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
);
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
);
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
661 for (int i
= 0, iMax
= header()->count(); i
< iMax
; ++i
)
663 if (!isColumnHidden(i
))
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
))
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))
690 setColumnHidden(i
, !checked
);
692 if (checked
&& (columnWidth(i
) <= 5))
693 resizeColumnToContents(i
);
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
);
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
)
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())
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())
817 const Path savePath
{dir
};
818 if (!savePath
.exists())
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
);
835 const nonstd::expected
<void, QString
> result
= torrent
->exportToFile(filePath
);
838 LogMsg(errorMsg
.arg(torrent
->name(), filePath
.toString(), result
.error()) , Log::WARNING
);
846 QMessageBox::warning(this, tr("Export .torrent file error")
847 , tr("Errors occurred when exporting .torrent files. Check execution log for details."));
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
)
871 const QString tagsInput
= AutoExpandableDialog::getText(
872 this, dialogTitle
, tr("Comma-separated tags:"), QLineEdit::Normal
, {}, &ok
).trimmed();
873 if (!ok
|| tagsInput
.isEmpty())
876 const QStringList tagStrings
= tagsInput
.split(u
',', Qt::SkipEmptyParts
);
878 for (const QString
&tagStr
: tagStrings
)
880 const Tag tag
{tagStr
};
883 QMessageBox::warning(this, tr("Invalid tag"), tr("Tag name: '%1' is invalid").arg(tag
.toString()));
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
);
909 void TransferListWidget::renameSelectedTorrent()
911 const QModelIndexList selectedIndexes
= selectionModel()->selectedRows();
912 if ((selectedIndexes
.size() != 1) || !selectedIndexes
.first().isValid())
915 const QModelIndex mi
= m_listModel
->index(mapToSource(selectedIndexes
.first()).row(), TransferListModel::TR_NAME
);
916 BitTorrent::Torrent
*const torrent
= m_listModel
->torrentHandle(mi
);
920 // Ask for a new Name
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())
957 auto *listMenu
= new QMenu(this);
958 listMenu
->setAttribute(Qt::WA_DeleteOnClose
);
959 listMenu
->setToolTipsVisible(true);
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
);
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
;
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
));
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
);
1053 firstAutoTMM
= torrent
->isAutoTMMEnabled();
1054 tagsInAll
= torrentTags
;
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;
1071 sequentialDownloadMode
= torrent
->isSequentialDownload();
1072 prioritizeFirstLast
= torrent
->hasFirstLastPiecePriority();
1076 if (sequentialDownloadMode
!= torrent
->isSequentialDownload())
1077 allSameSequentialDownloadMode
= false;
1078 if (prioritizeFirstLast
!= torrent
->hasFirstLastPiecePriority())
1079 allSamePrioFirstlast
= false;
1084 if (!oneNotFinished
&& allSameSuperSeeding
&& torrent
->hasMetadata())
1087 superSeedingMode
= torrent
->superSeeding();
1088 else if (superSeedingMode
!= torrent
->superSeeding())
1089 allSameSuperSeeding
= false;
1093 if (!torrent
->isForced())
1098 const bool isStopped
= torrent
->isStopped();
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.
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;
1122 const bool rechecking
= torrent
->isChecking();
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
)
1143 listMenu
->addAction(actionStart
);
1145 listMenu
->addAction(actionStop
);
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
);
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);
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")
1189 if (Preferences::instance()->confirmRemoveAllTags())
1190 confirmRemoveAllTagsForSelection();
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
)
1209 addSelectionTag(tag
);
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;
1235 listMenu
->addAction(actionPreviewFile
);
1236 addedPreviewAction
= true;
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();
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
¤t
, 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();
1310 m_sortFilterModel
->setCategoryFilter(category
);
1313 void TransferListWidget::applyTagFilter(const std::optional
<Tag
> &tag
)
1316 m_sortFilterModel
->disableTagFilter();
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
);
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();
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
)
1390 files
.append(url
.isLocalFile()
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
;
1412 // Download torrents
1413 if (!torrentFiles
.isEmpty())
1415 for (const QString
&file
: asConst(torrentFiles
))
1416 app()->addTorrentManager()->addTorrent(file
);
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
1434 void TransferListWidget::wheelEvent(QWheelEvent
*event
)
1436 if (event
->modifiers() & Qt::ShiftModifier
)
1438 // Shift + scroll = horizontal scroll
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
);
1447 QTreeView::wheelEvent(event
); // event delegated to base class