2 * Bittorrent Client using Qt and libtorrent.
3 * Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
5 * This program is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU General Public License
7 * as published by the Free Software Foundation; either version 2
8 * of the License, or (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 * In addition, as a special exception, the copyright holders give permission to
20 * link this program with the OpenSSL project's "OpenSSL" library (or with
21 * modified versions of it that use the same license as the "OpenSSL" library),
22 * and distribute the linked executables. You must obey the GNU General Public
23 * License in all respects for all of the code used other than "OpenSSL". If you
24 * modify file(s), you may extend this exception to your version of the file(s),
25 * but you are not obligated to do so. If you do not wish to do so, delete this
26 * exception statement from your version.
29 #include "transferlistwidget.h"
35 #include <QFileDialog>
36 #include <QHeaderView>
38 #include <QMessageBox>
39 #include <QRegularExpression>
43 #include <QWheelEvent>
45 #include "base/bittorrent/session.h"
46 #include "base/bittorrent/torrent.h"
47 #include "base/bittorrent/trackerentry.h"
48 #include "base/global.h"
49 #include "base/logger.h"
50 #include "base/path.h"
51 #include "base/preferences.h"
52 #include "base/torrentfilter.h"
53 #include "base/utils/compare.h"
54 #include "base/utils/fs.h"
55 #include "base/utils/misc.h"
56 #include "base/utils/string.h"
57 #include "autoexpandabledialog.h"
58 #include "deletionconfirmationdialog.h"
59 #include "mainwindow.h"
60 #include "optionsdialog.h"
61 #include "previewselectdialog.h"
62 #include "speedlimitdialog.h"
63 #include "torrentcategorydialog.h"
64 #include "torrentoptionsdialog.h"
65 #include "trackerentriesdialog.h"
66 #include "transferlistdelegate.h"
67 #include "transferlistsortmodel.h"
68 #include "tristateaction.h"
69 #include "uithememanager.h"
73 #include "macutilities.h"
78 QVector
<BitTorrent::TorrentID
> extractIDs(const QVector
<BitTorrent::Torrent
*> &torrents
)
80 QVector
<BitTorrent::TorrentID
> torrentIDs
;
81 torrentIDs
.reserve(torrents
.size());
82 for (const BitTorrent::Torrent
*torrent
: torrents
)
83 torrentIDs
<< torrent
->id();
87 bool torrentContainsPreviewableFiles(const BitTorrent::Torrent
*const torrent
)
89 if (!torrent
->hasMetadata())
92 for (const Path
&filePath
: asConst(torrent
->filePaths()))
94 if (Utils::Misc::isPreviewable(filePath
))
101 void openDestinationFolder(const BitTorrent::Torrent
*const torrent
)
104 MacUtils::openFiles({torrent
->contentPath()});
106 if (torrent
->filesCount() == 1)
107 Utils::Gui::openFolderSelect(torrent
->contentPath());
109 Utils::Gui::openPath(torrent
->contentPath());
113 void removeTorrents(const QVector
<BitTorrent::Torrent
*> &torrents
, const bool isDeleteFileSelected
)
115 auto *session
= BitTorrent::Session::instance();
116 const DeleteOption deleteOption
= isDeleteFileSelected
? DeleteTorrentAndFiles
: DeleteTorrent
;
117 for (const BitTorrent::Torrent
*torrent
: torrents
)
118 session
->deleteTorrent(torrent
->id(), deleteOption
);
122 TransferListWidget::TransferListWidget(QWidget
*parent
, MainWindow
*mainWindow
)
124 , m_listModel
{new TransferListModel
{this}}
125 , m_sortFilterModel
{new TransferListSortModel
{this}}
126 , m_mainWindow
{mainWindow
}
129 const bool columnLoaded
= loadSettings();
131 // Create and apply delegate
132 setItemDelegate(new TransferListDelegate
{this});
134 m_sortFilterModel
->setDynamicSortFilter(true);
135 m_sortFilterModel
->setSourceModel(m_listModel
);
136 m_sortFilterModel
->setFilterKeyColumn(TransferListModel::TR_NAME
);
137 m_sortFilterModel
->setFilterRole(Qt::DisplayRole
);
138 m_sortFilterModel
->setSortCaseSensitivity(Qt::CaseInsensitive
);
139 m_sortFilterModel
->setSortRole(TransferListModel::UnderlyingDataRole
);
140 setModel(m_sortFilterModel
);
143 setUniformRowHeights(true);
144 setRootIsDecorated(false);
145 setAllColumnsShowFocus(true);
146 setSortingEnabled(true);
147 setSelectionMode(QAbstractItemView::ExtendedSelection
);
148 setItemsExpandable(false);
150 setDragDropMode(QAbstractItemView::DragOnly
);
151 #if defined(Q_OS_MACOS)
152 setAttribute(Qt::WA_MacShowFocusRect
, false);
154 header()->setFirstSectionMovable(true);
155 header()->setStretchLastSection(false);
156 header()->setTextElideMode(Qt::ElideRight
);
158 // Default hidden columns
161 setColumnHidden(TransferListModel::TR_ADD_DATE
, true);
162 setColumnHidden(TransferListModel::TR_SEED_DATE
, true);
163 setColumnHidden(TransferListModel::TR_UPLIMIT
, true);
164 setColumnHidden(TransferListModel::TR_DLLIMIT
, true);
165 setColumnHidden(TransferListModel::TR_TRACKER
, true);
166 setColumnHidden(TransferListModel::TR_AMOUNT_DOWNLOADED
, true);
167 setColumnHidden(TransferListModel::TR_AMOUNT_UPLOADED
, true);
168 setColumnHidden(TransferListModel::TR_AMOUNT_DOWNLOADED_SESSION
, true);
169 setColumnHidden(TransferListModel::TR_AMOUNT_UPLOADED_SESSION
, true);
170 setColumnHidden(TransferListModel::TR_AMOUNT_LEFT
, true);
171 setColumnHidden(TransferListModel::TR_TIME_ELAPSED
, true);
172 setColumnHidden(TransferListModel::TR_SAVE_PATH
, true);
173 setColumnHidden(TransferListModel::TR_DOWNLOAD_PATH
, true);
174 setColumnHidden(TransferListModel::TR_INFOHASH_V1
, true);
175 setColumnHidden(TransferListModel::TR_INFOHASH_V2
, true);
176 setColumnHidden(TransferListModel::TR_COMPLETED
, true);
177 setColumnHidden(TransferListModel::TR_RATIO_LIMIT
, true);
178 setColumnHidden(TransferListModel::TR_SEEN_COMPLETE_DATE
, true);
179 setColumnHidden(TransferListModel::TR_LAST_ACTIVITY
, true);
180 setColumnHidden(TransferListModel::TR_TOTAL_SIZE
, true);
183 //Ensure that at least one column is visible at all times
184 bool atLeastOne
= false;
185 for (int i
= 0; i
< TransferListModel::NB_COLUMNS
; ++i
)
187 if (!isColumnHidden(i
))
194 setColumnHidden(TransferListModel::TR_NAME
, false);
196 //When adding/removing columns between versions some may
197 //end up being size 0 when the new version is launched with
198 //a conf file from the previous version.
199 for (int i
= 0; i
< TransferListModel::NB_COLUMNS
; ++i
)
201 if ((columnWidth(i
) <= 0) && (!isColumnHidden(i
)))
202 resizeColumnToContents(i
);
205 setContextMenuPolicy(Qt::CustomContextMenu
);
207 // Listen for list events
208 connect(this, &QAbstractItemView::doubleClicked
, this, &TransferListWidget::torrentDoubleClicked
);
209 connect(this, &QWidget::customContextMenuRequested
, this, &TransferListWidget::displayListMenu
);
210 header()->setContextMenuPolicy(Qt::CustomContextMenu
);
211 connect(header(), &QWidget::customContextMenuRequested
, this, &TransferListWidget::displayColumnHeaderMenu
);
212 connect(header(), &QHeaderView::sectionMoved
, this, &TransferListWidget::saveSettings
);
213 connect(header(), &QHeaderView::sectionResized
, this, &TransferListWidget::saveSettings
);
214 connect(header(), &QHeaderView::sortIndicatorChanged
, this, &TransferListWidget::saveSettings
);
216 const auto *editHotkey
= new QShortcut(Qt::Key_F2
, this, nullptr, nullptr, Qt::WidgetShortcut
);
217 connect(editHotkey
, &QShortcut::activated
, this, &TransferListWidget::renameSelectedTorrent
);
218 const auto *deleteHotkey
= new QShortcut(QKeySequence::Delete
, this, nullptr, nullptr, Qt::WidgetShortcut
);
219 connect(deleteHotkey
, &QShortcut::activated
, this, &TransferListWidget::softDeleteSelectedTorrents
);
220 const auto *permDeleteHotkey
= new QShortcut(Qt::SHIFT
+ Qt::Key_Delete
, this, nullptr, nullptr, Qt::WidgetShortcut
);
221 connect(permDeleteHotkey
, &QShortcut::activated
, this, &TransferListWidget::permDeleteSelectedTorrents
);
222 const auto *doubleClickHotkeyReturn
= new QShortcut(Qt::Key_Return
, this, nullptr, nullptr, Qt::WidgetShortcut
);
223 connect(doubleClickHotkeyReturn
, &QShortcut::activated
, this, &TransferListWidget::torrentDoubleClicked
);
224 const auto *doubleClickHotkeyEnter
= new QShortcut(Qt::Key_Enter
, this, nullptr, nullptr, Qt::WidgetShortcut
);
225 connect(doubleClickHotkeyEnter
, &QShortcut::activated
, this, &TransferListWidget::torrentDoubleClicked
);
226 const auto *recheckHotkey
= new QShortcut(Qt::CTRL
+ Qt::Key_R
, this, nullptr, nullptr, Qt::WidgetShortcut
);
227 connect(recheckHotkey
, &QShortcut::activated
, this, &TransferListWidget::recheckSelectedTorrents
);
228 const auto *forceStartHotkey
= new QShortcut(Qt::CTRL
+ Qt::Key_M
, this, nullptr, nullptr, Qt::WidgetShortcut
);
229 connect(forceStartHotkey
, &QShortcut::activated
, this, &TransferListWidget::forceStartSelectedTorrents
);
232 TransferListWidget::~TransferListWidget()
238 TransferListModel
*TransferListWidget::getSourceModel() const
243 void TransferListWidget::previewFile(const Path
&filePath
)
245 Utils::Gui::openPath(filePath
);
248 QModelIndex
TransferListWidget::mapToSource(const QModelIndex
&index
) const
250 Q_ASSERT(index
.isValid());
251 if (index
.model() == m_sortFilterModel
)
252 return m_sortFilterModel
->mapToSource(index
);
256 QModelIndex
TransferListWidget::mapFromSource(const QModelIndex
&index
) const
258 Q_ASSERT(index
.isValid());
259 Q_ASSERT(index
.model() == m_sortFilterModel
);
260 return m_sortFilterModel
->mapFromSource(index
);
263 void TransferListWidget::torrentDoubleClicked()
265 const QModelIndexList selectedIndexes
= selectionModel()->selectedRows();
266 if ((selectedIndexes
.size() != 1) || !selectedIndexes
.first().isValid()) return;
268 const QModelIndex index
= m_listModel
->index(mapToSource(selectedIndexes
.first()).row());
269 BitTorrent::Torrent
*const torrent
= m_listModel
->torrentHandle(index
);
270 if (!torrent
) return;
273 if (torrent
->isFinished())
274 action
= Preferences::instance()->getActionOnDblClOnTorrentFn();
276 action
= Preferences::instance()->getActionOnDblClOnTorrentDl();
281 if (torrent
->isPaused())
287 if (torrentContainsPreviewableFiles(torrent
))
289 auto *dialog
= new PreviewSelectDialog(this, torrent
);
290 dialog
->setAttribute(Qt::WA_DeleteOnClose
);
291 connect(dialog
, &PreviewSelectDialog::readyToPreviewFile
, this, &TransferListWidget::previewFile
);
296 openDestinationFolder(torrent
);
300 openDestinationFolder(torrent
);
308 QVector
<BitTorrent::Torrent
*> TransferListWidget::getSelectedTorrents() const
310 const QModelIndexList selectedRows
= selectionModel()->selectedRows();
312 QVector
<BitTorrent::Torrent
*> torrents
;
313 torrents
.reserve(selectedRows
.size());
314 for (const QModelIndex
&index
: selectedRows
)
315 torrents
<< m_listModel
->torrentHandle(mapToSource(index
));
319 QVector
<BitTorrent::Torrent
*> TransferListWidget::getVisibleTorrents() const
321 const int visibleTorrentsCount
= m_sortFilterModel
->rowCount();
323 QVector
<BitTorrent::Torrent
*> torrents
;
324 torrents
.reserve(visibleTorrentsCount
);
325 for (int i
= 0; i
< visibleTorrentsCount
; ++i
)
326 torrents
<< m_listModel
->torrentHandle(mapToSource(m_sortFilterModel
->index(i
, 0)));
330 void TransferListWidget::setSelectedTorrentsLocation()
332 const QVector
<BitTorrent::Torrent
*> torrents
= getSelectedTorrents();
333 if (torrents
.isEmpty())
336 const Path oldLocation
= torrents
[0]->savePath();
338 auto fileDialog
= new QFileDialog(this, tr("Choose save path"), oldLocation
.data());
339 fileDialog
->setAttribute(Qt::WA_DeleteOnClose
);
340 fileDialog
->setFileMode(QFileDialog::Directory
);
341 fileDialog
->setOptions(QFileDialog::DontConfirmOverwrite
| QFileDialog::ShowDirsOnly
| QFileDialog::HideNameFilterDetails
);
342 connect(fileDialog
, &QDialog::accepted
, this, [this, fileDialog
]()
344 const QVector
<BitTorrent::Torrent
*> torrents
= getSelectedTorrents();
345 if (torrents
.isEmpty())
348 const Path newLocation
{fileDialog
->selectedFiles().constFirst()};
349 if (!newLocation
.exists())
352 // Actually move storage
353 for (BitTorrent::Torrent
*const torrent
: torrents
)
355 torrent
->setAutoTMMEnabled(false);
356 torrent
->setSavePath(newLocation
);
363 void TransferListWidget::pauseAllTorrents()
365 // Show confirmation if user would really like to Pause All
366 const QMessageBox::StandardButton ret
=
367 QMessageBox::question(this, tr("Confirm pause")
368 , tr("Would you like to pause all torrents?")
369 , (QMessageBox::Yes
| QMessageBox::No
));
371 if (ret
!= QMessageBox::Yes
)
374 for (BitTorrent::Torrent
*const torrent
: asConst(BitTorrent::Session::instance()->torrents()))
378 void TransferListWidget::resumeAllTorrents()
380 // Show confirmation if user would really like to Resume All
381 const QMessageBox::StandardButton ret
=
382 QMessageBox::question(this, tr("Confirm resume")
383 , tr("Would you like to resume all torrents?")
384 , (QMessageBox::Yes
| QMessageBox::No
));
386 if (ret
!= QMessageBox::Yes
)
389 for (BitTorrent::Torrent
*const torrent
: asConst(BitTorrent::Session::instance()->torrents()))
393 void TransferListWidget::startSelectedTorrents()
395 for (BitTorrent::Torrent
*const torrent
: asConst(getSelectedTorrents()))
399 void TransferListWidget::forceStartSelectedTorrents()
401 for (BitTorrent::Torrent
*const torrent
: asConst(getSelectedTorrents()))
402 torrent
->resume(BitTorrent::TorrentOperatingMode::Forced
);
405 void TransferListWidget::startVisibleTorrents()
407 for (BitTorrent::Torrent
*const torrent
: asConst(getVisibleTorrents()))
411 void TransferListWidget::pauseSelectedTorrents()
413 for (BitTorrent::Torrent
*const torrent
: asConst(getSelectedTorrents()))
417 void TransferListWidget::pauseVisibleTorrents()
419 for (BitTorrent::Torrent
*const torrent
: asConst(getVisibleTorrents()))
423 void TransferListWidget::softDeleteSelectedTorrents()
425 deleteSelectedTorrents(false);
428 void TransferListWidget::permDeleteSelectedTorrents()
430 deleteSelectedTorrents(true);
433 void TransferListWidget::deleteSelectedTorrents(const bool deleteLocalFiles
)
435 if (m_mainWindow
->currentTabWidget() != this) return;
437 const QVector
<BitTorrent::Torrent
*> torrents
= getSelectedTorrents();
438 if (torrents
.empty()) return;
440 if (Preferences::instance()->confirmTorrentDeletion())
442 auto *dialog
= new DeletionConfirmationDialog(this, torrents
.size(), torrents
[0]->name(), deleteLocalFiles
);
443 dialog
->setAttribute(Qt::WA_DeleteOnClose
);
444 connect(dialog
, &DeletionConfirmationDialog::accepted
, this, [this, dialog
]()
446 // Some torrents might be removed when waiting for user input, so refetch the torrent list
447 // NOTE: this will only work when dialog is modal
448 removeTorrents(getSelectedTorrents(), dialog
->isDeleteFileSelected());
454 removeTorrents(torrents
, deleteLocalFiles
);
458 void TransferListWidget::deleteVisibleTorrents()
460 const QVector
<BitTorrent::Torrent
*> torrents
= getVisibleTorrents();
461 if (torrents
.empty()) return;
463 if (Preferences::instance()->confirmTorrentDeletion())
465 auto *dialog
= new DeletionConfirmationDialog(this, torrents
.size(), torrents
[0]->name(), false);
466 dialog
->setAttribute(Qt::WA_DeleteOnClose
);
467 connect(dialog
, &DeletionConfirmationDialog::accepted
, this, [this, dialog
]()
469 // Some torrents might be removed when waiting for user input, so refetch the torrent list
470 // NOTE: this will only work when dialog is modal
471 removeTorrents(getVisibleTorrents(), dialog
->isDeleteFileSelected());
477 removeTorrents(torrents
, false);
481 void TransferListWidget::increaseQueuePosSelectedTorrents()
483 qDebug() << Q_FUNC_INFO
;
484 if (m_mainWindow
->currentTabWidget() == this)
485 BitTorrent::Session::instance()->increaseTorrentsQueuePos(extractIDs(getSelectedTorrents()));
488 void TransferListWidget::decreaseQueuePosSelectedTorrents()
490 qDebug() << Q_FUNC_INFO
;
491 if (m_mainWindow
->currentTabWidget() == this)
492 BitTorrent::Session::instance()->decreaseTorrentsQueuePos(extractIDs(getSelectedTorrents()));
495 void TransferListWidget::topQueuePosSelectedTorrents()
497 if (m_mainWindow
->currentTabWidget() == this)
498 BitTorrent::Session::instance()->topTorrentsQueuePos(extractIDs(getSelectedTorrents()));
501 void TransferListWidget::bottomQueuePosSelectedTorrents()
503 if (m_mainWindow
->currentTabWidget() == this)
504 BitTorrent::Session::instance()->bottomTorrentsQueuePos(extractIDs(getSelectedTorrents()));
507 void TransferListWidget::copySelectedMagnetURIs() const
509 QStringList magnetUris
;
510 for (BitTorrent::Torrent
*const torrent
: asConst(getSelectedTorrents()))
511 magnetUris
<< torrent
->createMagnetURI();
513 qApp
->clipboard()->setText(magnetUris
.join(u
'\n'));
516 void TransferListWidget::copySelectedNames() const
518 QStringList torrentNames
;
519 for (BitTorrent::Torrent
*const torrent
: asConst(getSelectedTorrents()))
520 torrentNames
<< torrent
->name();
522 qApp
->clipboard()->setText(torrentNames
.join(u
'\n'));
525 void TransferListWidget::copySelectedInfohashes(const CopyInfohashPolicy policy
) const
527 const auto selectedTorrents
= getSelectedTorrents();
528 QStringList infoHashes
;
529 infoHashes
.reserve(selectedTorrents
.size());
532 case CopyInfohashPolicy::Version1
:
533 for (const BitTorrent::Torrent
*torrent
: selectedTorrents
)
535 if (const auto infoHash
= torrent
->infoHash().v1(); infoHash
.isValid())
536 infoHashes
<< infoHash
.toString();
539 case CopyInfohashPolicy::Version2
:
540 for (const BitTorrent::Torrent
*torrent
: selectedTorrents
)
542 if (const auto infoHash
= torrent
->infoHash().v2(); infoHash
.isValid())
543 infoHashes
<< infoHash
.toString();
548 qApp
->clipboard()->setText(infoHashes
.join(u
'\n'));
551 void TransferListWidget::copySelectedIDs() const
553 QStringList torrentIDs
;
554 for (BitTorrent::Torrent
*const torrent
: asConst(getSelectedTorrents()))
555 torrentIDs
<< torrent
->id().toString();
557 qApp
->clipboard()->setText(torrentIDs
.join(u
'\n'));
560 void TransferListWidget::hideQueuePosColumn(bool hide
)
562 setColumnHidden(TransferListModel::TR_QUEUE_POSITION
, hide
);
563 if (!hide
&& (columnWidth(TransferListModel::TR_QUEUE_POSITION
) == 0))
564 resizeColumnToContents(TransferListModel::TR_QUEUE_POSITION
);
567 void TransferListWidget::openSelectedTorrentsFolder() const
571 // On macOS you expect both the files and folders to be opened in their parent
572 // folders prehilighted for opening, so we use a custom method.
573 for (BitTorrent::Torrent
*const torrent
: asConst(getSelectedTorrents()))
575 const Path contentPath
= torrent
->contentPath();
576 paths
.insert(contentPath
);
578 MacUtils::openFiles(PathList(paths
.cbegin(), paths
.cend()));
580 for (BitTorrent::Torrent
*const torrent
: asConst(getSelectedTorrents()))
582 const Path contentPath
= torrent
->contentPath();
583 if (!paths
.contains(contentPath
))
585 if (torrent
->filesCount() == 1)
586 Utils::Gui::openFolderSelect(contentPath
);
588 Utils::Gui::openPath(contentPath
);
590 paths
.insert(contentPath
);
595 void TransferListWidget::previewSelectedTorrents()
597 for (const BitTorrent::Torrent
*torrent
: asConst(getSelectedTorrents()))
599 if (torrentContainsPreviewableFiles(torrent
))
601 auto *dialog
= new PreviewSelectDialog(this, torrent
);
602 dialog
->setAttribute(Qt::WA_DeleteOnClose
);
603 connect(dialog
, &PreviewSelectDialog::readyToPreviewFile
, this, &TransferListWidget::previewFile
);
608 QMessageBox::critical(this, tr("Unable to preview"), tr("The selected torrent \"%1\" does not contain previewable files")
609 .arg(torrent
->name()));
614 void TransferListWidget::setTorrentOptions()
616 const QVector
<BitTorrent::Torrent
*> selectedTorrents
= getSelectedTorrents();
617 if (selectedTorrents
.empty()) return;
619 auto dialog
= new TorrentOptionsDialog
{this, selectedTorrents
};
620 dialog
->setAttribute(Qt::WA_DeleteOnClose
);
624 void TransferListWidget::recheckSelectedTorrents()
626 if (Preferences::instance()->confirmTorrentRecheck())
628 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
);
629 if (ret
!= QMessageBox::Yes
) return;
632 for (BitTorrent::Torrent
*const torrent
: asConst(getSelectedTorrents()))
633 torrent
->forceRecheck();
636 void TransferListWidget::reannounceSelectedTorrents()
638 for (BitTorrent::Torrent
*const torrent
: asConst(getSelectedTorrents()))
639 torrent
->forceReannounce();
642 int TransferListWidget::visibleColumnsCount() const
645 for (int i
= 0, iMax
= header()->count(); i
< iMax
; ++i
)
647 if (!isColumnHidden(i
))
654 // hide/show columns menu
655 void TransferListWidget::displayColumnHeaderMenu()
657 auto menu
= new QMenu(this);
658 menu
->setAttribute(Qt::WA_DeleteOnClose
);
659 menu
->setTitle(tr("Column visibility"));
660 menu
->setToolTipsVisible(true);
662 for (int i
= 0; i
< TransferListModel::NB_COLUMNS
; ++i
)
664 if (!BitTorrent::Session::instance()->isQueueingSystemEnabled() && (i
== TransferListModel::TR_QUEUE_POSITION
))
667 const auto columnName
= m_listModel
->headerData(i
, Qt::Horizontal
, Qt::DisplayRole
).toString();
668 QAction
*action
= menu
->addAction(columnName
, this, [this, i
](const bool checked
)
670 if (!checked
&& (visibleColumnsCount() <= 1))
673 setColumnHidden(i
, !checked
);
675 if (checked
&& (columnWidth(i
) <= 5))
676 resizeColumnToContents(i
);
680 action
->setCheckable(true);
681 action
->setChecked(!isColumnHidden(i
));
684 menu
->addSeparator();
685 QAction
*resizeAction
= menu
->addAction(tr("Resize columns"), this, [this]()
687 for (int i
= 0, count
= header()->count(); i
< count
; ++i
)
689 if (!isColumnHidden(i
))
690 resizeColumnToContents(i
);
694 resizeAction
->setToolTip(tr("Resize all non-hidden columns to the size of their contents"));
696 menu
->popup(QCursor::pos());
699 void TransferListWidget::setSelectedTorrentsSuperSeeding(const bool enabled
) const
701 for (BitTorrent::Torrent
*const torrent
: asConst(getSelectedTorrents()))
703 if (torrent
->hasMetadata())
704 torrent
->setSuperSeeding(enabled
);
708 void TransferListWidget::setSelectedTorrentsSequentialDownload(const bool enabled
) const
710 for (BitTorrent::Torrent
*const torrent
: asConst(getSelectedTorrents()))
711 torrent
->setSequentialDownload(enabled
);
714 void TransferListWidget::setSelectedFirstLastPiecePrio(const bool enabled
) const
716 for (BitTorrent::Torrent
*const torrent
: asConst(getSelectedTorrents()))
717 torrent
->setFirstLastPiecePriority(enabled
);
720 void TransferListWidget::setSelectedAutoTMMEnabled(const bool enabled
)
724 const QMessageBox::StandardButton btn
= QMessageBox::question(this, tr("Enable automatic torrent management")
725 , tr("Are you sure you want to enable Automatic Torrent Management for the selected torrent(s)? They may be relocated.")
726 , (QMessageBox::Yes
| QMessageBox::No
), QMessageBox::Yes
);
727 if (btn
!= QMessageBox::Yes
) return;
730 for (BitTorrent::Torrent
*const torrent
: asConst(getSelectedTorrents()))
731 torrent
->setAutoTMMEnabled(enabled
);
734 void TransferListWidget::askNewCategoryForSelection()
736 const QString newCategoryName
= TorrentCategoryDialog::createCategory(this);
737 if (!newCategoryName
.isEmpty())
738 setSelectionCategory(newCategoryName
);
741 void TransferListWidget::askAddTagsForSelection()
743 const QStringList tags
= askTagsForSelection(tr("Add Tags"));
744 for (const QString
&tag
: tags
)
745 addSelectionTag(tag
);
748 void TransferListWidget::editTorrentTrackers()
750 const QVector
<BitTorrent::Torrent
*> torrents
= getSelectedTorrents();
751 QVector
<BitTorrent::TrackerEntry
> commonTrackers
;
753 if (!torrents
.empty())
755 commonTrackers
= torrents
[0]->trackers();
757 for (const BitTorrent::Torrent
*torrent
: torrents
)
759 QSet
<BitTorrent::TrackerEntry
> trackerSet
;
761 for (const BitTorrent::TrackerEntry
&entry
: asConst(torrent
->trackers()))
762 trackerSet
.insert(entry
);
764 commonTrackers
.erase(std::remove_if(commonTrackers
.begin(), commonTrackers
.end()
765 , [&trackerSet
](const BitTorrent::TrackerEntry
&entry
) { return !trackerSet
.contains(entry
); })
766 , commonTrackers
.end());
770 auto trackerDialog
= new TrackerEntriesDialog(this);
771 trackerDialog
->setAttribute(Qt::WA_DeleteOnClose
);
772 trackerDialog
->setTrackers(commonTrackers
);
774 connect(trackerDialog
, &QDialog::accepted
, this, [torrents
, trackerDialog
]()
776 for (BitTorrent::Torrent
*torrent
: torrents
)
777 torrent
->replaceTrackers(trackerDialog
->trackers());
780 trackerDialog
->open();
783 void TransferListWidget::exportTorrent()
785 if (getSelectedTorrents().isEmpty())
788 auto fileDialog
= new QFileDialog(this, tr("Choose folder to save exported .torrent files"));
789 fileDialog
->setAttribute(Qt::WA_DeleteOnClose
);
790 fileDialog
->setFileMode(QFileDialog::Directory
);
791 fileDialog
->setOptions(QFileDialog::ShowDirsOnly
);
792 connect(fileDialog
, &QFileDialog::fileSelected
, this, [this](const QString
&dir
)
794 const QVector
<BitTorrent::Torrent
*> torrents
= getSelectedTorrents();
795 if (torrents
.isEmpty())
798 const Path savePath
{dir
};
799 if (!savePath
.exists())
802 const QString errorMsg
= tr("Export .torrent file failed. Torrent: \"%1\". Save path: \"%2\". Reason: \"%3\"");
804 bool hasError
= false;
805 for (const BitTorrent::Torrent
*torrent
: torrents
)
807 const Path filePath
= savePath
/ Path(torrent
->name() + u
".torrent");
808 if (filePath
.exists())
810 LogMsg(errorMsg
.arg(torrent
->name(), filePath
.toString(), tr("A file with the same name already exists")) , Log::WARNING
);
815 const nonstd::expected
<void, QString
> result
= torrent
->exportToFile(filePath
);
818 LogMsg(errorMsg
.arg(torrent
->name(), filePath
.toString(), result
.error()) , Log::WARNING
);
826 QMessageBox::warning(this, tr("Export .torrent file error")
827 , tr("Errors occurred when exporting .torrent files. Check execution log for details."));
834 void TransferListWidget::confirmRemoveAllTagsForSelection()
836 QMessageBox::StandardButton response
= QMessageBox::question(
837 this, tr("Remove All Tags"), tr("Remove all tags from selected torrents?"),
838 QMessageBox::Yes
| QMessageBox::No
);
839 if (response
== QMessageBox::Yes
)
840 clearSelectionTags();
843 QStringList
TransferListWidget::askTagsForSelection(const QString
&dialogTitle
)
851 const QString tagsInput
= AutoExpandableDialog::getText(
852 this, dialogTitle
, tr("Comma-separated tags:"), QLineEdit::Normal
, {}, &ok
).trimmed();
853 if (!ok
|| tagsInput
.isEmpty())
855 tags
= tagsInput
.split(u
',', Qt::SkipEmptyParts
);
856 for (QString
&tag
: tags
)
859 if (!BitTorrent::Session::isValidTag(tag
))
861 QMessageBox::warning(this, tr("Invalid tag")
862 , tr("Tag name: '%1' is invalid").arg(tag
));
870 void TransferListWidget::applyToSelectedTorrents(const std::function
<void (BitTorrent::Torrent
*const)> &fn
)
872 for (const QModelIndex
&index
: asConst(selectionModel()->selectedRows()))
874 BitTorrent::Torrent
*const torrent
= m_listModel
->torrentHandle(mapToSource(index
));
880 void TransferListWidget::renameSelectedTorrent()
882 const QModelIndexList selectedIndexes
= selectionModel()->selectedRows();
883 if ((selectedIndexes
.size() != 1) || !selectedIndexes
.first().isValid()) return;
885 const QModelIndex mi
= m_listModel
->index(mapToSource(selectedIndexes
.first()).row(), TransferListModel::TR_NAME
);
886 BitTorrent::Torrent
*const torrent
= m_listModel
->torrentHandle(mi
);
887 if (!torrent
) return;
889 // Ask for a new Name
891 QString name
= AutoExpandableDialog::getText(this, tr("Rename"), tr("New name:"), QLineEdit::Normal
, torrent
->name(), &ok
);
892 if (ok
&& !name
.isEmpty())
894 name
.replace(QRegularExpression(u
"\r?\n|\r"_qs
), u
" "_qs
);
895 // Rename the torrent
896 m_listModel
->setData(mi
, name
, Qt::DisplayRole
);
900 void TransferListWidget::setSelectionCategory(const QString
&category
)
902 for (const QModelIndex
&index
: asConst(selectionModel()->selectedRows()))
903 m_listModel
->setData(m_listModel
->index(mapToSource(index
).row(), TransferListModel::TR_CATEGORY
), category
, Qt::DisplayRole
);
906 void TransferListWidget::addSelectionTag(const QString
&tag
)
908 applyToSelectedTorrents([&tag
](BitTorrent::Torrent
*const torrent
) { torrent
->addTag(tag
); });
911 void TransferListWidget::removeSelectionTag(const QString
&tag
)
913 applyToSelectedTorrents([&tag
](BitTorrent::Torrent
*const torrent
) { torrent
->removeTag(tag
); });
916 void TransferListWidget::clearSelectionTags()
918 applyToSelectedTorrents([](BitTorrent::Torrent
*const torrent
) { torrent
->removeAllTags(); });
921 void TransferListWidget::displayListMenu()
923 const QModelIndexList selectedIndexes
= selectionModel()->selectedRows();
924 if (selectedIndexes
.isEmpty()) return;
926 auto *listMenu
= new QMenu(this);
927 listMenu
->setAttribute(Qt::WA_DeleteOnClose
);
928 listMenu
->setToolTipsVisible(true);
932 auto *actionStart
= new QAction(UIThemeManager::instance()->getIcon(u
"torrent-start"_qs
, u
"media-playback-start"_qs
), tr("&Resume", "Resume/start the torrent"), listMenu
);
933 connect(actionStart
, &QAction::triggered
, this, &TransferListWidget::startSelectedTorrents
);
934 auto *actionPause
= new QAction(UIThemeManager::instance()->getIcon(u
"torrent-stop"_qs
, u
"media-playback-pause"_qs
), tr("&Pause", "Pause the torrent"), listMenu
);
935 connect(actionPause
, &QAction::triggered
, this, &TransferListWidget::pauseSelectedTorrents
);
936 auto *actionForceStart
= new QAction(UIThemeManager::instance()->getIcon(u
"torrent-start-forced"_qs
, u
"media-playback-start"_qs
), tr("Force Resu&me", "Force Resume/start the torrent"), listMenu
);
937 connect(actionForceStart
, &QAction::triggered
, this, &TransferListWidget::forceStartSelectedTorrents
);
938 auto *actionDelete
= new QAction(UIThemeManager::instance()->getIcon(u
"list-remove"_qs
), tr("&Remove", "Remove the torrent"), listMenu
);
939 connect(actionDelete
, &QAction::triggered
, this, &TransferListWidget::softDeleteSelectedTorrents
);
940 auto *actionPreviewFile
= new QAction(UIThemeManager::instance()->getIcon(u
"view-preview"_qs
), tr("Pre&view file..."), listMenu
);
941 connect(actionPreviewFile
, &QAction::triggered
, this, &TransferListWidget::previewSelectedTorrents
);
942 auto *actionTorrentOptions
= new QAction(UIThemeManager::instance()->getIcon(u
"configure"_qs
), tr("Torrent &options..."), listMenu
);
943 connect(actionTorrentOptions
, &QAction::triggered
, this, &TransferListWidget::setTorrentOptions
);
944 auto *actionOpenDestinationFolder
= new QAction(UIThemeManager::instance()->getIcon(u
"directory"_qs
), tr("Open destination &folder"), listMenu
);
945 connect(actionOpenDestinationFolder
, &QAction::triggered
, this, &TransferListWidget::openSelectedTorrentsFolder
);
946 auto *actionIncreaseQueuePos
= new QAction(UIThemeManager::instance()->getIcon(u
"go-up"_qs
), tr("Move &up", "i.e. move up in the queue"), listMenu
);
947 connect(actionIncreaseQueuePos
, &QAction::triggered
, this, &TransferListWidget::increaseQueuePosSelectedTorrents
);
948 auto *actionDecreaseQueuePos
= new QAction(UIThemeManager::instance()->getIcon(u
"go-down"_qs
), tr("Move &down", "i.e. Move down in the queue"), listMenu
);
949 connect(actionDecreaseQueuePos
, &QAction::triggered
, this, &TransferListWidget::decreaseQueuePosSelectedTorrents
);
950 auto *actionTopQueuePos
= new QAction(UIThemeManager::instance()->getIcon(u
"go-top"_qs
), tr("Move to &top", "i.e. Move to top of the queue"), listMenu
);
951 connect(actionTopQueuePos
, &QAction::triggered
, this, &TransferListWidget::topQueuePosSelectedTorrents
);
952 auto *actionBottomQueuePos
= new QAction(UIThemeManager::instance()->getIcon(u
"go-bottom"_qs
), tr("Move to &bottom", "i.e. Move to bottom of the queue"), listMenu
);
953 connect(actionBottomQueuePos
, &QAction::triggered
, this, &TransferListWidget::bottomQueuePosSelectedTorrents
);
954 auto *actionSetTorrentPath
= new QAction(UIThemeManager::instance()->getIcon(u
"set-location"_qs
, u
"inode-directory"_qs
), tr("Set loc&ation..."), listMenu
);
955 connect(actionSetTorrentPath
, &QAction::triggered
, this, &TransferListWidget::setSelectedTorrentsLocation
);
956 auto *actionForceRecheck
= new QAction(UIThemeManager::instance()->getIcon(u
"force-recheck"_qs
, u
"document-edit-verify"_qs
), tr("Force rec&heck"), listMenu
);
957 connect(actionForceRecheck
, &QAction::triggered
, this, &TransferListWidget::recheckSelectedTorrents
);
958 auto *actionForceReannounce
= new QAction(UIThemeManager::instance()->getIcon(u
"reannounce"_qs
, u
"document-edit-verify"_qs
), tr("Force r&eannounce"), listMenu
);
959 connect(actionForceReannounce
, &QAction::triggered
, this, &TransferListWidget::reannounceSelectedTorrents
);
960 auto *actionCopyMagnetLink
= new QAction(UIThemeManager::instance()->getIcon(u
"torrent-magnet"_qs
, u
"kt-magnet"_qs
), tr("&Magnet link"), listMenu
);
961 connect(actionCopyMagnetLink
, &QAction::triggered
, this, &TransferListWidget::copySelectedMagnetURIs
);
962 auto *actionCopyID
= new QAction(UIThemeManager::instance()->getIcon(u
"help-about"_qs
, u
"edit-copy"_qs
), tr("Torrent &ID"), listMenu
);
963 connect(actionCopyID
, &QAction::triggered
, this, &TransferListWidget::copySelectedIDs
);
964 auto *actionCopyName
= new QAction(UIThemeManager::instance()->getIcon(u
"name"_qs
, u
"edit-copy"_qs
), tr("&Name"), listMenu
);
965 connect(actionCopyName
, &QAction::triggered
, this, &TransferListWidget::copySelectedNames
);
966 auto *actionCopyHash1
= new QAction(UIThemeManager::instance()->getIcon(u
"hash"_qs
, u
"edit-copy"_qs
), tr("Info &hash v1"), listMenu
);
967 connect(actionCopyHash1
, &QAction::triggered
, this, [this]() { copySelectedInfohashes(CopyInfohashPolicy::Version1
); });
968 auto *actionCopyHash2
= new QAction(UIThemeManager::instance()->getIcon(u
"hash"_qs
, u
"edit-copy"_qs
), tr("Info h&ash v2"), listMenu
);
969 connect(actionCopyHash2
, &QAction::triggered
, this, [this]() { copySelectedInfohashes(CopyInfohashPolicy::Version2
); });
970 auto *actionSuperSeedingMode
= new TriStateAction(tr("Super seeding mode"), listMenu
);
971 connect(actionSuperSeedingMode
, &QAction::triggered
, this, &TransferListWidget::setSelectedTorrentsSuperSeeding
);
972 auto *actionRename
= new QAction(UIThemeManager::instance()->getIcon(u
"edit-rename"_qs
), tr("Re&name..."), listMenu
);
973 connect(actionRename
, &QAction::triggered
, this, &TransferListWidget::renameSelectedTorrent
);
974 auto *actionSequentialDownload
= new TriStateAction(tr("Download in sequential order"), listMenu
);
975 connect(actionSequentialDownload
, &QAction::triggered
, this, &TransferListWidget::setSelectedTorrentsSequentialDownload
);
976 auto *actionFirstLastPiecePrio
= new TriStateAction(tr("Download first and last pieces first"), listMenu
);
977 connect(actionFirstLastPiecePrio
, &QAction::triggered
, this, &TransferListWidget::setSelectedFirstLastPiecePrio
);
978 auto *actionAutoTMM
= new TriStateAction(tr("Automatic Torrent Management"), listMenu
);
979 actionAutoTMM
->setToolTip(tr("Automatic mode means that various torrent properties (e.g. save path) will be decided by the associated category"));
980 connect(actionAutoTMM
, &QAction::triggered
, this, &TransferListWidget::setSelectedAutoTMMEnabled
);
981 auto *actionEditTracker
= new QAction(UIThemeManager::instance()->getIcon(u
"edit-rename"_qs
), tr("Edit trac&kers..."), listMenu
);
982 connect(actionEditTracker
, &QAction::triggered
, this, &TransferListWidget::editTorrentTrackers
);
983 auto *actionExportTorrent
= new QAction(UIThemeManager::instance()->getIcon(u
"edit-copy"_qs
), tr("E&xport .torrent..."), listMenu
);
984 connect(actionExportTorrent
, &QAction::triggered
, this, &TransferListWidget::exportTorrent
);
987 // Enable/disable pause/start action given the DL state
988 bool needsPause
= false, needsStart
= false, needsForce
= false, needsPreview
= false;
989 bool allSameSuperSeeding
= true;
990 bool superSeedingMode
= false;
991 bool allSameSequentialDownloadMode
= true, allSamePrioFirstlast
= true;
992 bool sequentialDownloadMode
= false, prioritizeFirstLast
= false;
993 bool oneHasMetadata
= false, oneNotFinished
= false;
994 bool allSameCategory
= true;
995 bool allSameAutoTMM
= true;
996 bool firstAutoTMM
= false;
997 QString firstCategory
;
1001 bool hasInfohashV1
= false, hasInfohashV2
= false;
1002 bool oneCanForceReannounce
= false;
1004 for (const QModelIndex
&index
: selectedIndexes
)
1006 // Get the file name
1007 // Get handle and pause the torrent
1008 const BitTorrent::Torrent
*torrent
= m_listModel
->torrentHandle(mapToSource(index
));
1009 if (!torrent
) continue;
1011 if (firstCategory
.isEmpty() && first
)
1012 firstCategory
= torrent
->category();
1013 if (firstCategory
!= torrent
->category())
1014 allSameCategory
= false;
1016 const TagSet torrentTags
= torrent
->tags();
1017 tagsInAny
.unite(torrentTags
);
1021 firstAutoTMM
= torrent
->isAutoTMMEnabled();
1022 tagsInAll
= torrentTags
;
1026 tagsInAll
.intersect(torrentTags
);
1029 if (firstAutoTMM
!= torrent
->isAutoTMMEnabled())
1030 allSameAutoTMM
= false;
1032 if (torrent
->hasMetadata())
1033 oneHasMetadata
= true;
1034 if (!torrent
->isFinished())
1036 oneNotFinished
= true;
1039 sequentialDownloadMode
= torrent
->isSequentialDownload();
1040 prioritizeFirstLast
= torrent
->hasFirstLastPiecePriority();
1044 if (sequentialDownloadMode
!= torrent
->isSequentialDownload())
1045 allSameSequentialDownloadMode
= false;
1046 if (prioritizeFirstLast
!= torrent
->hasFirstLastPiecePriority())
1047 allSamePrioFirstlast
= false;
1052 if (!oneNotFinished
&& allSameSuperSeeding
&& torrent
->hasMetadata())
1055 superSeedingMode
= torrent
->superSeeding();
1056 else if (superSeedingMode
!= torrent
->superSeeding())
1057 allSameSuperSeeding
= false;
1061 if (!torrent
->isForced())
1066 const bool isPaused
= torrent
->isPaused();
1072 if (torrent
->isErrored() || torrent
->hasMissingFiles())
1074 // If torrent is in "errored" or "missing files" state
1075 // it cannot keep further processing until you restart it.
1080 if (torrent
->hasMetadata())
1081 needsPreview
= true;
1083 if (!hasInfohashV1
&& torrent
->infoHash().v1().isValid())
1084 hasInfohashV1
= true;
1085 if (!hasInfohashV2
&& torrent
->infoHash().v2().isValid())
1086 hasInfohashV2
= true;
1090 const bool rechecking
= torrent
->isChecking();
1097 const bool queued
= (BitTorrent::Session::instance()->isQueueingSystemEnabled() && torrent
->isQueued());
1099 if (!isPaused
&& !rechecking
&& !queued
)
1100 oneCanForceReannounce
= true;
1102 if (oneHasMetadata
&& oneNotFinished
&& !allSameSequentialDownloadMode
1103 && !allSamePrioFirstlast
&& !allSameSuperSeeding
&& !allSameCategory
1104 && needsStart
&& needsForce
&& needsPause
&& needsPreview
&& !allSameAutoTMM
1105 && hasInfohashV1
&& hasInfohashV2
&& oneCanForceReannounce
)
1112 listMenu
->addAction(actionStart
);
1114 listMenu
->addAction(actionPause
);
1116 listMenu
->addAction(actionForceStart
);
1117 listMenu
->addSeparator();
1118 listMenu
->addAction(actionDelete
);
1119 listMenu
->addSeparator();
1120 listMenu
->addAction(actionSetTorrentPath
);
1121 if (selectedIndexes
.size() == 1)
1122 listMenu
->addAction(actionRename
);
1123 listMenu
->addAction(actionEditTracker
);
1126 QStringList categories
= BitTorrent::Session::instance()->categories();
1127 std::sort(categories
.begin(), categories
.end(), Utils::Compare::NaturalLessThan
<Qt::CaseInsensitive
>());
1129 QMenu
*categoryMenu
= listMenu
->addMenu(UIThemeManager::instance()->getIcon(u
"view-categories"_qs
), tr("Categor&y"));
1131 categoryMenu
->addAction(UIThemeManager::instance()->getIcon(u
"list-add"_qs
), tr("&New...", "New category...")
1132 , this, &TransferListWidget::askNewCategoryForSelection
);
1133 categoryMenu
->addAction(UIThemeManager::instance()->getIcon(u
"edit-clear"_qs
), tr("&Reset", "Reset category")
1134 , this, [this]() { setSelectionCategory(u
""_qs
); });
1135 categoryMenu
->addSeparator();
1137 for (const QString
&category
: asConst(categories
))
1139 const QString escapedCategory
= QString(category
).replace(u
'&', u
"&&"_qs
); // avoid '&' becomes accelerator key
1140 QAction
*categoryAction
= categoryMenu
->addAction(UIThemeManager::instance()->getIcon(u
"view-categories"_qs
), escapedCategory
1141 , this, [this, category
]() { setSelectionCategory(category
); });
1143 if (allSameCategory
&& (category
== firstCategory
))
1145 categoryAction
->setCheckable(true);
1146 categoryAction
->setChecked(true);
1151 QStringList
tags(BitTorrent::Session::instance()->tags().values());
1152 std::sort(tags
.begin(), tags
.end(), Utils::Compare::NaturalLessThan
<Qt::CaseInsensitive
>());
1154 QMenu
*tagsMenu
= listMenu
->addMenu(UIThemeManager::instance()->getIcon(u
"tags"_qs
, u
"view-categories"_qs
), tr("Ta&gs"));
1156 tagsMenu
->addAction(UIThemeManager::instance()->getIcon(u
"list-add"_qs
), tr("&Add...", "Add / assign multiple tags...")
1157 , this, &TransferListWidget::askAddTagsForSelection
);
1158 tagsMenu
->addAction(UIThemeManager::instance()->getIcon(u
"edit-clear"_qs
), tr("&Remove All", "Remove all tags")
1161 if (Preferences::instance()->confirmRemoveAllTags())
1162 confirmRemoveAllTagsForSelection();
1164 clearSelectionTags();
1166 tagsMenu
->addSeparator();
1168 for (const QString
&tag
: asConst(tags
))
1170 auto *action
= new TriStateAction(tag
, tagsMenu
);
1171 action
->setCloseOnInteraction(false);
1173 const Qt::CheckState initialState
= tagsInAll
.contains(tag
) ? Qt::Checked
1174 : tagsInAny
.contains(tag
) ? Qt::PartiallyChecked
: Qt::Unchecked
;
1175 action
->setCheckState(initialState
);
1177 connect(action
, &QAction::toggled
, this, [this, tag
](const bool checked
)
1180 addSelectionTag(tag
);
1182 removeSelectionTag(tag
);
1185 tagsMenu
->addAction(action
);
1188 actionAutoTMM
->setCheckState(allSameAutoTMM
1189 ? (firstAutoTMM
? Qt::Checked
: Qt::Unchecked
)
1190 : Qt::PartiallyChecked
);
1191 listMenu
->addAction(actionAutoTMM
);
1193 listMenu
->addSeparator();
1194 listMenu
->addAction(actionTorrentOptions
);
1195 if (!oneNotFinished
&& oneHasMetadata
)
1197 actionSuperSeedingMode
->setCheckState(allSameSuperSeeding
1198 ? (superSeedingMode
? Qt::Checked
: Qt::Unchecked
)
1199 : Qt::PartiallyChecked
);
1200 listMenu
->addAction(actionSuperSeedingMode
);
1202 listMenu
->addSeparator();
1203 bool addedPreviewAction
= false;
1206 listMenu
->addAction(actionPreviewFile
);
1207 addedPreviewAction
= true;
1211 actionSequentialDownload
->setCheckState(allSameSequentialDownloadMode
1212 ? (sequentialDownloadMode
? Qt::Checked
: Qt::Unchecked
)
1213 : Qt::PartiallyChecked
);
1214 listMenu
->addAction(actionSequentialDownload
);
1216 actionFirstLastPiecePrio
->setCheckState(allSamePrioFirstlast
1217 ? (prioritizeFirstLast
? Qt::Checked
: Qt::Unchecked
)
1218 : Qt::PartiallyChecked
);
1219 listMenu
->addAction(actionFirstLastPiecePrio
);
1221 addedPreviewAction
= true;
1224 if (addedPreviewAction
)
1225 listMenu
->addSeparator();
1227 listMenu
->addAction(actionForceRecheck
);
1228 // We can not force reannounce torrents that are paused/errored/checking/missing files/queued.
1229 // We may already have the tracker list from magnet url. So we can force reannounce torrents without metadata anyway.
1230 listMenu
->addAction(actionForceReannounce
);
1231 actionForceReannounce
->setEnabled(oneCanForceReannounce
);
1232 if (!oneCanForceReannounce
)
1233 actionForceReannounce
->setToolTip(tr("Can not force reannounce if torrent is Paused/Queued/Errored/Checking"));
1234 listMenu
->addSeparator();
1235 listMenu
->addAction(actionOpenDestinationFolder
);
1236 if (BitTorrent::Session::instance()->isQueueingSystemEnabled() && oneNotFinished
)
1238 listMenu
->addSeparator();
1239 QMenu
*queueMenu
= listMenu
->addMenu(
1240 UIThemeManager::instance()->getIcon(u
"queued"_qs
), tr("&Queue"));
1241 queueMenu
->addAction(actionTopQueuePos
);
1242 queueMenu
->addAction(actionIncreaseQueuePos
);
1243 queueMenu
->addAction(actionDecreaseQueuePos
);
1244 queueMenu
->addAction(actionBottomQueuePos
);
1247 QMenu
*copySubMenu
= listMenu
->addMenu(UIThemeManager::instance()->getIcon(u
"edit-copy"_qs
), tr("&Copy"));
1248 copySubMenu
->addAction(actionCopyName
);
1249 copySubMenu
->addAction(actionCopyHash1
);
1250 actionCopyHash1
->setEnabled(hasInfohashV1
);
1251 copySubMenu
->addAction(actionCopyHash2
);
1252 actionCopyHash2
->setEnabled(hasInfohashV2
);
1253 copySubMenu
->addAction(actionCopyMagnetLink
);
1254 copySubMenu
->addAction(actionCopyID
);
1256 actionExportTorrent
->setToolTip(tr("Exported torrent is not necessarily the same as the imported"));
1257 listMenu
->addAction(actionExportTorrent
);
1259 listMenu
->popup(QCursor::pos());
1262 void TransferListWidget::currentChanged(const QModelIndex
¤t
, const QModelIndex
&)
1264 qDebug("CURRENT CHANGED");
1265 BitTorrent::Torrent
*torrent
= nullptr;
1266 if (current
.isValid())
1268 torrent
= m_listModel
->torrentHandle(mapToSource(current
));
1269 // Fix scrolling to the lowermost visible torrent
1270 QMetaObject::invokeMethod(this, [this, current
] { scrollTo(current
); }, Qt::QueuedConnection
);
1272 emit
currentTorrentChanged(torrent
);
1275 void TransferListWidget::applyCategoryFilter(const QString
&category
)
1277 if (category
.isNull())
1278 m_sortFilterModel
->disableCategoryFilter();
1280 m_sortFilterModel
->setCategoryFilter(category
);
1283 void TransferListWidget::applyTagFilter(const QString
&tag
)
1286 m_sortFilterModel
->disableTagFilter();
1288 m_sortFilterModel
->setTagFilter(tag
);
1291 void TransferListWidget::applyTrackerFilterAll()
1293 m_sortFilterModel
->disableTrackerFilter();
1296 void TransferListWidget::applyTrackerFilter(const QSet
<BitTorrent::TorrentID
> &torrentIDs
)
1298 m_sortFilterModel
->setTrackerFilter(torrentIDs
);
1301 void TransferListWidget::applyFilter(const QString
&name
, const TransferListModel::Column
&type
)
1303 m_sortFilterModel
->setFilterKeyColumn(type
);
1304 const QString pattern
= (Preferences::instance()->getRegexAsFilteringPatternForTransferList()
1305 ? name
: Utils::String::wildcardToRegexPattern(name
));
1306 m_sortFilterModel
->setFilterRegularExpression(QRegularExpression(pattern
, QRegularExpression::CaseInsensitiveOption
));
1309 void TransferListWidget::applyStatusFilter(int f
)
1311 m_sortFilterModel
->setStatusFilter(static_cast<TorrentFilter::Type
>(f
));
1312 // Select first item if nothing is selected
1313 if (selectionModel()->selectedRows(0).empty() && (m_sortFilterModel
->rowCount() > 0))
1315 qDebug("Nothing is selected, selecting first row: %s", qUtf8Printable(m_sortFilterModel
->index(0, TransferListModel::TR_NAME
).data().toString()));
1316 selectionModel()->setCurrentIndex(m_sortFilterModel
->index(0, TransferListModel::TR_NAME
), QItemSelectionModel::SelectCurrent
| QItemSelectionModel::Rows
);
1320 void TransferListWidget::saveSettings()
1322 Preferences::instance()->setTransHeaderState(header()->saveState());
1325 bool TransferListWidget::loadSettings()
1327 return header()->restoreState(Preferences::instance()->getTransHeaderState());
1330 void TransferListWidget::wheelEvent(QWheelEvent
*event
)
1332 if (event
->modifiers() & Qt::ShiftModifier
)
1334 // Shift + scroll = horizontal scroll
1336 QWheelEvent scrollHEvent
{event
->position(), event
->globalPosition()
1337 , event
->pixelDelta(), event
->angleDelta().transposed(), event
->buttons()
1338 , event
->modifiers(), event
->phase(), event
->inverted(), event
->source()};
1339 QTreeView::wheelEvent(&scrollHEvent
);
1343 QTreeView::wheelEvent(event
); // event delegated to base class