35 #include <QFileDialog>
36 #include <QHeaderView>
38 #include <QMessageBox>
39 #include <QRegularExpression>
44 #include <QWheelEvent>
79 QVector<BitTorrent::TorrentID>
extractIDs(
const QVector<BitTorrent::Torrent *> &torrents)
81 QVector<BitTorrent::TorrentID> torrentIDs;
82 torrentIDs.reserve(torrents.size());
84 torrentIDs << torrent->id();
115 void removeTorrents(
const QVector<BitTorrent::Torrent *> &torrents,
const bool isDeleteFileSelected)
120 session->deleteTorrent(torrent->id(), deleteOption);
128 , m_mainWindow {mainWindow}
145 setUniformRowHeights(
true);
146 setRootIsDecorated(
false);
147 setAllColumnsShowFocus(
true);
148 setSortingEnabled(
true);
149 setSelectionMode(QAbstractItemView::ExtendedSelection);
150 setItemsExpandable(
false);
152 setDragDropMode(QAbstractItemView::DragOnly);
153 #if defined(Q_OS_MACOS)
154 setAttribute(Qt::WA_MacShowFocusRect,
false);
156 header()->setStretchLastSection(
false);
157 header()->setTextElideMode(Qt::ElideRight);
182 bool atLeastOne =
false;
185 if (!isColumnHidden(i))
198 if ((columnWidth(i) <= 0) && (!isColumnHidden(i)))
199 resizeColumnToContents(i);
201 setContextMenuPolicy(Qt::CustomContextMenu);
206 header()->setContextMenuPolicy(Qt::CustomContextMenu);
212 const auto *editHotkey =
new QShortcut(Qt::Key_F2,
this,
nullptr,
nullptr, Qt::WidgetShortcut);
214 const auto *deleteHotkey =
new QShortcut(QKeySequence::Delete,
this,
nullptr,
nullptr, Qt::WidgetShortcut);
216 const auto *permDeleteHotkey =
new QShortcut(Qt::SHIFT + Qt::Key_Delete,
this,
nullptr,
nullptr, Qt::WidgetShortcut);
218 const auto *doubleClickHotkeyReturn =
new QShortcut(Qt::Key_Return,
this,
nullptr,
nullptr, Qt::WidgetShortcut);
220 const auto *doubleClickHotkeyEnter =
new QShortcut(Qt::Key_Enter,
this,
nullptr,
nullptr, Qt::WidgetShortcut);
222 const auto *recheckHotkey =
new QShortcut(Qt::CTRL + Qt::Key_R,
this,
nullptr,
nullptr, Qt::WidgetShortcut);
228 unused.setVerticalHeader(header());
229 header()->setParent(
this);
230 unused.setVerticalHeader(
new QHeaderView(Qt::Horizontal));
251 Q_ASSERT(index.isValid());
259 Q_ASSERT(index.isValid());
266 const QModelIndexList selectedIndexes = selectionModel()->selectedRows();
267 if ((selectedIndexes.size() != 1) || !selectedIndexes.first().isValid())
return;
271 if (!torrent)
return;
291 dialog->setAttribute(Qt::WA_DeleteOnClose);
311 const QModelIndexList selectedRows = selectionModel()->selectedRows();
313 QVector<BitTorrent::Torrent *> torrents;
314 torrents.reserve(selectedRows.size());
315 for (
const QModelIndex &index : selectedRows)
324 QVector<BitTorrent::Torrent *> torrents;
325 torrents.reserve(visibleTorrentsCount);
326 for (
int i = 0; i < visibleTorrentsCount; ++i)
352 torrent->resume(BitTorrent::TorrentOperatingMode::Forced);
388 if (torrents.empty())
return;
393 dialog->setAttribute(Qt::WA_DeleteOnClose);
394 connect(dialog, &DeletionConfirmationDialog::accepted,
this, [
this, dialog]()
411 if (torrents.empty())
return;
416 dialog->setAttribute(Qt::WA_DeleteOnClose);
417 connect(dialog, &DeletionConfirmationDialog::accepted,
this, [
this, dialog]()
433 qDebug() << Q_FUNC_INFO;
440 qDebug() << Q_FUNC_INFO;
459 QStringList magnetUris;
461 magnetUris << torrent->createMagnetURI();
463 qApp->clipboard()->setText(magnetUris.join(
'\n'));
468 QStringList torrentNames;
470 torrentNames << torrent->name();
472 qApp->clipboard()->setText(torrentNames.join(
'\n'));
478 QStringList infoHashes;
479 infoHashes.reserve(selectedTorrents.size());
485 if (
const auto infoHash = torrent->infoHash().v1(); infoHash.isValid())
486 infoHashes << infoHash.toString();
492 if (
const auto infoHash = torrent->infoHash().v2(); infoHash.isValid())
493 infoHashes << infoHash.toString();
498 qApp->clipboard()->setText(infoHashes.join(
'\n'));
503 QStringList torrentIDs;
505 torrentIDs << torrent->id().toString();
507 qApp->clipboard()->setText(torrentIDs.join(
'\n'));
519 QSet<QString> pathsList;
525 const QString contentPath = QDir(torrent->actualStorageLocation()).absoluteFilePath(torrent->contentPath());
526 pathsList.insert(contentPath);
532 const QString contentPath = torrent->contentPath();
533 if (!pathsList.contains(contentPath))
535 if (torrent->filesCount() == 1)
540 pathsList.insert(contentPath);
552 dialog->setAttribute(Qt::WA_DeleteOnClose);
558 QMessageBox::critical(
this, tr(
"Unable to preview"), tr(
"The selected torrent \"%1\" does not contain previewable files")
559 .arg(torrent->name()));
567 if (selectedTorrents.empty())
return;
570 dialog->setAttribute(Qt::WA_DeleteOnClose);
578 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);
579 if (ret != QMessageBox::Yes)
return;
583 torrent->forceRecheck();
589 torrent->forceReannounce();
595 auto menu =
new QMenu(
this);
596 menu->setAttribute(Qt::WA_DeleteOnClose);
597 menu->setTitle(tr(
"Column visibility"));
604 QAction *myAct = menu->addAction(
m_listModel->
headerData(i, Qt::Horizontal, Qt::DisplayRole).toString());
605 myAct->setCheckable(
true);
606 myAct->setChecked(!isColumnHidden(i));
610 connect(menu, &QMenu::triggered,
this, [
this](
const QAction *
action)
615 if (!isColumnHidden(i))
622 const int col =
action->data().toInt();
624 if (!isColumnHidden(col) && visibleCols == 1)
627 setColumnHidden(col, !isColumnHidden(col));
629 if (!isColumnHidden(col) && columnWidth(col) <= 5)
630 resizeColumnToContents(col);
635 menu->popup(QCursor::pos());
642 if (torrent->hasMetadata())
643 torrent->setSuperSeeding(enabled);
650 torrent->setAutoTMMEnabled(enabled);
656 if (!newCategoryName.isEmpty())
663 for (
const QString &tag : tags)
670 QVector<BitTorrent::TrackerEntry> commonTrackers;
672 if (!torrents.empty())
674 commonTrackers = torrents[0]->trackers();
678 QSet<BitTorrent::TrackerEntry> trackerSet;
681 trackerSet.insert(entry);
683 commonTrackers.erase(std::remove_if(commonTrackers.begin(), commonTrackers.end()
685 , commonTrackers.end());
690 trackerDialog->setAttribute(Qt::WA_DeleteOnClose);
691 trackerDialog->setTrackers(commonTrackers);
693 connect(trackerDialog, &QDialog::accepted,
this, [torrents, trackerDialog]()
696 torrent->replaceTrackers(trackerDialog->trackers());
699 trackerDialog->open();
704 QMessageBox::StandardButton response = QMessageBox::question(
705 this, tr(
"Remove All Tags"), tr(
"Remove all tags from selected torrents?"),
706 QMessageBox::Yes | QMessageBox::No);
707 if (response == QMessageBox::Yes)
720 this, dialogTitle, tr(
"Comma-separated tags:"), QLineEdit::Normal,
"", &ok).trimmed();
721 if (!ok || tagsInput.isEmpty())
723 tags = tagsInput.split(
',', Qt::SkipEmptyParts);
724 for (QString &tag : tags)
729 QMessageBox::warning(
this, tr(
"Invalid tag")
730 , tr(
"Tag name: '%1' is invalid").arg(tag));
740 for (
const QModelIndex &index :
asConst(selectionModel()->selectedRows()))
750 const QModelIndexList selectedIndexes = selectionModel()->selectedRows();
751 if ((selectedIndexes.size() != 1) || !selectedIndexes.first().isValid())
return;
755 if (!torrent)
return;
760 if (ok && !name.isEmpty())
762 name.replace(QRegularExpression(
"\r?\n|\r"),
" ");
770 for (
const QModelIndex &index :
asConst(selectionModel()->selectedRows()))
791 const QModelIndexList selectedIndexes = selectionModel()->selectedRows();
792 if (selectedIndexes.isEmpty())
return;
794 auto *listMenu =
new QMenu(
this);
795 listMenu->setAttribute(Qt::WA_DeleteOnClose);
799 auto *actionStart =
new QAction(
UIThemeManager::instance()->getIcon(
"media-playback-start"), tr(
"Resume",
"Resume/start the torrent"), listMenu);
801 auto *actionPause =
new QAction(
UIThemeManager::instance()->getIcon(
"media-playback-pause"), tr(
"Pause",
"Pause the torrent"), listMenu);
803 auto *actionForceStart =
new QAction(
UIThemeManager::instance()->getIcon(
"media-seek-forward"), tr(
"Force Resume",
"Force Resume/start the torrent"), listMenu);
805 auto *actionDelete =
new QAction(
UIThemeManager::instance()->getIcon(
"list-remove"), tr(
"Delete",
"Delete the torrent"), listMenu);
807 auto *actionPreviewFile =
new QAction(
UIThemeManager::instance()->getIcon(
"view-preview"), tr(
"Preview file..."), listMenu);
809 auto *actionTorrentOptions =
new QAction(
UIThemeManager::instance()->getIcon(
"configure"), tr(
"Torrent options..."), listMenu);
811 auto *actionOpenDestinationFolder =
new QAction(
UIThemeManager::instance()->getIcon(
"inode-directory"), tr(
"Open destination folder"), listMenu);
813 auto *actionIncreaseQueuePos =
new QAction(
UIThemeManager::instance()->getIcon(
"go-up"), tr(
"Move up",
"i.e. move up in the queue"), listMenu);
815 auto *actionDecreaseQueuePos =
new QAction(
UIThemeManager::instance()->getIcon(
"go-down"), tr(
"Move down",
"i.e. Move down in the queue"), listMenu);
817 auto *actionTopQueuePos =
new QAction(
UIThemeManager::instance()->getIcon(
"go-top"), tr(
"Move to top",
"i.e. Move to top of the queue"), listMenu);
819 auto *actionBottomQueuePos =
new QAction(
UIThemeManager::instance()->getIcon(
"go-bottom"), tr(
"Move to bottom",
"i.e. Move to bottom of the queue"), listMenu);
821 auto *actionForceRecheck =
new QAction(
UIThemeManager::instance()->getIcon(
"document-edit-verify"), tr(
"Force recheck"), listMenu);
823 auto *actionForceReannounce =
new QAction(
UIThemeManager::instance()->getIcon(
"document-edit-verify"), tr(
"Force reannounce"), listMenu);
825 auto *actionCopyMagnetLink =
new QAction(
UIThemeManager::instance()->getIcon(
"kt-magnet"), tr(
"Magnet link"), listMenu);
831 auto *actionCopyHash1 =
new QAction(
UIThemeManager::instance()->getIcon(
"edit-copy"), tr(
"Info hash v1"), listMenu);
833 auto *actionCopyHash2 =
new QAction(
UIThemeManager::instance()->getIcon(
"edit-copy"), tr(
"Info hash v2"), listMenu);
835 auto *actionSuperSeedingMode =
new TriStateAction(tr(
"Super seeding mode"), listMenu);
839 auto *actionEditTracker =
new QAction(
UIThemeManager::instance()->getIcon(
"edit-rename"), tr(
"Edit trackers..."), listMenu);
844 bool needsPause =
false, needsStart =
false, needsForce =
false, needsPreview =
false;
845 bool allSameSuperSeeding =
true;
846 bool superSeedingMode =
false;
847 bool allSameSequentialDownloadMode =
true, allSamePrioFirstlast =
true;
848 bool sequentialDownloadMode =
false, prioritizeFirstLast =
false;
849 bool oneHasMetadata =
false, oneNotSeed =
false;
850 bool allSameCategory =
true;
851 QString firstCategory;
855 bool hasInfohashV1 =
false, hasInfohashV2 =
false;
857 for (
const QModelIndex &index : selectedIndexes)
862 if (!torrent)
continue;
864 if (firstCategory.isEmpty() && first)
865 firstCategory = torrent->
category();
866 if (firstCategory != torrent->
category())
867 allSameCategory =
false;
870 tagsInAny.
unite(torrentTags);
874 tagsInAll = torrentTags;
882 oneHasMetadata =
true;
894 allSameSequentialDownloadMode =
false;
896 allSamePrioFirstlast =
false;
901 if (!oneNotSeed && allSameSuperSeeding && torrent->
hasMetadata())
906 allSameSuperSeeding =
false;
932 hasInfohashV1 =
true;
934 hasInfohashV2 =
true;
938 if (oneHasMetadata && oneNotSeed && !allSameSequentialDownloadMode
939 && !allSamePrioFirstlast && !allSameSuperSeeding && !allSameCategory
940 && needsStart && needsForce && needsPause && needsPreview
941 && hasInfohashV1 && hasInfohashV2)
948 listMenu->addAction(actionStart);
950 listMenu->addAction(actionPause);
952 listMenu->addAction(actionForceStart);
953 listMenu->addSeparator();
954 listMenu->addAction(actionDelete);
955 listMenu->addSeparator();
956 if (selectedIndexes.size() == 1)
957 listMenu->addAction(actionRename);
958 listMenu->addAction(actionEditTracker);
976 tagsMenu->addSeparator();
978 for (
const QString &tag :
asConst(tags))
981 action->setCloseOnInteraction(
false);
983 const Qt::CheckState initialState = tagsInAll.
contains(tag) ? Qt::Checked
984 : tagsInAny.
contains(tag) ? Qt::PartiallyChecked : Qt::Unchecked;
985 action->setCheckState(initialState);
987 connect(
action, &QAction::toggled,
this, [
this, tag](
const bool checked)
995 tagsMenu->addAction(
action);
998 listMenu->addSeparator();
999 listMenu->addAction(actionTorrentOptions);
1000 if (!oneNotSeed && oneHasMetadata)
1002 actionSuperSeedingMode->setCheckState(allSameSuperSeeding
1003 ? (superSeedingMode ? Qt::Checked : Qt::Unchecked)
1004 : Qt::PartiallyChecked);
1005 listMenu->addAction(actionSuperSeedingMode);
1007 listMenu->addSeparator();
1008 bool addedPreviewAction =
false;
1011 listMenu->addAction(actionPreviewFile);
1012 addedPreviewAction =
true;
1015 addedPreviewAction =
true;
1017 if (addedPreviewAction)
1018 listMenu->addSeparator();
1021 listMenu->addAction(actionForceRecheck);
1022 listMenu->addAction(actionForceReannounce);
1023 listMenu->addSeparator();
1025 listMenu->addAction(actionOpenDestinationFolder);
1028 listMenu->addSeparator();
1029 QMenu *queueMenu = listMenu->addMenu(tr(
"Queue"));
1030 queueMenu->addAction(actionTopQueuePos);
1031 queueMenu->addAction(actionIncreaseQueuePos);
1032 queueMenu->addAction(actionDecreaseQueuePos);
1033 queueMenu->addAction(actionBottomQueuePos);
1036 QMenu *copySubMenu = listMenu->addMenu(
1038 copySubMenu->addAction(actionCopyName);
1039 copySubMenu->addAction(actionCopyHash1);
1040 actionCopyHash1->setEnabled(hasInfohashV1);
1041 copySubMenu->addAction(actionCopyHash2);
1042 actionCopyHash2->setEnabled(hasInfohashV2);
1043 copySubMenu->addAction(actionCopyMagnetLink);
1044 copySubMenu->addAction(actionCopyID);
1046 listMenu->popup(QCursor::pos());
1051 qDebug(
"CURRENT CHANGED");
1053 if (current.isValid())
1064 if (category.isNull())
1092 m_sortFilterModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption));
1099 if (selectionModel()->selectedRows(0).empty() && (
m_sortFilterModel->rowCount() > 0))
1118 if (event->modifiers() & Qt::ShiftModifier)
1122 QWheelEvent scrollHEvent(event->position(), event->globalPosition()
1123 , event->pixelDelta(), event->angleDelta().transposed(), event->buttons()
1124 , event->modifiers(), event->phase(), event->inverted(), event->source());
1125 QTreeView::wheelEvent(&scrollHEvent);
1129 QTreeView::wheelEvent(event);
static QString getText(QWidget *parent, const QString &title, const QString &label, QLineEdit::EchoMode mode=QLineEdit::Normal, const QString &text={}, bool *ok=nullptr, bool excludeExtension=false, Qt::InputMethodHints inputMethodHints=Qt::ImhNone)
virtual int filesCount() const =0
static Session * instance()
void decreaseTorrentsQueuePos(const QVector< TorrentID > &ids)
void bottomTorrentsQueuePos(const QVector< TorrentID > &ids)
bool isQueueingSystemEnabled() const
static bool isValidTag(const QString &tag)
void topTorrentsQueuePos(const QVector< TorrentID > &ids)
void increaseTorrentsQueuePos(const QVector< TorrentID > &ids)
virtual bool isErrored() const =0
virtual TagSet tags() const =0
virtual bool removeTag(const QString &tag)=0
virtual bool hasFirstLastPiecePriority() const =0
virtual QString category() const =0
virtual bool hasMissingFiles() const =0
virtual InfoHash infoHash() const =0
virtual QStringList filePaths() const =0
virtual bool superSeeding() const =0
virtual void resume(TorrentOperatingMode mode=TorrentOperatingMode::AutoManaged)=0
virtual void removeAllTags()=0
virtual bool isPaused() const =0
virtual bool addTag(const QString &tag)=0
virtual bool isForced() const =0
virtual bool isSeed() const =0
virtual bool isSequentialDownload() const =0
virtual bool hasMetadata() const =0
virtual QString name() const =0
virtual QString contentPath() const =0
QWidget * currentTabWidget() const
bool contains(const key_type &value) const
ThisType & unite(const ThisType &other)
ThisType & intersect(const ThisType &other)
void setTransHeaderState(const QByteArray &state)
static Preferences * instance()
int getActionOnDblClOnTorrentFn() const
bool getRegexAsFilteringPatternForTransferList() const
int getActionOnDblClOnTorrentDl() const
void readyToPreviewFile(QString) const
static QString createCategory(QWidget *parent, const QString &parentCategoryName={})
@ TR_AMOUNT_DOWNLOADED_SESSION
@ TR_AMOUNT_UPLOADED_SESSION
bool setData(const QModelIndex &index, const QVariant &value, int role) override
QVariant headerData(int section, Qt::Orientation orientation, int role) const override
BitTorrent::Torrent * torrentHandle(const QModelIndex &index) const
int columnCount(const QModelIndex &parent={}) const override
void setStatusFilter(TorrentFilter::Type filter)
void setTagFilter(const QString &tag)
void setCategoryFilter(const QString &category)
void disableTrackerFilter()
void disableCategoryFilter()
void setTrackerFilter(const QSet< BitTorrent::TorrentID > &torrentIDs)
static UIThemeManager * instance()
QIcon getIcon(const QString &iconId, const QString &fallback={}) const
constexpr std::add_const_t< T > & asConst(T &t) noexcept
void openFiles(const QSet< QString > &pathsList)
QString fileName(const QString &filePath)
void openFolderSelect(const QString &absolutePath)
void openPath(const QString &absolutePath)
bool isPreviewable(const QString &filename)
QString wildcardToRegexPattern(const QString &pattern)