1 /*
2  * Bittorrent Client using Qt and libtorrent.
3  * Copyright (C) 2006  Christophe Dumez <chris@qbittorrent.org>
4  *
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.
9  *
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.
14  *
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.
18  *
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.
27  */
28 
29 #include "transferlistwidget.h"
30 
31 #include <algorithm>
32 
33 #include <QClipboard>
34 #include <QDebug>
35 #include <QFileDialog>
36 #include <QHeaderView>
37 #include <QMenu>
38 #include <QMessageBox>
39 #include <QRegExp>
40 #include <QRegularExpression>
41 #include <QSet>
42 #include <QShortcut>
43 #include <QTableView>
44 #include <QVector>
45 #include <QWheelEvent>
46 
47 #include "base/bittorrent/common.h"
48 #include "base/bittorrent/infohash.h"
49 #include "base/bittorrent/session.h"
50 #include "base/bittorrent/torrent.h"
51 #include "base/bittorrent/trackerentry.h"
52 #include "base/global.h"
53 #include "base/logger.h"
54 #include "base/preferences.h"
55 #include "base/torrentfilter.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 "mainwindow.h"
62 #include "optionsdialog.h"
63 #include "previewselectdialog.h"
64 #include "speedlimitdialog.h"
65 #include "torrentcategorydialog.h"
66 #include "torrentoptionsdialog.h"
67 #include "trackerentriesdialog.h"
68 #include "transferlistdelegate.h"
69 #include "transferlistmodel.h"
70 #include "transferlistsortmodel.h"
71 #include "tristateaction.h"
72 #include "uithememanager.h"
73 #include "utils.h"
74 
75 #ifdef Q_OS_MACOS
76 #include "macutilities.h"
77 #endif
78 
79 namespace
80 {
extractIDs(const QVector<BitTorrent::Torrent * > & torrents)81     QVector<BitTorrent::TorrentID> extractIDs(const QVector<BitTorrent::Torrent *> &torrents)
82     {
83         QVector<BitTorrent::TorrentID> torrentIDs;
84         torrentIDs.reserve(torrents.size());
85         for (const BitTorrent::Torrent *torrent : torrents)
86             torrentIDs << torrent->id();
87         return torrentIDs;
88     }
89 
torrentContainsPreviewableFiles(const BitTorrent::Torrent * const torrent)90     bool torrentContainsPreviewableFiles(const BitTorrent::Torrent *const torrent)
91     {
92         if (!torrent->hasMetadata())
93             return false;
94 
95         for (int i = 0; i < torrent->filesCount(); ++i)
96         {
97             QString fileName = torrent->fileName(i);
98             if (fileName.endsWith(QB_EXT))
99                 fileName.chop(QB_EXT.length());
100             if (Utils::Misc::isPreviewable(fileName))
101                 return true;
102         }
103 
104         return false;
105     }
106 
openDestinationFolder(const BitTorrent::Torrent * const torrent)107     void openDestinationFolder(const BitTorrent::Torrent *const torrent)
108     {
109 #ifdef Q_OS_MACOS
110         MacUtils::openFiles({torrent->contentPath(true)});
111 #else
112         if (torrent->filesCount() == 1)
113             Utils::Gui::openFolderSelect(torrent->contentPath(true));
114         else
115             Utils::Gui::openPath(torrent->contentPath(true));
116 #endif
117     }
118 
removeTorrents(const QVector<BitTorrent::Torrent * > & torrents,const bool isDeleteFileSelected)119     void removeTorrents(const QVector<BitTorrent::Torrent *> &torrents, const bool isDeleteFileSelected)
120     {
121         auto *session = BitTorrent::Session::instance();
122         const DeleteOption deleteOption = isDeleteFileSelected ? DeleteTorrentAndFiles : DeleteTorrent;
123         for (const BitTorrent::Torrent *torrent : torrents)
124             session->deleteTorrent(torrent->id(), deleteOption);
125     }
126 }
127 
TransferListWidget(QWidget * parent,MainWindow * mainWindow)128 TransferListWidget::TransferListWidget(QWidget *parent, MainWindow *mainWindow)
129     : QTreeView {parent}
130     , m_listModel {new TransferListModel {this}}
131     , m_sortFilterModel {new TransferListSortModel {this}}
132     , m_mainWindow {mainWindow}
133 {
134     // Load settings
135     const bool columnLoaded = loadSettings();
136 
137     // Create and apply delegate
138     setItemDelegate(new TransferListDelegate {this});
139 
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);
147 
148     // Visual settings
149     setUniformRowHeights(true);
150     setRootIsDecorated(false);
151     setAllColumnsShowFocus(true);
152     setSortingEnabled(true);
153     setSelectionMode(QAbstractItemView::ExtendedSelection);
154     setItemsExpandable(false);
155     setAutoScroll(true);
156     setDragDropMode(QAbstractItemView::DragOnly);
157 #if defined(Q_OS_MACOS)
158     setAttribute(Qt::WA_MacShowFocusRect, false);
159 #endif
160     header()->setStretchLastSection(false);
161 
162     // Default hidden columns
163     if (!columnLoaded)
164     {
165         setColumnHidden(TransferListModel::TR_ADD_DATE, true);
166         setColumnHidden(TransferListModel::TR_SEED_DATE, true);
167         setColumnHidden(TransferListModel::TR_UPLIMIT, true);
168         setColumnHidden(TransferListModel::TR_DLLIMIT, true);
169         setColumnHidden(TransferListModel::TR_TRACKER, true);
170         setColumnHidden(TransferListModel::TR_AMOUNT_DOWNLOADED, true);
171         setColumnHidden(TransferListModel::TR_AMOUNT_UPLOADED, true);
172         setColumnHidden(TransferListModel::TR_AMOUNT_DOWNLOADED_SESSION, true);
173         setColumnHidden(TransferListModel::TR_AMOUNT_UPLOADED_SESSION, true);
174         setColumnHidden(TransferListModel::TR_AMOUNT_LEFT, true);
175         setColumnHidden(TransferListModel::TR_TIME_ELAPSED, true);
176         setColumnHidden(TransferListModel::TR_SAVE_PATH, true);
177         setColumnHidden(TransferListModel::TR_COMPLETED, true);
178         setColumnHidden(TransferListModel::TR_RATIO_LIMIT, true);
179         setColumnHidden(TransferListModel::TR_SEEN_COMPLETE_DATE, true);
180         setColumnHidden(TransferListModel::TR_LAST_ACTIVITY, true);
181         setColumnHidden(TransferListModel::TR_TOTAL_SIZE, true);
182     }
183 
184     //Ensure that at least one column is visible at all times
185     bool atLeastOne = false;
186     for (int i = 0; i < TransferListModel::NB_COLUMNS; ++i)
187     {
188         if (!isColumnHidden(i))
189         {
190             atLeastOne = true;
191             break;
192         }
193     }
194     if (!atLeastOne)
195         setColumnHidden(TransferListModel::TR_NAME, false);
196 
197     //When adding/removing columns between versions some may
198     //end up being size 0 when the new version is launched with
199     //a conf file from the previous version.
200     for (int i = 0; i < TransferListModel::NB_COLUMNS; ++i)
201         if ((columnWidth(i) <= 0) && (!isColumnHidden(i)))
202             resizeColumnToContents(i);
203 
204     setContextMenuPolicy(Qt::CustomContextMenu);
205 
206     // Listen for list events
207     connect(this, &QAbstractItemView::doubleClicked, this, &TransferListWidget::torrentDoubleClicked);
208     connect(this, &QWidget::customContextMenuRequested, this, &TransferListWidget::displayListMenu);
209     header()->setContextMenuPolicy(Qt::CustomContextMenu);
210     connect(header(), &QWidget::customContextMenuRequested, this, &TransferListWidget::displayDLHoSMenu);
211     connect(header(), &QHeaderView::sectionMoved, this, &TransferListWidget::saveSettings);
212     connect(header(), &QHeaderView::sectionResized, this, &TransferListWidget::saveSettings);
213     connect(header(), &QHeaderView::sortIndicatorChanged, this, &TransferListWidget::saveSettings);
214 
215     const auto *editHotkey = new QShortcut(Qt::Key_F2, this, nullptr, nullptr, Qt::WidgetShortcut);
216     connect(editHotkey, &QShortcut::activated, this, &TransferListWidget::renameSelectedTorrent);
217     const auto *deleteHotkey = new QShortcut(QKeySequence::Delete, this, nullptr, nullptr, Qt::WidgetShortcut);
218     connect(deleteHotkey, &QShortcut::activated, this, &TransferListWidget::softDeleteSelectedTorrents);
219     const auto *permDeleteHotkey = new QShortcut(Qt::SHIFT + Qt::Key_Delete, this, nullptr, nullptr, Qt::WidgetShortcut);
220     connect(permDeleteHotkey, &QShortcut::activated, this, &TransferListWidget::permDeleteSelectedTorrents);
221     const auto *doubleClickHotkeyReturn = new QShortcut(Qt::Key_Return, this, nullptr, nullptr, Qt::WidgetShortcut);
222     connect(doubleClickHotkeyReturn, &QShortcut::activated, this, &TransferListWidget::torrentDoubleClicked);
223     const auto *doubleClickHotkeyEnter = new QShortcut(Qt::Key_Enter, this, nullptr, nullptr, Qt::WidgetShortcut);
224     connect(doubleClickHotkeyEnter, &QShortcut::activated, this, &TransferListWidget::torrentDoubleClicked);
225     const auto *recheckHotkey = new QShortcut(Qt::CTRL + Qt::Key_R, this, nullptr, nullptr, Qt::WidgetShortcut);
226     connect(recheckHotkey, &QShortcut::activated, this, &TransferListWidget::recheckSelectedTorrents);
227 
228     // This hack fixes reordering of first column with Qt5.
229     // https://github.com/qtproject/qtbase/commit/e0fc088c0c8bc61dbcaf5928b24986cd61a22777
230     QTableView unused;
231     unused.setVerticalHeader(header());
232     header()->setParent(this);
233     unused.setVerticalHeader(new QHeaderView(Qt::Horizontal));
234 }
235 
~TransferListWidget()236 TransferListWidget::~TransferListWidget()
237 {
238     // Save settings
239     saveSettings();
240 }
241 
getSourceModel() const242 TransferListModel *TransferListWidget::getSourceModel() const
243 {
244     return m_listModel;
245 }
246 
previewFile(const QString & filePath)247 void TransferListWidget::previewFile(const QString &filePath)
248 {
249     Utils::Gui::openPath(filePath);
250 }
251 
mapToSource(const QModelIndex & index) const252 QModelIndex TransferListWidget::mapToSource(const QModelIndex &index) const
253 {
254     Q_ASSERT(index.isValid());
255     if (index.model() == m_sortFilterModel)
256         return m_sortFilterModel->mapToSource(index);
257     return index;
258 }
259 
mapFromSource(const QModelIndex & index) const260 QModelIndex TransferListWidget::mapFromSource(const QModelIndex &index) const
261 {
262     Q_ASSERT(index.isValid());
263     Q_ASSERT(index.model() == m_sortFilterModel);
264     return m_sortFilterModel->mapFromSource(index);
265 }
266 
torrentDoubleClicked()267 void TransferListWidget::torrentDoubleClicked()
268 {
269     const QModelIndexList selectedIndexes = selectionModel()->selectedRows();
270     if ((selectedIndexes.size() != 1) || !selectedIndexes.first().isValid()) return;
271 
272     const QModelIndex index = m_listModel->index(mapToSource(selectedIndexes.first()).row());
273     BitTorrent::Torrent *const torrent = m_listModel->torrentHandle(index);
274     if (!torrent) return;
275 
276     int action;
277     if (torrent->isSeed())
278         action = Preferences::instance()->getActionOnDblClOnTorrentFn();
279     else
280         action = Preferences::instance()->getActionOnDblClOnTorrentDl();
281 
282     switch (action)
283     {
284     case TOGGLE_PAUSE:
285         if (torrent->isPaused())
286             torrent->resume();
287         else
288             torrent->pause();
289         break;
290     case PREVIEW_FILE:
291         if (torrentContainsPreviewableFiles(torrent))
292         {
293             auto *dialog = new PreviewSelectDialog(this, torrent);
294             dialog->setAttribute(Qt::WA_DeleteOnClose);
295             connect(dialog, &PreviewSelectDialog::readyToPreviewFile, this, &TransferListWidget::previewFile);
296             dialog->show();
297         }
298         else
299         {
300             openDestinationFolder(torrent);
301         }
302         break;
303     case OPEN_DEST:
304         openDestinationFolder(torrent);
305         break;
306     }
307 }
308 
getSelectedTorrents() const309 QVector<BitTorrent::Torrent *> TransferListWidget::getSelectedTorrents() const
310 {
311     const QModelIndexList selectedRows = selectionModel()->selectedRows();
312 
313     QVector<BitTorrent::Torrent *> torrents;
314     torrents.reserve(selectedRows.size());
315     for (const QModelIndex &index : selectedRows)
316         torrents << m_listModel->torrentHandle(mapToSource(index));
317     return torrents;
318 }
319 
getVisibleTorrents() const320 QVector<BitTorrent::Torrent *> TransferListWidget::getVisibleTorrents() const
321 {
322     const int visibleTorrentsCount = m_sortFilterModel->rowCount();
323 
324     QVector<BitTorrent::Torrent *> torrents;
325     torrents.reserve(visibleTorrentsCount);
326     for (int i = 0; i < visibleTorrentsCount; ++i)
327         torrents << m_listModel->torrentHandle(mapToSource(m_sortFilterModel->index(i, 0)));
328     return torrents;
329 }
330 
setSelectedTorrentsLocation()331 void TransferListWidget::setSelectedTorrentsLocation()
332 {
333     const QVector<BitTorrent::Torrent *> torrents = getSelectedTorrents();
334     if (torrents.isEmpty()) return;
335 
336     const QString oldLocation = torrents[0]->savePath();
337     const QString newLocation = QFileDialog::getExistingDirectory(this, tr("Choose save path"), oldLocation,
338                                             QFileDialog::DontConfirmOverwrite | QFileDialog::ShowDirsOnly | QFileDialog::HideNameFilterDetails);
339     if (newLocation.isEmpty() || !QDir(newLocation).exists()) return;
340 
341     // Actually move storage
342     for (BitTorrent::Torrent *const torrent : torrents)
343         torrent->move(Utils::Fs::expandPathAbs(newLocation));
344 }
345 
pauseAllTorrents()346 void TransferListWidget::pauseAllTorrents()
347 {
348     for (BitTorrent::Torrent *const torrent : asConst(BitTorrent::Session::instance()->torrents()))
349         torrent->pause();
350 }
351 
resumeAllTorrents()352 void TransferListWidget::resumeAllTorrents()
353 {
354     for (BitTorrent::Torrent *const torrent : asConst(BitTorrent::Session::instance()->torrents()))
355         torrent->resume();
356 }
357 
startSelectedTorrents()358 void TransferListWidget::startSelectedTorrents()
359 {
360     for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
361         torrent->resume();
362 }
363 
forceStartSelectedTorrents()364 void TransferListWidget::forceStartSelectedTorrents()
365 {
366     for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
367         torrent->resume(BitTorrent::TorrentOperatingMode::Forced);
368 }
369 
startVisibleTorrents()370 void TransferListWidget::startVisibleTorrents()
371 {
372     for (BitTorrent::Torrent *const torrent : asConst(getVisibleTorrents()))
373         torrent->resume();
374 }
375 
pauseSelectedTorrents()376 void TransferListWidget::pauseSelectedTorrents()
377 {
378     for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
379         torrent->pause();
380 }
381 
pauseVisibleTorrents()382 void TransferListWidget::pauseVisibleTorrents()
383 {
384     for (BitTorrent::Torrent *const torrent : asConst(getVisibleTorrents()))
385         torrent->pause();
386 }
387 
softDeleteSelectedTorrents()388 void TransferListWidget::softDeleteSelectedTorrents()
389 {
390     deleteSelectedTorrents(false);
391 }
392 
permDeleteSelectedTorrents()393 void TransferListWidget::permDeleteSelectedTorrents()
394 {
395     deleteSelectedTorrents(true);
396 }
397 
deleteSelectedTorrents(const bool deleteLocalFiles)398 void TransferListWidget::deleteSelectedTorrents(const bool deleteLocalFiles)
399 {
400     if (m_mainWindow->currentTabWidget() != this) return;
401 
402     const QVector<BitTorrent::Torrent *> torrents = getSelectedTorrents();
403     if (torrents.empty()) return;
404 
405     if (Preferences::instance()->confirmTorrentDeletion())
406     {
407         auto *dialog = new DeletionConfirmationDialog(this, torrents.size(), torrents[0]->name(), deleteLocalFiles);
408         dialog->setAttribute(Qt::WA_DeleteOnClose);
409         connect(dialog, &DeletionConfirmationDialog::accepted, this, [this, dialog]()
410         {
411             // Some torrents might be removed when waiting for user input, so refetch the torrent list
412             // NOTE: this will only work when dialog is modal
413             removeTorrents(getSelectedTorrents(), dialog->isDeleteFileSelected());
414         });
415         dialog->open();
416     }
417     else
418     {
419         removeTorrents(torrents, deleteLocalFiles);
420     }
421 }
422 
deleteVisibleTorrents()423 void TransferListWidget::deleteVisibleTorrents()
424 {
425     const QVector<BitTorrent::Torrent *> torrents = getVisibleTorrents();
426     if (torrents.empty()) return;
427 
428     if (Preferences::instance()->confirmTorrentDeletion())
429     {
430         auto *dialog = new DeletionConfirmationDialog(this, torrents.size(), torrents[0]->name(), false);
431         dialog->setAttribute(Qt::WA_DeleteOnClose);
432         connect(dialog, &DeletionConfirmationDialog::accepted, this, [this, dialog]()
433         {
434             // Some torrents might be removed when waiting for user input, so refetch the torrent list
435             // NOTE: this will only work when dialog is modal
436             removeTorrents(getVisibleTorrents(), dialog->isDeleteFileSelected());
437         });
438         dialog->open();
439     }
440     else
441     {
442         removeTorrents(torrents, false);
443     }
444 }
445 
increaseQueuePosSelectedTorrents()446 void TransferListWidget::increaseQueuePosSelectedTorrents()
447 {
448     qDebug() << Q_FUNC_INFO;
449     if (m_mainWindow->currentTabWidget() == this)
450         BitTorrent::Session::instance()->increaseTorrentsQueuePos(extractIDs(getSelectedTorrents()));
451 }
452 
decreaseQueuePosSelectedTorrents()453 void TransferListWidget::decreaseQueuePosSelectedTorrents()
454 {
455     qDebug() << Q_FUNC_INFO;
456     if (m_mainWindow->currentTabWidget() == this)
457         BitTorrent::Session::instance()->decreaseTorrentsQueuePos(extractIDs(getSelectedTorrents()));
458 }
459 
topQueuePosSelectedTorrents()460 void TransferListWidget::topQueuePosSelectedTorrents()
461 {
462     if (m_mainWindow->currentTabWidget() == this)
463         BitTorrent::Session::instance()->topTorrentsQueuePos(extractIDs(getSelectedTorrents()));
464 }
465 
bottomQueuePosSelectedTorrents()466 void TransferListWidget::bottomQueuePosSelectedTorrents()
467 {
468     if (m_mainWindow->currentTabWidget() == this)
469         BitTorrent::Session::instance()->bottomTorrentsQueuePos(extractIDs(getSelectedTorrents()));
470 }
471 
copySelectedMagnetURIs() const472 void TransferListWidget::copySelectedMagnetURIs() const
473 {
474     QStringList magnetUris;
475     for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
476         magnetUris << torrent->createMagnetURI();
477 
478     qApp->clipboard()->setText(magnetUris.join('\n'));
479 }
480 
copySelectedNames() const481 void TransferListWidget::copySelectedNames() const
482 {
483     QStringList torrentNames;
484     for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
485         torrentNames << torrent->name();
486 
487     qApp->clipboard()->setText(torrentNames.join('\n'));
488 }
489 
copySelectedHashes() const490 void TransferListWidget::copySelectedHashes() const
491 {
492     QStringList torrentIDs;
493     for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
494         torrentIDs << torrent->id().toString();
495 
496     qApp->clipboard()->setText(torrentIDs.join('\n'));
497 }
498 
hideQueuePosColumn(bool hide)499 void TransferListWidget::hideQueuePosColumn(bool hide)
500 {
501     setColumnHidden(TransferListModel::TR_QUEUE_POSITION, hide);
502     if (!hide && (columnWidth(TransferListModel::TR_QUEUE_POSITION) == 0))
503         resizeColumnToContents(TransferListModel::TR_QUEUE_POSITION);
504 }
505 
openSelectedTorrentsFolder() const506 void TransferListWidget::openSelectedTorrentsFolder() const
507 {
508     QSet<QString> pathsList;
509 #ifdef Q_OS_MACOS
510     // On macOS you expect both the files and folders to be opened in their parent
511     // folders prehilighted for opening, so we use a custom method.
512     for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
513     {
514         QString path = torrent->contentPath(true);
515         pathsList.insert(path);
516     }
517     MacUtils::openFiles(pathsList);
518 #else
519     for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
520     {
521         QString path = torrent->contentPath(true);
522         if (!pathsList.contains(path))
523         {
524             if (torrent->filesCount() == 1)
525                 Utils::Gui::openFolderSelect(path);
526             else
527                 Utils::Gui::openPath(path);
528         }
529         pathsList.insert(path);
530     }
531 #endif // Q_OS_MACOS
532 }
533 
previewSelectedTorrents()534 void TransferListWidget::previewSelectedTorrents()
535 {
536     for (const BitTorrent::Torrent *torrent : asConst(getSelectedTorrents()))
537     {
538         if (torrentContainsPreviewableFiles(torrent))
539         {
540             auto *dialog = new PreviewSelectDialog(this, torrent);
541             dialog->setAttribute(Qt::WA_DeleteOnClose);
542             connect(dialog, &PreviewSelectDialog::readyToPreviewFile, this, &TransferListWidget::previewFile);
543             dialog->show();
544         }
545         else
546         {
547             QMessageBox::critical(this, tr("Unable to preview"), tr("The selected torrent \"%1\" does not contain previewable files")
548                 .arg(torrent->name()));
549         }
550     }
551 }
552 
setTorrentOptions()553 void TransferListWidget::setTorrentOptions()
554 {
555     const QVector<BitTorrent::Torrent *> selectedTorrents = getSelectedTorrents();
556     if (selectedTorrents.empty()) return;
557 
558     auto dialog = new TorrentOptionsDialog {this, selectedTorrents};
559     dialog->setAttribute(Qt::WA_DeleteOnClose);
560     dialog->open();
561 }
562 
recheckSelectedTorrents()563 void TransferListWidget::recheckSelectedTorrents()
564 {
565     if (Preferences::instance()->confirmTorrentRecheck())
566     {
567         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);
568         if (ret != QMessageBox::Yes) return;
569     }
570 
571     for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
572         torrent->forceRecheck();
573 }
574 
reannounceSelectedTorrents()575 void TransferListWidget::reannounceSelectedTorrents()
576 {
577     for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
578         torrent->forceReannounce();
579 }
580 
581 // hide/show columns menu
displayDLHoSMenu(const QPoint &)582 void TransferListWidget::displayDLHoSMenu(const QPoint&)
583 {
584     auto menu = new QMenu(this);
585     menu->setAttribute(Qt::WA_DeleteOnClose);
586     menu->setTitle(tr("Column visibility"));
587 
588     for (int i = 0; i < m_listModel->columnCount(); ++i)
589     {
590         if (!BitTorrent::Session::instance()->isQueueingSystemEnabled() && (i == TransferListModel::TR_QUEUE_POSITION))
591             continue;
592 
593         QAction *myAct = menu->addAction(m_listModel->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString());
594         myAct->setCheckable(true);
595         myAct->setChecked(!isColumnHidden(i));
596         myAct->setData(i);
597     }
598 
599     connect(menu, &QMenu::triggered, this, [this](const QAction *action)
600     {
601         int visibleCols = 0;
602         for (int i = 0; i < TransferListModel::NB_COLUMNS; ++i)
603         {
604             if (!isColumnHidden(i))
605                 ++visibleCols;
606 
607             if (visibleCols > 1)
608                 break;
609         }
610 
611         const int col = action->data().toInt();
612 
613         if (!isColumnHidden(col) && visibleCols == 1)
614             return;
615 
616         setColumnHidden(col, !isColumnHidden(col));
617 
618         if (!isColumnHidden(col) && columnWidth(col) <= 5)
619             resizeColumnToContents(col);
620 
621         saveSettings();
622     });
623 
624     menu->popup(QCursor::pos());
625 }
626 
setSelectedTorrentsSuperSeeding(const bool enabled) const627 void TransferListWidget::setSelectedTorrentsSuperSeeding(const bool enabled) const
628 {
629     for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
630     {
631         if (torrent->hasMetadata())
632             torrent->setSuperSeeding(enabled);
633     }
634 }
635 
setSelectedTorrentsSequentialDownload(const bool enabled) const636 void TransferListWidget::setSelectedTorrentsSequentialDownload(const bool enabled) const
637 {
638     for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
639         torrent->setSequentialDownload(enabled);
640 }
641 
setSelectedFirstLastPiecePrio(const bool enabled) const642 void TransferListWidget::setSelectedFirstLastPiecePrio(const bool enabled) const
643 {
644     for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
645         torrent->setFirstLastPiecePriority(enabled);
646 }
647 
setSelectedAutoTMMEnabled(const bool enabled) const648 void TransferListWidget::setSelectedAutoTMMEnabled(const bool enabled) const
649 {
650     for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
651         torrent->setAutoTMMEnabled(enabled);
652 }
653 
askNewCategoryForSelection()654 void TransferListWidget::askNewCategoryForSelection()
655 {
656     const QString newCategoryName = TorrentCategoryDialog::createCategory(this);
657     if (!newCategoryName.isEmpty())
658         setSelectionCategory(newCategoryName);
659 }
660 
askAddTagsForSelection()661 void TransferListWidget::askAddTagsForSelection()
662 {
663     const QStringList tags = askTagsForSelection(tr("Add Tags"));
664     for (const QString &tag : tags)
665         addSelectionTag(tag);
666 }
667 
editTorrentTrackers()668 void TransferListWidget::editTorrentTrackers()
669 {
670     const QVector<BitTorrent::Torrent *> torrents = getSelectedTorrents();
671     QVector<BitTorrent::TrackerEntry> commonTrackers;
672 
673     if (!torrents.empty())
674     {
675         commonTrackers = torrents[0]->trackers();
676 
677         for (const BitTorrent::Torrent *torrent : torrents)
678         {
679             QSet<BitTorrent::TrackerEntry> trackerSet;
680 
681             for (const BitTorrent::TrackerEntry &entry : asConst(torrent->trackers()))
682                 trackerSet.insert(entry);
683 
684             commonTrackers.erase(std::remove_if(commonTrackers.begin(), commonTrackers.end()
685                 , [&trackerSet](const BitTorrent::TrackerEntry &entry) { return !trackerSet.contains(entry); })
686                 , commonTrackers.end());
687         }
688     }
689 
690     auto trackerDialog = new TrackerEntriesDialog(this);
691     trackerDialog->setAttribute(Qt::WA_DeleteOnClose);
692     trackerDialog->setTrackers(commonTrackers);
693 
694     connect(trackerDialog, &QDialog::accepted, this, [torrents, trackerDialog]()
695     {
696         for (BitTorrent::Torrent *torrent : torrents)
697             torrent->replaceTrackers(trackerDialog->trackers());
698     });
699 
700     trackerDialog->open();
701 }
702 
confirmRemoveAllTagsForSelection()703 void TransferListWidget::confirmRemoveAllTagsForSelection()
704 {
705     QMessageBox::StandardButton response = QMessageBox::question(
706         this, tr("Remove All Tags"), tr("Remove all tags from selected torrents?"),
707         QMessageBox::Yes | QMessageBox::No);
708     if (response == QMessageBox::Yes)
709         clearSelectionTags();
710 }
711 
askTagsForSelection(const QString & dialogTitle)712 QStringList TransferListWidget::askTagsForSelection(const QString &dialogTitle)
713 {
714     QStringList tags;
715     bool invalid = true;
716     while (invalid)
717     {
718         bool ok = false;
719         invalid = false;
720         const QString tagsInput = AutoExpandableDialog::getText(
721             this, dialogTitle, tr("Comma-separated tags:"), QLineEdit::Normal, "", &ok).trimmed();
722         if (!ok || tagsInput.isEmpty())
723             return {};
724         tags = tagsInput.split(',', QString::SkipEmptyParts);
725         for (QString &tag : tags)
726         {
727             tag = tag.trimmed();
728             if (!BitTorrent::Session::isValidTag(tag))
729             {
730                 QMessageBox::warning(this, tr("Invalid tag")
731                                      , tr("Tag name: '%1' is invalid").arg(tag));
732                 invalid = true;
733             }
734         }
735     }
736     return tags;
737 }
738 
applyToSelectedTorrents(const std::function<void (BitTorrent::Torrent * const)> & fn)739 void TransferListWidget::applyToSelectedTorrents(const std::function<void (BitTorrent::Torrent *const)> &fn)
740 {
741     for (const QModelIndex &index : asConst(selectionModel()->selectedRows()))
742     {
743         BitTorrent::Torrent *const torrent = m_listModel->torrentHandle(mapToSource(index));
744         Q_ASSERT(torrent);
745         fn(torrent);
746     }
747 }
748 
renameSelectedTorrent()749 void TransferListWidget::renameSelectedTorrent()
750 {
751     const QModelIndexList selectedIndexes = selectionModel()->selectedRows();
752     if ((selectedIndexes.size() != 1) || !selectedIndexes.first().isValid()) return;
753 
754     const QModelIndex mi = m_listModel->index(mapToSource(selectedIndexes.first()).row(), TransferListModel::TR_NAME);
755     BitTorrent::Torrent *const torrent = m_listModel->torrentHandle(mi);
756     if (!torrent) return;
757 
758     // Ask for a new Name
759     bool ok = false;
760     QString name = AutoExpandableDialog::getText(this, tr("Rename"), tr("New name:"), QLineEdit::Normal, torrent->name(), &ok);
761     if (ok && !name.isEmpty())
762     {
763         name.replace(QRegularExpression("\r?\n|\r"), " ");
764         // Rename the torrent
765         m_listModel->setData(mi, name, Qt::DisplayRole);
766     }
767 }
768 
setSelectionCategory(const QString & category)769 void TransferListWidget::setSelectionCategory(const QString &category)
770 {
771     for (const QModelIndex &index : asConst(selectionModel()->selectedRows()))
772         m_listModel->setData(m_listModel->index(mapToSource(index).row(), TransferListModel::TR_CATEGORY), category, Qt::DisplayRole);
773 }
774 
addSelectionTag(const QString & tag)775 void TransferListWidget::addSelectionTag(const QString &tag)
776 {
777     applyToSelectedTorrents([&tag](BitTorrent::Torrent *const torrent) { torrent->addTag(tag); });
778 }
779 
removeSelectionTag(const QString & tag)780 void TransferListWidget::removeSelectionTag(const QString &tag)
781 {
782     applyToSelectedTorrents([&tag](BitTorrent::Torrent *const torrent) { torrent->removeTag(tag); });
783 }
784 
clearSelectionTags()785 void TransferListWidget::clearSelectionTags()
786 {
787     applyToSelectedTorrents([](BitTorrent::Torrent *const torrent) { torrent->removeAllTags(); });
788 }
789 
displayListMenu(const QPoint &)790 void TransferListWidget::displayListMenu(const QPoint &)
791 {
792     const QModelIndexList selectedIndexes = selectionModel()->selectedRows();
793     if (selectedIndexes.isEmpty()) return;
794 
795     auto *listMenu = new QMenu(this);
796     listMenu->setAttribute(Qt::WA_DeleteOnClose);
797 
798     // Create actions
799 
800     auto *actionStart = new QAction(UIThemeManager::instance()->getIcon("media-playback-start"), tr("Resume", "Resume/start the torrent"), listMenu);
801     connect(actionStart, &QAction::triggered, this, &TransferListWidget::startSelectedTorrents);
802     auto *actionPause = new QAction(UIThemeManager::instance()->getIcon("media-playback-pause"), tr("Pause", "Pause the torrent"), listMenu);
803     connect(actionPause, &QAction::triggered, this, &TransferListWidget::pauseSelectedTorrents);
804     auto *actionForceStart = new QAction(UIThemeManager::instance()->getIcon("media-seek-forward"), tr("Force Resume", "Force Resume/start the torrent"), listMenu);
805     connect(actionForceStart, &QAction::triggered, this, &TransferListWidget::forceStartSelectedTorrents);
806     auto *actionDelete = new QAction(UIThemeManager::instance()->getIcon("list-remove"), tr("Delete", "Delete the torrent"), listMenu);
807     connect(actionDelete, &QAction::triggered, this, &TransferListWidget::softDeleteSelectedTorrents);
808     auto *actionPreviewFile = new QAction(UIThemeManager::instance()->getIcon("view-preview"), tr("Preview file..."), listMenu);
809     connect(actionPreviewFile, &QAction::triggered, this, &TransferListWidget::previewSelectedTorrents);
810     auto *actionTorrentOptions = new QAction(UIThemeManager::instance()->getIcon("configure"), tr("Torrent options..."), listMenu);
811     connect(actionTorrentOptions, &QAction::triggered, this, &TransferListWidget::setTorrentOptions);
812     auto *actionOpenDestinationFolder = new QAction(UIThemeManager::instance()->getIcon("inode-directory"), tr("Open destination folder"), listMenu);
813     connect(actionOpenDestinationFolder, &QAction::triggered, this, &TransferListWidget::openSelectedTorrentsFolder);
814     auto *actionIncreaseQueuePos = new QAction(UIThemeManager::instance()->getIcon("go-up"), tr("Move up", "i.e. move up in the queue"), listMenu);
815     connect(actionIncreaseQueuePos, &QAction::triggered, this, &TransferListWidget::increaseQueuePosSelectedTorrents);
816     auto *actionDecreaseQueuePos = new QAction(UIThemeManager::instance()->getIcon("go-down"), tr("Move down", "i.e. Move down in the queue"), listMenu);
817     connect(actionDecreaseQueuePos, &QAction::triggered, this, &TransferListWidget::decreaseQueuePosSelectedTorrents);
818     auto *actionTopQueuePos = new QAction(UIThemeManager::instance()->getIcon("go-top"), tr("Move to top", "i.e. Move to top of the queue"), listMenu);
819     connect(actionTopQueuePos, &QAction::triggered, this, &TransferListWidget::topQueuePosSelectedTorrents);
820     auto *actionBottomQueuePos = new QAction(UIThemeManager::instance()->getIcon("go-bottom"), tr("Move to bottom", "i.e. Move to bottom of the queue"), listMenu);
821     connect(actionBottomQueuePos, &QAction::triggered, this, &TransferListWidget::bottomQueuePosSelectedTorrents);
822     auto *actionSetTorrentPath = new QAction(UIThemeManager::instance()->getIcon("inode-directory"), tr("Set location..."), listMenu);
823     connect(actionSetTorrentPath, &QAction::triggered, this, &TransferListWidget::setSelectedTorrentsLocation);
824     auto *actionForceRecheck = new QAction(UIThemeManager::instance()->getIcon("document-edit-verify"), tr("Force recheck"), listMenu);
825     connect(actionForceRecheck, &QAction::triggered, this, &TransferListWidget::recheckSelectedTorrents);
826     auto *actionForceReannounce = new QAction(UIThemeManager::instance()->getIcon("document-edit-verify"), tr("Force reannounce"), listMenu);
827     connect(actionForceReannounce, &QAction::triggered, this, &TransferListWidget::reannounceSelectedTorrents);
828     auto *actionCopyMagnetLink = new QAction(UIThemeManager::instance()->getIcon("kt-magnet"), tr("Magnet link"), listMenu);
829     connect(actionCopyMagnetLink, &QAction::triggered, this, &TransferListWidget::copySelectedMagnetURIs);
830     auto *actionCopyName = new QAction(UIThemeManager::instance()->getIcon("edit-copy"), tr("Name"), listMenu);
831     connect(actionCopyName, &QAction::triggered, this, &TransferListWidget::copySelectedNames);
832     auto *actionCopyHash = new QAction(UIThemeManager::instance()->getIcon("edit-copy"), tr("Hash"), listMenu);
833     connect(actionCopyHash, &QAction::triggered, this, &TransferListWidget::copySelectedHashes);
834     auto *actionSuperSeedingMode = new TriStateAction(tr("Super seeding mode"), listMenu);
835     connect(actionSuperSeedingMode, &QAction::triggered, this, &TransferListWidget::setSelectedTorrentsSuperSeeding);
836     auto *actionRename = new QAction(UIThemeManager::instance()->getIcon("edit-rename"), tr("Rename..."), listMenu);
837     connect(actionRename, &QAction::triggered, this, &TransferListWidget::renameSelectedTorrent);
838     auto *actionSequentialDownload = new TriStateAction(tr("Download in sequential order"), listMenu);
839     connect(actionSequentialDownload, &QAction::triggered, this, &TransferListWidget::setSelectedTorrentsSequentialDownload);
840     auto *actionFirstLastPiecePrio = new TriStateAction(tr("Download first and last pieces first"), listMenu);
841     connect(actionFirstLastPiecePrio, &QAction::triggered, this, &TransferListWidget::setSelectedFirstLastPiecePrio);
842     auto *actionAutoTMM = new TriStateAction(tr("Automatic Torrent Management"), listMenu);
843     actionAutoTMM->setToolTip(tr("Automatic mode means that various torrent properties(eg save path) will be decided by the associated category"));
844     connect(actionAutoTMM, &QAction::triggered, this, &TransferListWidget::setSelectedAutoTMMEnabled);
845     auto *actionEditTracker = new QAction(UIThemeManager::instance()->getIcon("edit-rename"), tr("Edit trackers..."), listMenu);
846     connect(actionEditTracker, &QAction::triggered, this, &TransferListWidget::editTorrentTrackers);
847     // End of actions
848 
849     // Enable/disable pause/start action given the DL state
850     bool needsPause = false, needsStart = false, needsForce = false, needsPreview = false;
851     bool allSameSuperSeeding = true;
852     bool superSeedingMode = false;
853     bool allSameSequentialDownloadMode = true, allSamePrioFirstlast = true;
854     bool sequentialDownloadMode = false, prioritizeFirstLast = false;
855     bool oneHasMetadata = false, oneNotSeed = false;
856     bool allSameCategory = true;
857     bool allSameAutoTMM = true;
858     bool firstAutoTMM = false;
859     QString firstCategory;
860     bool first = true;
861     QSet<QString> tagsInAny;
862     QSet<QString> tagsInAll;
863 
864     for (const QModelIndex &index : selectedIndexes)
865     {
866         // Get the file name
867         // Get handle and pause the torrent
868         const BitTorrent::Torrent *torrent = m_listModel->torrentHandle(mapToSource(index));
869         if (!torrent) continue;
870 
871         if (firstCategory.isEmpty() && first)
872             firstCategory = torrent->category();
873         if (firstCategory != torrent->category())
874             allSameCategory = false;
875 
876         tagsInAny.unite(torrent->tags());
877 
878         if (first)
879         {
880             firstAutoTMM = torrent->isAutoTMMEnabled();
881             tagsInAll = torrent->tags();
882         }
883         else
884         {
885             tagsInAll.intersect(torrent->tags());
886         }
887 
888         if (firstAutoTMM != torrent->isAutoTMMEnabled())
889             allSameAutoTMM = false;
890 
891         if (torrent->hasMetadata())
892             oneHasMetadata = true;
893         if (!torrent->isSeed())
894         {
895             oneNotSeed = true;
896             if (first)
897             {
898                 sequentialDownloadMode = torrent->isSequentialDownload();
899                 prioritizeFirstLast = torrent->hasFirstLastPiecePriority();
900             }
901             else
902             {
903                 if (sequentialDownloadMode != torrent->isSequentialDownload())
904                     allSameSequentialDownloadMode = false;
905                 if (prioritizeFirstLast != torrent->hasFirstLastPiecePriority())
906                     allSamePrioFirstlast = false;
907             }
908         }
909         else
910         {
911             if (!oneNotSeed && allSameSuperSeeding && torrent->hasMetadata())
912             {
913                 if (first)
914                     superSeedingMode = torrent->superSeeding();
915                 else if (superSeedingMode != torrent->superSeeding())
916                     allSameSuperSeeding = false;
917             }
918         }
919 
920         if (!torrent->isForced())
921             needsForce = true;
922         else
923             needsStart = true;
924 
925         if (torrent->isPaused())
926             needsStart = true;
927         else
928             needsPause = true;
929 
930         if (torrent->isErrored() || torrent->hasMissingFiles())
931         {
932             // If torrent is in "errored" or "missing files" state
933             // it cannot keep further processing until you restart it.
934             needsStart = true;
935             needsForce = true;
936         }
937 
938         if (torrent->hasMetadata())
939             needsPreview = true;
940 
941         first = false;
942 
943         if (oneHasMetadata && oneNotSeed && !allSameSequentialDownloadMode
944             && !allSamePrioFirstlast && !allSameSuperSeeding && !allSameCategory
945             && needsStart && needsForce && needsPause && needsPreview && !allSameAutoTMM)
946             {
947             break;
948         }
949     }
950 
951     if (needsStart)
952         listMenu->addAction(actionStart);
953     if (needsPause)
954         listMenu->addAction(actionPause);
955     if (needsForce)
956         listMenu->addAction(actionForceStart);
957     listMenu->addSeparator();
958     listMenu->addAction(actionDelete);
959     listMenu->addSeparator();
960     listMenu->addAction(actionSetTorrentPath);
961     if (selectedIndexes.size() == 1)
962         listMenu->addAction(actionRename);
963     listMenu->addAction(actionEditTracker);
964 
965     // Category Menu
966     QStringList categories = BitTorrent::Session::instance()->categories().keys();
967     std::sort(categories.begin(), categories.end(), Utils::String::naturalLessThan<Qt::CaseInsensitive>);
968 
969     QMenu *categoryMenu = listMenu->addMenu(UIThemeManager::instance()->getIcon("view-categories"), tr("Category"));
970 
971     categoryMenu->addAction(UIThemeManager::instance()->getIcon("list-add"), tr("New...", "New category...")
972         , this, &TransferListWidget::askNewCategoryForSelection);
973     categoryMenu->addAction(UIThemeManager::instance()->getIcon("edit-clear"), tr("Reset", "Reset category")
974         , this, [this]() { setSelectionCategory(""); });
975     categoryMenu->addSeparator();
976 
977     for (const QString &category : asConst(categories))
978     {
979         const QString escapedCategory = QString(category).replace('&', "&&");  // avoid '&' becomes accelerator key
980         QAction *cat = categoryMenu->addAction(UIThemeManager::instance()->getIcon("inode-directory"), escapedCategory
981             , this, [this, category]() { setSelectionCategory(category); });
982 
983         if (allSameCategory && (category == firstCategory))
984         {
985             cat->setCheckable(true);
986             cat->setChecked(true);
987         }
988     }
989 
990     // Tag Menu
991     QStringList tags(BitTorrent::Session::instance()->tags().values());
992     std::sort(tags.begin(), tags.end(), Utils::String::naturalLessThan<Qt::CaseInsensitive>);
993 
994     QMenu *tagsMenu = listMenu->addMenu(UIThemeManager::instance()->getIcon("view-categories"), tr("Tags"));
995 
996     tagsMenu->addAction(UIThemeManager::instance()->getIcon("list-add"), tr("Add...", "Add / assign multiple tags...")
997         , this, &TransferListWidget::askAddTagsForSelection);
998     tagsMenu->addAction(UIThemeManager::instance()->getIcon("edit-clear"), tr("Remove All", "Remove all tags")
999         , this, [this]()
1000     {
1001         if (Preferences::instance()->confirmRemoveAllTags())
1002             confirmRemoveAllTagsForSelection();
1003         else
1004             clearSelectionTags();
1005     });
1006     tagsMenu->addSeparator();
1007 
1008     for (const QString &tag : asConst(tags))
1009     {
1010         auto *action = new TriStateAction(tag, tagsMenu);
1011         action->setCloseOnInteraction(false);
1012 
1013         const Qt::CheckState initialState = tagsInAll.contains(tag) ? Qt::Checked
1014                                             : tagsInAny.contains(tag) ? Qt::PartiallyChecked
1015                                             : Qt::Unchecked;
1016         action->setCheckState(initialState);
1017 
1018         connect(action, &QAction::toggled, this, [this, tag](const bool checked)
1019         {
1020             if (checked)
1021                 addSelectionTag(tag);
1022             else
1023                 removeSelectionTag(tag);
1024         });
1025 
1026         tagsMenu->addAction(action);
1027     }
1028 
1029     actionAutoTMM->setCheckState(allSameAutoTMM
1030         ? (firstAutoTMM ? Qt::Checked : Qt::Unchecked)
1031         : Qt::PartiallyChecked);
1032     listMenu->addAction(actionAutoTMM);
1033 
1034     listMenu->addSeparator();
1035     listMenu->addAction(actionTorrentOptions);
1036     if (!oneNotSeed && oneHasMetadata)
1037     {
1038         actionSuperSeedingMode->setCheckState(allSameSuperSeeding
1039             ? (superSeedingMode ? Qt::Checked : Qt::Unchecked)
1040             : Qt::PartiallyChecked);
1041         listMenu->addAction(actionSuperSeedingMode);
1042     }
1043     listMenu->addSeparator();
1044     bool addedPreviewAction = false;
1045     if (needsPreview)
1046     {
1047         listMenu->addAction(actionPreviewFile);
1048         addedPreviewAction = true;
1049     }
1050     if (oneNotSeed)
1051     {
1052         actionSequentialDownload->setCheckState(allSameSequentialDownloadMode
1053             ? (sequentialDownloadMode ? Qt::Checked : Qt::Unchecked)
1054             : Qt::PartiallyChecked);
1055         listMenu->addAction(actionSequentialDownload);
1056 
1057         actionFirstLastPiecePrio->setCheckState(allSamePrioFirstlast
1058             ? (prioritizeFirstLast ? Qt::Checked : Qt::Unchecked)
1059             : Qt::PartiallyChecked);
1060         listMenu->addAction(actionFirstLastPiecePrio);
1061 
1062         addedPreviewAction = true;
1063     }
1064 
1065     if (addedPreviewAction)
1066         listMenu->addSeparator();
1067     if (oneHasMetadata)
1068     {
1069         listMenu->addAction(actionForceRecheck);
1070         listMenu->addAction(actionForceReannounce);
1071         listMenu->addSeparator();
1072     }
1073     listMenu->addAction(actionOpenDestinationFolder);
1074     if (BitTorrent::Session::instance()->isQueueingSystemEnabled() && oneNotSeed)
1075     {
1076         listMenu->addSeparator();
1077         QMenu *queueMenu = listMenu->addMenu(tr("Queue"));
1078         queueMenu->addAction(actionTopQueuePos);
1079         queueMenu->addAction(actionIncreaseQueuePos);
1080         queueMenu->addAction(actionDecreaseQueuePos);
1081         queueMenu->addAction(actionBottomQueuePos);
1082     }
1083 
1084     QMenu *copySubMenu = listMenu->addMenu(
1085         UIThemeManager::instance()->getIcon("edit-copy"), tr("Copy"));
1086     copySubMenu->addAction(actionCopyName);
1087     copySubMenu->addAction(actionCopyHash);
1088     copySubMenu->addAction(actionCopyMagnetLink);
1089 
1090     listMenu->popup(QCursor::pos());
1091 }
1092 
currentChanged(const QModelIndex & current,const QModelIndex &)1093 void TransferListWidget::currentChanged(const QModelIndex &current, const QModelIndex&)
1094 {
1095     qDebug("CURRENT CHANGED");
1096     BitTorrent::Torrent *torrent = nullptr;
1097     if (current.isValid())
1098     {
1099         torrent = m_listModel->torrentHandle(mapToSource(current));
1100         // Scroll Fix
1101         scrollTo(current);
1102     }
1103     emit currentTorrentChanged(torrent);
1104 }
1105 
applyCategoryFilter(const QString & category)1106 void TransferListWidget::applyCategoryFilter(const QString &category)
1107 {
1108     if (category.isNull())
1109         m_sortFilterModel->disableCategoryFilter();
1110     else
1111         m_sortFilterModel->setCategoryFilter(category);
1112 }
1113 
applyTagFilter(const QString & tag)1114 void TransferListWidget::applyTagFilter(const QString &tag)
1115 {
1116     if (tag.isNull())
1117         m_sortFilterModel->disableTagFilter();
1118     else
1119         m_sortFilterModel->setTagFilter(tag);
1120 }
1121 
applyTrackerFilterAll()1122 void TransferListWidget::applyTrackerFilterAll()
1123 {
1124     m_sortFilterModel->disableTrackerFilter();
1125 }
1126 
applyTrackerFilter(const QSet<BitTorrent::TorrentID> & torrentIDs)1127 void TransferListWidget::applyTrackerFilter(const QSet<BitTorrent::TorrentID> &torrentIDs)
1128 {
1129     m_sortFilterModel->setTrackerFilter(torrentIDs);
1130 }
1131 
applyNameFilter(const QString & name)1132 void TransferListWidget::applyNameFilter(const QString &name)
1133 {
1134     const QRegExp::PatternSyntax patternSyntax = Preferences::instance()->getRegexAsFilteringPatternForTransferList()
1135                 ? QRegExp::RegExp : QRegExp::WildcardUnix;
1136     m_sortFilterModel->setFilterRegExp(QRegExp(name, Qt::CaseInsensitive, patternSyntax));
1137 }
1138 
applyStatusFilter(int f)1139 void TransferListWidget::applyStatusFilter(int f)
1140 {
1141     m_sortFilterModel->setStatusFilter(static_cast<TorrentFilter::Type>(f));
1142     // Select first item if nothing is selected
1143     if (selectionModel()->selectedRows(0).empty() && (m_sortFilterModel->rowCount() > 0))
1144     {
1145         qDebug("Nothing is selected, selecting first row: %s", qUtf8Printable(m_sortFilterModel->index(0, TransferListModel::TR_NAME).data().toString()));
1146         selectionModel()->setCurrentIndex(m_sortFilterModel->index(0, TransferListModel::TR_NAME), QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows);
1147     }
1148 }
1149 
saveSettings()1150 void TransferListWidget::saveSettings()
1151 {
1152     Preferences::instance()->setTransHeaderState(header()->saveState());
1153 }
1154 
loadSettings()1155 bool TransferListWidget::loadSettings()
1156 {
1157     return header()->restoreState(Preferences::instance()->getTransHeaderState());
1158 }
1159 
wheelEvent(QWheelEvent * event)1160 void TransferListWidget::wheelEvent(QWheelEvent *event)
1161 {
1162     if (event->modifiers() & Qt::ShiftModifier)
1163     {
1164         // Shift + scroll = horizontal scroll
1165         event->accept();
1166 
1167 #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
1168         QWheelEvent scrollHEvent(event->position(), event->globalPosition()
1169             , event->pixelDelta(), event->angleDelta().transposed(), event->buttons()
1170             , event->modifiers(), event->phase(), event->inverted(), event->source());
1171 #else
1172         QWheelEvent scrollHEvent(event->pos(), event->globalPos()
1173             , event->delta(), event->buttons(), event->modifiers(), Qt::Horizontal);
1174 #endif
1175         QTreeView::wheelEvent(&scrollHEvent);
1176         return;
1177     }
1178 
1179     QTreeView::wheelEvent(event);  // event delegated to base class
1180 }
1181