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 ¤t, 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