From 1be5b3abd8feec8ffa80f737ded0e6292d327199 Mon Sep 17 00:00:00 2001 From: Vladimir Golovnev Date: Sat, 11 Feb 2023 15:22:01 +0300 Subject: [PATCH] Revamp torrent content widget PR #18162. --- src/base/CMakeLists.txt | 2 + src/base/base.pri | 2 + src/base/bittorrent/torrent.h | 15 +- .../bittorrent/torrentcontenthandler.cpp} | 29 +- src/base/bittorrent/torrentcontenthandler.h | 62 ++ src/gui/CMakeLists.txt | 8 +- src/gui/addnewtorrentdialog.cpp | 543 +++++++----------- src/gui/addnewtorrentdialog.h | 15 +- src/gui/addnewtorrentdialog.ui | 6 +- src/gui/gui.pri | 8 +- src/gui/properties/propertieswidget.cpp | 329 +---------- src/gui/properties/propertieswidget.h | 14 +- src/gui/properties/propertieswidget.ui | 6 +- src/gui/torrentcontentfiltermodel.cpp | 34 +- src/gui/torrentcontentfiltermodel.h | 17 +- ...ate.cpp => torrentcontentitemdelegate.cpp} | 35 +- ...elegate.h => torrentcontentitemdelegate.h} | 24 +- src/gui/torrentcontentmodel.cpp | 300 ++++++---- src/gui/torrentcontentmodel.h | 31 +- src/gui/torrentcontentmodelfolder.h | 2 +- src/gui/torrentcontenttreeview.cpp | 165 ------ src/gui/torrentcontentwidget.cpp | 489 ++++++++++++++++ src/gui/torrentcontentwidget.h | 121 ++++ 23 files changed, 1161 insertions(+), 1096 deletions(-) rename src/{gui/torrentcontenttreeview.h => base/bittorrent/torrentcontenthandler.cpp} (69%) create mode 100644 src/base/bittorrent/torrentcontenthandler.h rename src/gui/{properties/proplistdelegate.cpp => torrentcontentitemdelegate.cpp} (78%) rename src/gui/{properties/proplistdelegate.h => torrentcontentitemdelegate.h} (84%) delete mode 100644 src/gui/torrentcontenttreeview.cpp create mode 100644 src/gui/torrentcontentwidget.cpp create mode 100644 src/gui/torrentcontentwidget.h diff --git a/src/base/CMakeLists.txt b/src/base/CMakeLists.txt index 15929773c..6898346ec 100644 --- a/src/base/CMakeLists.txt +++ b/src/base/CMakeLists.txt @@ -34,6 +34,7 @@ add_library(qbt_base STATIC bittorrent/sessionstatus.h bittorrent/speedmonitor.h bittorrent/torrent.h + bittorrent/torrentcontenthandler.h bittorrent/torrentcontentlayout.h bittorrent/torrentcreatorthread.h bittorrent/torrentimpl.h @@ -129,6 +130,7 @@ add_library(qbt_base STATIC bittorrent/sessionimpl.cpp bittorrent/speedmonitor.cpp bittorrent/torrent.cpp + bittorrent/torrentcontenthandler.cpp bittorrent/torrentcreatorthread.cpp bittorrent/torrentimpl.cpp bittorrent/torrentinfo.cpp diff --git a/src/base/base.pri b/src/base/base.pri index 393743167..7686090b3 100644 --- a/src/base/base.pri +++ b/src/base/base.pri @@ -34,6 +34,7 @@ HEADERS += \ $$PWD/bittorrent/speedmonitor.h \ $$PWD/bittorrent/torrent.h \ $$PWD/bittorrent/torrentcontentlayout.h \ + $$PWD/bittorrent/torrentcontenthandler.h \ $$PWD/bittorrent/torrentcreatorthread.h \ $$PWD/bittorrent/torrentimpl.h \ $$PWD/bittorrent/torrentinfo.h \ @@ -129,6 +130,7 @@ SOURCES += \ $$PWD/bittorrent/sessionimpl.cpp \ $$PWD/bittorrent/speedmonitor.cpp \ $$PWD/bittorrent/torrent.cpp \ + $$PWD/bittorrent/torrentcontenthandler.h \ $$PWD/bittorrent/torrentcreatorthread.cpp \ $$PWD/bittorrent/torrentimpl.cpp \ $$PWD/bittorrent/torrentinfo.cpp \ diff --git a/src/base/bittorrent/torrent.h b/src/base/bittorrent/torrent.h index 71291a1ae..52127bc42 100644 --- a/src/base/bittorrent/torrent.h +++ b/src/base/bittorrent/torrent.h @@ -37,7 +37,7 @@ #include "base/3rdparty/expected.hpp" #include "base/pathfwd.h" #include "base/tagset.h" -#include "abstractfilestorage.h" +#include "torrentcontenthandler.h" class QBitArray; class QByteArray; @@ -106,7 +106,7 @@ namespace BitTorrent uint qHash(TorrentState key, uint seed = 0); #endif - class Torrent : public QObject, public AbstractFileStorage + class Torrent : public TorrentContentHandler { Q_OBJECT Q_DISABLE_COPY_MOVE(Torrent) @@ -129,7 +129,7 @@ namespace BitTorrent static const qreal MAX_RATIO; static const int MAX_SEEDING_TIME; - using QObject::QObject; + using TorrentContentHandler::TorrentContentHandler; virtual InfoHash infoHash() const = 0; virtual QString name() const = 0; @@ -293,7 +293,6 @@ namespace BitTorrent virtual void forceReannounce(int index = -1) = 0; virtual void forceDHTAnnounce() = 0; virtual void forceRecheck() = 0; - virtual void prioritizeFiles(const QVector &priorities) = 0; virtual void setRatioLimit(qreal limit) = 0; virtual void setSeedingTimeLimit(int limit) = 0; virtual void setUploadLimit(int limit) = 0; @@ -321,16 +320,8 @@ namespace BitTorrent virtual void fetchPeerInfo(std::function)> resultHandler) const = 0; virtual void fetchURLSeeds(std::function)> resultHandler) const = 0; - virtual void fetchFilesProgress(std::function)> resultHandler) const = 0; virtual void fetchPieceAvailability(std::function)> resultHandler) const = 0; virtual void fetchDownloadingPieces(std::function resultHandler) const = 0; - /** - * @brief fraction of file pieces that are available at least from one peer - * - * This is not the same as torrrent availability, it is just a fraction of pieces - * that can be downloaded right now. It varies between 0 to 1. - */ - virtual void fetchAvailableFileFractions(std::function)> resultHandler) const = 0; TorrentID id() const; bool isResumed() const; diff --git a/src/gui/torrentcontenttreeview.h b/src/base/bittorrent/torrentcontenthandler.cpp similarity index 69% rename from src/gui/torrentcontenttreeview.h rename to src/base/bittorrent/torrentcontenthandler.cpp index f03be6a2f..62fe6ef57 100644 --- a/src/gui/torrentcontenttreeview.h +++ b/src/base/bittorrent/torrentcontenthandler.cpp @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2014 Ivan Sorokin + * Copyright (C) 2022 Vladimir Golovnev * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -26,29 +26,4 @@ * exception statement from your version. */ -#pragma once - -#include - -namespace BitTorrent -{ - class AbstractFileStorage; - class Torrent; - class TorrentInfo; -} - -class TorrentContentTreeView final : public QTreeView -{ - Q_OBJECT - Q_DISABLE_COPY_MOVE(TorrentContentTreeView) - -public: - explicit TorrentContentTreeView(QWidget *parent = nullptr); - void keyPressEvent(QKeyEvent *event) override; - - void renameSelectedFile(BitTorrent::AbstractFileStorage &fileStorage); - -private: - QModelIndex currentNameCell() const; - void wheelEvent(QWheelEvent *event) override; -}; +#include "torrentcontenthandler.h" diff --git a/src/base/bittorrent/torrentcontenthandler.h b/src/base/bittorrent/torrentcontenthandler.h new file mode 100644 index 000000000..6046265de --- /dev/null +++ b/src/base/bittorrent/torrentcontenthandler.h @@ -0,0 +1,62 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2022 Vladimir Golovnev + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#pragma once + +#include + +#include "base/pathfwd.h" +#include "abstractfilestorage.h" +#include "downloadpriority.h" + +namespace BitTorrent +{ + class TorrentContentHandler : public QObject, public AbstractFileStorage + { + public: + using QObject::QObject; + + virtual bool hasMetadata() const = 0; + virtual Path actualStorageLocation() const = 0; + virtual Path actualFilePath(int fileIndex) const = 0; + virtual QVector filePriorities() const = 0; + virtual QVector filesProgress() const = 0; + virtual void fetchFilesProgress(std::function)> resultHandler) const = 0; + /** + * @brief fraction of file pieces that are available at least from one peer + * + * This is not the same as torrrent availability, it is just a fraction of pieces + * that can be downloaded right now. It varies between 0 to 1. + */ + virtual QVector availableFileFractions() const = 0; + virtual void fetchAvailableFileFractions(std::function)> resultHandler) const = 0; + + virtual void prioritizeFiles(const QVector &priorities) = 0; + virtual void flushCache() const = 0; + }; +} diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 40c5ed7a8..c98eb5d8a 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -80,7 +80,6 @@ add_library(qbt_gui STATIC properties/pieceavailabilitybar.h properties/piecesbar.h properties/propertieswidget.h - properties/proplistdelegate.h properties/proptabbar.h properties/speedplotview.h properties/speedwidget.h @@ -106,11 +105,12 @@ add_library(qbt_gui STATIC tagfilterwidget.h torrentcategorydialog.h torrentcontentfiltermodel.h + torrentcontentitemdelegate.h torrentcontentmodel.h torrentcontentmodelfile.h torrentcontentmodelfolder.h torrentcontentmodelitem.h - torrentcontenttreeview.h + torrentcontentwidget.h torrentcreatordialog.h torrentoptionsdialog.h trackerentriesdialog.h @@ -164,7 +164,6 @@ add_library(qbt_gui STATIC properties/pieceavailabilitybar.cpp properties/piecesbar.cpp properties/propertieswidget.cpp - properties/proplistdelegate.cpp properties/proptabbar.cpp properties/speedplotview.cpp properties/speedwidget.cpp @@ -190,11 +189,12 @@ add_library(qbt_gui STATIC tagfilterwidget.cpp torrentcategorydialog.cpp torrentcontentfiltermodel.cpp + torrentcontentitemdelegate.cpp torrentcontentmodel.cpp torrentcontentmodelfile.cpp torrentcontentmodelfolder.cpp torrentcontentmodelitem.cpp - torrentcontenttreeview.cpp + torrentcontentwidget.cpp torrentcreatordialog.cpp torrentoptionsdialog.cpp trackerentriesdialog.cpp diff --git a/src/gui/addnewtorrentdialog.cpp b/src/gui/addnewtorrentdialog.cpp index dedd38f54..4219e934b 100644 --- a/src/gui/addnewtorrentdialog.cpp +++ b/src/gui/addnewtorrentdialog.cpp @@ -1,5 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2022 Vladimir Golovnev * Copyright (C) 2012 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -46,6 +47,7 @@ #include "base/bittorrent/magneturi.h" #include "base/bittorrent/session.h" #include "base/bittorrent/torrent.h" +#include "base/bittorrent/torrentcontenthandler.h" #include "base/bittorrent/torrentcontentlayout.h" #include "base/global.h" #include "base/net/downloadmanager.h" @@ -54,15 +56,10 @@ #include "base/utils/compare.h" #include "base/utils/fs.h" #include "base/utils/misc.h" -#include "autoexpandabledialog.h" #include "lineedit.h" -#include "properties/proplistdelegate.h" #include "raisedmessagebox.h" -#include "torrentcontentfiltermodel.h" -#include "torrentcontentmodel.h" #include "ui_addnewtorrentdialog.h" #include "uithememanager.h" -#include "utils.h" namespace { @@ -79,51 +76,6 @@ namespace return SettingsStorage::instance(); } - class FileStorageAdaptor final : public BitTorrent::AbstractFileStorage - { - public: - FileStorageAdaptor(const BitTorrent::TorrentInfo &torrentInfo, PathList &filePaths) - : m_torrentInfo {torrentInfo} - , m_filePaths {filePaths} - { - Q_ASSERT(filePaths.isEmpty() || (filePaths.size() == torrentInfo.filesCount())); - } - - int filesCount() const override - { - return m_torrentInfo.filesCount(); - } - - qlonglong fileSize(const int index) const override - { - Q_ASSERT((index >= 0) && (index < filesCount())); - return m_torrentInfo.fileSize(index); - } - - Path filePath(const int index) const override - { - Q_ASSERT((index >= 0) && (index < filesCount())); - return (m_filePaths.isEmpty() ? m_torrentInfo.filePath(index) : m_filePaths.at(index)); - } - - void renameFile(const int index, const Path &newFilePath) override - { - Q_ASSERT((index >= 0) && (index < filesCount())); - const Path currentFilePath = filePath(index); - if (currentFilePath == newFilePath) - return; - - if (m_filePaths.isEmpty()) - m_filePaths = m_torrentInfo.filePaths(); - - m_filePaths[index] = newFilePath; - } - - private: - const BitTorrent::TorrentInfo &m_torrentInfo; - PathList &m_filePaths; - }; - // savePath is a folder, not an absolute file path int indexOfPath(const FileSystemPathComboEdit *fsPathEdit, const Path &savePath) { @@ -180,6 +132,154 @@ namespace } } +class AddNewTorrentDialog::TorrentContentAdaptor final + : public BitTorrent::TorrentContentHandler +{ +public: + TorrentContentAdaptor(BitTorrent::TorrentInfo &torrentInfo, PathList &filePaths + , QVector &filePriorities) + : m_torrentInfo {torrentInfo} + , m_filePaths {filePaths} + , m_filePriorities {filePriorities} + { + Q_ASSERT(filePaths.isEmpty() || (filePaths.size() == torrentInfo.filesCount())); + + m_originalRootFolder = Path::findRootFolder(m_torrentInfo.filePaths()); + m_currentContentLayout = (m_originalRootFolder.isEmpty() + ? BitTorrent::TorrentContentLayout::NoSubfolder + : BitTorrent::TorrentContentLayout::Subfolder); + + if (!m_filePriorities.isEmpty()) + { +#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) + const int currentSize = m_filePriorities.size(); + m_filePriorities.resize(filesCount()); + for (int i = currentSize; i < filesCount(); ++i) + m_filePriorities[i] = BitTorrent::DownloadPriority::Normal; +#else + m_filePriorities.resize(filesCount(), BitTorrent::DownloadPriority::Normal); +#endif + } + } + + bool hasMetadata() const override + { + return m_torrentInfo.isValid(); + } + + int filesCount() const override + { + return m_torrentInfo.filesCount(); + } + + qlonglong fileSize(const int index) const override + { + Q_ASSERT((index >= 0) && (index < filesCount())); + return m_torrentInfo.fileSize(index); + } + + Path filePath(const int index) const override + { + Q_ASSERT((index >= 0) && (index < filesCount())); + return (m_filePaths.isEmpty() ? m_torrentInfo.filePath(index) : m_filePaths.at(index)); + } + + void renameFile(const int index, const Path &newFilePath) override + { + Q_ASSERT((index >= 0) && (index < filesCount())); + const Path currentFilePath = filePath(index); + if (currentFilePath == newFilePath) + return; + + if (m_filePaths.isEmpty()) + m_filePaths = m_torrentInfo.filePaths(); + + m_filePaths[index] = newFilePath; + } + + void applyContentLayout(const BitTorrent::TorrentContentLayout contentLayout) + { + Q_ASSERT(hasMetadata()); + Q_ASSERT(!m_filePaths.isEmpty()); + + const auto originalContentLayout = (m_originalRootFolder.isEmpty() + ? BitTorrent::TorrentContentLayout::NoSubfolder + : BitTorrent::TorrentContentLayout::Subfolder); + const auto newContentLayout = ((contentLayout == BitTorrent::TorrentContentLayout::Original) + ? originalContentLayout : contentLayout); + if (newContentLayout != m_currentContentLayout) + { + if (newContentLayout == BitTorrent::TorrentContentLayout::NoSubfolder) + { + Path::stripRootFolder(m_filePaths); + } + else + { + const auto rootFolder = ((originalContentLayout == BitTorrent::TorrentContentLayout::Subfolder) + ? m_originalRootFolder + : m_filePaths.at(0).removedExtension()); + Path::addRootFolder(m_filePaths, rootFolder); + } + + m_currentContentLayout = newContentLayout; + } + } + + QVector filePriorities() const override + { + return m_filePriorities.isEmpty() + ? QVector(filesCount(), BitTorrent::DownloadPriority::Normal) + : m_filePriorities; + } + + QVector filesProgress() const override + { + return QVector(filesCount(), 0); + } + + void fetchFilesProgress(std::function)> resultHandler) const override + { + resultHandler(filesProgress()); + } + + QVector availableFileFractions() const override + { + return QVector(filesCount(), 0); + } + + void fetchAvailableFileFractions(std::function)> resultHandler) const override + { + resultHandler(availableFileFractions()); + } + + void prioritizeFiles(const QVector &priorities) override + { + Q_ASSERT(priorities.size() == filesCount()); + m_filePriorities = priorities; + } + + Path actualStorageLocation() const override + { + return {}; + } + + Path actualFilePath([[maybe_unused]] int fileIndex) const override + { + return {}; + } + + void flushCache() const override + { + } + +private: + BitTorrent::TorrentInfo &m_torrentInfo; + PathList &m_filePaths; + QVector &m_filePriorities; + Path m_originalRootFolder; + BitTorrent::TorrentContentLayout m_currentContentLayout; +}; + const int AddNewTorrentDialog::minPathHistoryLength; const int AddNewTorrentDialog::maxPathHistoryLength; @@ -270,26 +370,37 @@ AddNewTorrentDialog::AddNewTorrentDialog(const BitTorrent::AddTorrentParams &inP m_ui->categoryComboBox->addItem(u""_qs); for (const QString &category : asConst(categories)) + { if (category != defaultCategory && category != m_torrentParams.category) m_ui->categoryComboBox->addItem(category); - - m_ui->contentTreeView->header()->setContextMenuPolicy(Qt::CustomContextMenu); - m_ui->contentTreeView->header()->setSortIndicator(0, Qt::AscendingOrder); - - connect(m_ui->contentTreeView->header(), &QWidget::customContextMenuRequested, this, &AddNewTorrentDialog::displayColumnHeaderMenu); + } // Torrent content filtering m_filterLine->setPlaceholderText(tr("Filter files...")); m_filterLine->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); - connect(m_filterLine, &LineEdit::textChanged, this, &AddNewTorrentDialog::handleFilterTextChanged); + connect(m_filterLine, &LineEdit::textChanged, m_ui->contentTreeView, &TorrentContentWidget::setFilterPattern); m_ui->contentFilterLayout->insertWidget(3, m_filterLine); loadState(); - // Signal / slots + connect(m_ui->doNotDeleteTorrentCheckBox, &QCheckBox::clicked, this, &AddNewTorrentDialog::doNotDeleteTorrentClicked); - QShortcut *editHotkey = new QShortcut(Qt::Key_F2, m_ui->contentTreeView, nullptr, nullptr, Qt::WidgetShortcut); - connect(editHotkey, &QShortcut::activated, this, &AddNewTorrentDialog::renameSelectedFile); - connect(m_ui->contentTreeView, &QAbstractItemView::doubleClicked, this, &AddNewTorrentDialog::renameSelectedFile); + + connect(m_ui->buttonSelectAll, &QPushButton::clicked, m_ui->contentTreeView, &TorrentContentWidget::checkAll); + connect(m_ui->buttonSelectNone, &QPushButton::clicked, m_ui->contentTreeView, &TorrentContentWidget::checkNone); + + if (const QByteArray state = m_storeTreeHeaderState; !state.isEmpty()) + m_ui->contentTreeView->header()->restoreState(state); + // Hide useless columns after loading the header state + m_ui->contentTreeView->hideColumn(TorrentContentWidget::Progress); + m_ui->contentTreeView->hideColumn(TorrentContentWidget::Remaining); + m_ui->contentTreeView->hideColumn(TorrentContentWidget::Availability); + m_ui->contentTreeView->setColumnsVisibilityMode(TorrentContentWidget::ColumnsVisibilityMode::Locked); + m_ui->contentTreeView->setDoubleClickAction(TorrentContentWidget::DoubleClickAction::Rename); + + m_ui->labelCommentData->setText(tr("Not Available", "This comment is unavailable")); + m_ui->labelDateData->setText(tr("Not Available", "This date is unavailable")); + + m_filterLine->blockSignals(true); // Default focus if (m_ui->comboTTM->currentIndex() == 0) // 0 is Manual mode @@ -298,43 +409,9 @@ AddNewTorrentDialog::AddNewTorrentDialog(const BitTorrent::AddTorrentParams &inP m_ui->categoryComboBox->setFocus(); } -void AddNewTorrentDialog::applyContentLayout() -{ - Q_ASSERT(hasMetadata()); - Q_ASSERT(!m_torrentParams.filePaths.isEmpty()); - - const auto originalContentLayout = (m_originalRootFolder.isEmpty() - ? BitTorrent::TorrentContentLayout::NoSubfolder - : BitTorrent::TorrentContentLayout::Subfolder); - const int currentIndex = m_ui->contentLayoutComboBox->currentIndex(); - const auto contentLayout = ((currentIndex == 0) - ? originalContentLayout - : static_cast(currentIndex)); - if (contentLayout != m_currentContentLayout) - { - PathList &filePaths = m_torrentParams.filePaths; - - if (contentLayout == BitTorrent::TorrentContentLayout::NoSubfolder) - { - Path::stripRootFolder(filePaths); - } - else - { - const auto rootFolder = ((originalContentLayout == BitTorrent::TorrentContentLayout::Subfolder) - ? m_originalRootFolder - : filePaths.at(0).removedExtension()); - Path::addRootFolder(filePaths, rootFolder); - } - - m_currentContentLayout = contentLayout; - } -} - AddNewTorrentDialog::~AddNewTorrentDialog() { saveState(); - - delete m_contentDelegate; delete m_ui; } @@ -389,7 +466,7 @@ void AddNewTorrentDialog::saveState() { m_storeDialogSize = size(); m_storeSplitterState = m_ui->splitter->saveState(); - if (m_contentModel) + if (hasMetadata()) m_storeTreeHeaderState = m_ui->contentTreeView->header()->saveState(); } @@ -537,7 +614,7 @@ bool AddNewTorrentDialog::loadMagnet(const BitTorrent::MagnetUri &magnetUri) const QString torrentName = magnetUri.name(); setWindowTitle(torrentName.isEmpty() ? tr("Magnet link") : torrentName); - setupTreeview(); + updateDiskSpaceLabel(); TMMChanged(m_ui->comboTTM->currentIndex()); BitTorrent::Session::instance()->downloadMetadata(magnetUri); @@ -565,19 +642,12 @@ void AddNewTorrentDialog::updateDiskSpaceLabel() if (hasMetadata()) { - if (m_contentModel) + const QVector &priorities = m_contentAdaptor->filePriorities(); + Q_ASSERT(priorities.size() == m_torrentInfo.filesCount()); + for (int i = 0; i < priorities.size(); ++i) { - const QVector priorities = m_contentModel->model()->getFilePriorities(); - Q_ASSERT(priorities.size() == m_torrentInfo.filesCount()); - for (int i = 0; i < priorities.size(); ++i) - { - if (priorities[i] > BitTorrent::DownloadPriority::Ignored) - torrentSize += m_torrentInfo.fileSize(i); - } - } - else - { - torrentSize = m_torrentInfo.totalSize(); + if (priorities[i] > BitTorrent::DownloadPriority::Ignored) + torrentSize += m_torrentInfo.fileSize(i); } } @@ -637,21 +707,9 @@ void AddNewTorrentDialog::contentLayoutChanged() if (!hasMetadata()) return; - const auto filePriorities = m_contentModel->model()->getFilePriorities(); - m_contentModel->model()->clear(); - - applyContentLayout(); - - m_contentModel->model()->setupModelData(FileStorageAdaptor(m_torrentInfo, m_torrentParams.filePaths)); - m_contentModel->model()->updateFilesPriorities(filePriorities); - - // Expand single-item folders recursively - QModelIndex currentIndex; - while (m_contentModel->rowCount(currentIndex) == 1) - { - currentIndex = m_contentModel->index(0, 0, currentIndex); - m_ui->contentTreeView->setExpanded(currentIndex, true); - } + const auto contentLayout = static_cast(m_ui->contentLayoutComboBox->currentIndex()); + m_contentAdaptor->applyContentLayout(contentLayout); + m_ui->contentTreeView->setContentHandler(m_contentAdaptor); // to cause reloading } void AddNewTorrentDialog::saveTorrentFile() @@ -755,125 +813,6 @@ void AddNewTorrentDialog::populateSavePaths() m_ui->groupBoxDownloadPath->blockSignals(false); } -void AddNewTorrentDialog::displayContentTreeMenu() -{ - const QModelIndexList selectedRows = m_ui->contentTreeView->selectionModel()->selectedRows(0); - - const auto applyPriorities = [this](const BitTorrent::DownloadPriority prio) - { - const QModelIndexList selectedRows = m_ui->contentTreeView->selectionModel()->selectedRows(0); - for (const QModelIndex &index : selectedRows) - { - m_contentModel->setData(index.sibling(index.row(), PRIORITY) - , static_cast(prio)); - } - }; - const auto applyPrioritiesByOrder = [this]() - { - // Equally distribute the selected items into groups and for each group assign - // a download priority that will apply to each item. The number of groups depends on how - // many "download priority" are available to be assigned - - const QModelIndexList selectedRows = m_ui->contentTreeView->selectionModel()->selectedRows(0); - - const qsizetype priorityGroups = 3; - const auto priorityGroupSize = std::max((selectedRows.length() / priorityGroups), 1); - - for (qsizetype i = 0; i < selectedRows.length(); ++i) - { - auto priority = BitTorrent::DownloadPriority::Ignored; - switch (i / priorityGroupSize) - { - case 0: - priority = BitTorrent::DownloadPriority::Maximum; - break; - case 1: - priority = BitTorrent::DownloadPriority::High; - break; - default: - case 2: - priority = BitTorrent::DownloadPriority::Normal; - break; - } - - const QModelIndex &index = selectedRows[i]; - m_contentModel->setData(index.sibling(index.row(), PRIORITY) - , static_cast(priority)); - } - }; - - QMenu *menu = new QMenu(this); - menu->setAttribute(Qt::WA_DeleteOnClose); - - if (selectedRows.size() == 1) - { - menu->addAction(UIThemeManager::instance()->getIcon(u"edit-rename"_qs), tr("Rename..."), this, &AddNewTorrentDialog::renameSelectedFile); - menu->addSeparator(); - - QMenu *priorityMenu = menu->addMenu(tr("Priority")); - priorityMenu->addAction(tr("Do not download"), priorityMenu, [applyPriorities]() - { - applyPriorities(BitTorrent::DownloadPriority::Ignored); - }); - priorityMenu->addAction(tr("Normal"), priorityMenu, [applyPriorities]() - { - applyPriorities(BitTorrent::DownloadPriority::Normal); - }); - priorityMenu->addAction(tr("High"), priorityMenu, [applyPriorities]() - { - applyPriorities(BitTorrent::DownloadPriority::High); - }); - priorityMenu->addAction(tr("Maximum"), priorityMenu, [applyPriorities]() - { - applyPriorities(BitTorrent::DownloadPriority::Maximum); - }); - priorityMenu->addSeparator(); - priorityMenu->addAction(tr("By shown file order"), priorityMenu, applyPrioritiesByOrder); - } - else - { - menu->addAction(tr("Do not download"), menu, [applyPriorities]() - { - applyPriorities(BitTorrent::DownloadPriority::Ignored); - }); - menu->addAction(tr("Normal priority"), menu, [applyPriorities]() - { - applyPriorities(BitTorrent::DownloadPriority::Normal); - }); - menu->addAction(tr("High priority"), menu, [applyPriorities]() - { - applyPriorities(BitTorrent::DownloadPriority::High); - }); - menu->addAction(tr("Maximum priority"), menu, [applyPriorities]() - { - applyPriorities(BitTorrent::DownloadPriority::Maximum); - }); - menu->addSeparator(); - menu->addAction(tr("Priority by shown file order"), menu, applyPrioritiesByOrder); - } - - menu->popup(QCursor::pos()); -} - -void AddNewTorrentDialog::displayColumnHeaderMenu() -{ - QMenu *menu = new QMenu(this); - menu->setAttribute(Qt::WA_DeleteOnClose); - menu->setToolTipsVisible(true); - - QAction *resizeAction = menu->addAction(tr("Resize columns"), this, [this]() - { - for (int i = 0, count = m_ui->contentTreeView->header()->count(); i < count; ++i) - { - if (!m_ui->contentTreeView->isColumnHidden(i)) - m_ui->contentTreeView->resizeColumnToContents(i); - } - }); - resizeAction->setToolTip(tr("Resize all non-hidden columns to the size of their contents")); - - menu->popup(QCursor::pos()); -} - void AddNewTorrentDialog::accept() { // TODO: Check if destination actually exists @@ -886,10 +825,6 @@ void AddNewTorrentDialog::accept() m_storeRememberLastSavePath = m_ui->checkBoxRememberLastSavePath->isChecked(); - // Save file priorities - if (m_contentModel) - m_torrentParams.filePriorities = m_contentModel->model()->getFilePriorities(); - m_torrentParams.addToQueueTop = m_ui->addToQueueTopCheckBox->isChecked(); m_torrentParams.addPaused = !m_ui->startTorrentCheckBox->isChecked(); m_torrentParams.stopCondition = m_ui->stopConditionComboBox->currentData().value(); @@ -972,82 +907,44 @@ void AddNewTorrentDialog::setMetadataProgressIndicator(bool visibleIndicator, co void AddNewTorrentDialog::setupTreeview() { - if (!hasMetadata()) - { - m_ui->labelCommentData->setText(tr("Not Available", "This comment is unavailable")); - m_ui->labelDateData->setText(tr("Not Available", "This date is unavailable")); - // Prevent crash if something is typed in the filter. m_contentModel is not initialized at this point - m_filterLine->blockSignals(true); - } - else - { - // Set dialog title - setWindowTitle(m_torrentInfo.name()); - - // Set torrent information - m_ui->labelCommentData->setText(Utils::Misc::parseHtmlLinks(m_torrentInfo.comment().toHtmlEscaped())); - m_ui->labelDateData->setText(!m_torrentInfo.creationDate().isNull() ? QLocale().toString(m_torrentInfo.creationDate(), QLocale::ShortFormat) : tr("Not available")); - - // Prepare content tree - m_contentModel = new TorrentContentFilterModel(this); - connect(m_contentModel->model(), &TorrentContentModel::filteredFilesChanged, this, &AddNewTorrentDialog::updateDiskSpaceLabel); - m_ui->contentTreeView->setModel(m_contentModel); - m_contentDelegate = new PropListDelegate(nullptr); - m_ui->contentTreeView->setItemDelegate(m_contentDelegate); - connect(m_ui->contentTreeView, &QAbstractItemView::clicked, m_ui->contentTreeView - , qOverload(&QAbstractItemView::edit)); - connect(m_ui->contentTreeView, &QWidget::customContextMenuRequested, this, &AddNewTorrentDialog::displayContentTreeMenu); - connect(m_ui->buttonSelectAll, &QPushButton::clicked, m_contentModel, &TorrentContentFilterModel::selectAll); - connect(m_ui->buttonSelectNone, &QPushButton::clicked, m_contentModel, &TorrentContentFilterModel::selectNone); - + Q_ASSERT(hasMetadata()); + if (Q_UNLIKELY(!hasMetadata())) + return; - if (m_torrentParams.filePaths.isEmpty()) - m_torrentParams.filePaths = m_torrentInfo.filePaths(); + // Set dialog title + setWindowTitle(m_torrentInfo.name()); - m_originalRootFolder = Path::findRootFolder(m_torrentInfo.filePaths()); - m_currentContentLayout = (m_originalRootFolder.isEmpty() - ? BitTorrent::TorrentContentLayout::NoSubfolder - : BitTorrent::TorrentContentLayout::Subfolder); - applyContentLayout(); + // Set torrent information + m_ui->labelCommentData->setText(Utils::Misc::parseHtmlLinks(m_torrentInfo.comment().toHtmlEscaped())); + m_ui->labelDateData->setText(!m_torrentInfo.creationDate().isNull() ? QLocale().toString(m_torrentInfo.creationDate(), QLocale::ShortFormat) : tr("Not available")); - // List files in torrent - m_contentModel->model()->setupModelData(FileStorageAdaptor(m_torrentInfo, m_torrentParams.filePaths)); - if (const QByteArray state = m_storeTreeHeaderState; !state.isEmpty()) - m_ui->contentTreeView->header()->restoreState(state); + if (m_torrentParams.filePaths.isEmpty()) + m_torrentParams.filePaths = m_torrentInfo.filePaths(); - m_filterLine->blockSignals(false); + m_contentAdaptor = new TorrentContentAdaptor(m_torrentInfo, m_torrentParams.filePaths, m_torrentParams.filePriorities); - // Hide useless columns after loading the header state - m_ui->contentTreeView->hideColumn(PROGRESS); - m_ui->contentTreeView->hideColumn(REMAINING); - m_ui->contentTreeView->hideColumn(AVAILABILITY); + const auto contentLayout = static_cast(m_ui->contentLayoutComboBox->currentIndex()); + m_contentAdaptor->applyContentLayout(contentLayout); - // Expand single-item folders recursively - QModelIndex currentIndex; - while (m_contentModel->rowCount(currentIndex) == 1) + if (BitTorrent::Session::instance()->isExcludedFileNamesEnabled()) + { + // Check file name blacklist for torrents that are manually added + QVector priorities = m_contentAdaptor->filePriorities(); + for (int i = 0; i < priorities.size(); ++i) { - currentIndex = m_contentModel->index(0, 0, currentIndex); - m_ui->contentTreeView->setExpanded(currentIndex, true); - } + if (priorities[i] == BitTorrent::DownloadPriority::Ignored) + continue; - if (BitTorrent::Session::instance()->isExcludedFileNamesEnabled()) - { - // Check file name blacklist for torrents that are manually added - QVector priorities = m_contentModel->model()->getFilePriorities(); - Q_ASSERT(priorities.size() == m_torrentInfo.filesCount()); + if (BitTorrent::Session::instance()->isFilenameExcluded(m_torrentInfo.filePath(i).filename())) + priorities[i] = BitTorrent::DownloadPriority::Ignored; + } - for (int i = 0; i < priorities.size(); ++i) - { - if (priorities[i] == BitTorrent::DownloadPriority::Ignored) - continue; + m_contentAdaptor->prioritizeFiles(priorities); + } - if (BitTorrent::Session::instance()->isFilenameExcluded(m_torrentInfo.filePath(i).filename())) - priorities[i] = BitTorrent::DownloadPriority::Ignored; - } + m_ui->contentTreeView->setContentHandler(m_contentAdaptor); - m_contentModel->model()->updateFilesPriorities(priorities); - } - } + m_filterLine->blockSignals(false); updateDiskSpaceLabel(); } @@ -1122,27 +1019,3 @@ void AddNewTorrentDialog::doNotDeleteTorrentClicked(bool checked) { m_torrentGuard->setAutoRemove(!checked); } - -void AddNewTorrentDialog::renameSelectedFile() -{ - if (hasMetadata()) - { - FileStorageAdaptor fileStorageAdaptor {m_torrentInfo, m_torrentParams.filePaths}; - m_ui->contentTreeView->renameSelectedFile(fileStorageAdaptor); - } -} - -void AddNewTorrentDialog::handleFilterTextChanged(const QString &filter) -{ - const QString pattern = Utils::String::wildcardToRegexPattern(filter); - m_contentModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption)); - if (filter.isEmpty()) - { - m_ui->contentTreeView->collapseAll(); - m_ui->contentTreeView->expand(m_contentModel->index(0, 0)); - } - else - { - m_ui->contentTreeView->expandAll(); - } -} diff --git a/src/gui/addnewtorrentdialog.h b/src/gui/addnewtorrentdialog.h index 3eeeb7624..2a830e14b 100644 --- a/src/gui/addnewtorrentdialog.h +++ b/src/gui/addnewtorrentdialog.h @@ -1,5 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2022 Vladimir Golovnev * Copyright (C) 2012 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -54,8 +55,6 @@ namespace Ui } class LineEdit; -class PropListDelegate; -class TorrentContentFilterModel; class TorrentFileGuard; class AddNewTorrentDialog final : public QDialog @@ -80,8 +79,6 @@ public: static void show(const QString &source, QWidget *parent); private slots: - void displayContentTreeMenu(); - void displayColumnHeaderMenu(); void updateDiskSpaceLabel(); void onSavePathChanged(const Path &newPath); void onDownloadPathChanged(const Path &newPath); @@ -92,16 +89,15 @@ private slots: void categoryChanged(int index); void contentLayoutChanged(); void doNotDeleteTorrentClicked(bool checked); - void renameSelectedFile(); - void handleFilterTextChanged(const QString &filter); void accept() override; void reject() override; private: + class TorrentContentAdaptor; + explicit AddNewTorrentDialog(const BitTorrent::AddTorrentParams &inParams, QWidget *parent); - void applyContentLayout(); bool loadTorrentFile(const QString &source); bool loadTorrentImpl(); bool loadMagnet(const BitTorrent::MagnetUri &magnetUri); @@ -116,12 +112,9 @@ private: void showEvent(QShowEvent *event) override; Ui::AddNewTorrentDialog *m_ui = nullptr; - TorrentContentFilterModel *m_contentModel = nullptr; - PropListDelegate *m_contentDelegate = nullptr; + TorrentContentAdaptor *m_contentAdaptor = nullptr; BitTorrent::MagnetUri m_magnetURI; BitTorrent::TorrentInfo m_torrentInfo; - Path m_originalRootFolder; - BitTorrent::TorrentContentLayout m_currentContentLayout; int m_savePathIndex = -1; int m_downloadPathIndex = -1; bool m_useDownloadPath = false; diff --git a/src/gui/addnewtorrentdialog.ui b/src/gui/addnewtorrentdialog.ui index dac5358b4..f28a71032 100644 --- a/src/gui/addnewtorrentdialog.ui +++ b/src/gui/addnewtorrentdialog.ui @@ -510,7 +510,7 @@ - + 1 @@ -612,9 +612,9 @@ - TorrentContentTreeView + TorrentContentWidget QTreeView -
gui/torrentcontenttreeview.h
+
gui/torrentcontentwidget.h
FileSystemPathComboEdit diff --git a/src/gui/gui.pri b/src/gui/gui.pri index 15e5fdfd4..a07251a8a 100644 --- a/src/gui/gui.pri +++ b/src/gui/gui.pri @@ -39,7 +39,6 @@ HEADERS += \ $$PWD/properties/pieceavailabilitybar.h \ $$PWD/properties/piecesbar.h \ $$PWD/properties/propertieswidget.h \ - $$PWD/properties/proplistdelegate.h \ $$PWD/properties/proptabbar.h \ $$PWD/properties/speedplotview.h \ $$PWD/properties/speedwidget.h \ @@ -65,11 +64,12 @@ HEADERS += \ $$PWD/tagfilterwidget.h \ $$PWD/torrentcategorydialog.h \ $$PWD/torrentcontentfiltermodel.h \ + $$PWD/torrentcontentitemdelegate.h \ $$PWD/torrentcontentmodel.h \ $$PWD/torrentcontentmodelfile.h \ $$PWD/torrentcontentmodelfolder.h \ $$PWD/torrentcontentmodelitem.h \ - $$PWD/torrentcontenttreeview.h \ + $$PWD/torrentcontentwidget.h \ $$PWD/torrentcreatordialog.h \ $$PWD/torrentoptionsdialog.h \ $$PWD/trackerentriesdialog.h \ @@ -123,7 +123,6 @@ SOURCES += \ $$PWD/properties/pieceavailabilitybar.cpp \ $$PWD/properties/piecesbar.cpp \ $$PWD/properties/propertieswidget.cpp \ - $$PWD/properties/proplistdelegate.cpp \ $$PWD/properties/proptabbar.cpp \ $$PWD/properties/speedplotview.cpp \ $$PWD/properties/speedwidget.cpp \ @@ -149,11 +148,12 @@ SOURCES += \ $$PWD/tagfilterwidget.cpp \ $$PWD/torrentcategorydialog.cpp \ $$PWD/torrentcontentfiltermodel.cpp \ + $$PWD/torrentcontentitemdelegate.cpp \ $$PWD/torrentcontentmodel.cpp \ $$PWD/torrentcontentmodelfile.cpp \ $$PWD/torrentcontentmodelfolder.cpp \ $$PWD/torrentcontentmodelitem.cpp \ - $$PWD/torrentcontenttreeview.cpp \ + $$PWD/torrentcontentwidget.cpp \ $$PWD/torrentcreatordialog.cpp \ $$PWD/torrentoptionsdialog.cpp \ $$PWD/trackerentriesdialog.cpp \ diff --git a/src/gui/properties/propertieswidget.cpp b/src/gui/properties/propertieswidget.cpp index 5612c214e..7baa15b91 100644 --- a/src/gui/properties/propertieswidget.cpp +++ b/src/gui/properties/propertieswidget.cpp @@ -1,5 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2022 Vladimir Golovnev * Copyright (C) 2006 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -31,9 +32,9 @@ #include #include #include -#include #include #include +#include #include #include #include @@ -41,7 +42,6 @@ #include #include -#include "base/bittorrent/downloadpriority.h" #include "base/bittorrent/infohash.h" #include "base/bittorrent/session.h" #include "base/bittorrent/torrent.h" @@ -49,32 +49,23 @@ #include "base/preferences.h" #include "base/types.h" #include "base/unicodestrings.h" -#include "base/utils/fs.h" #include "base/utils/misc.h" #include "base/utils/string.h" #include "gui/autoexpandabledialog.h" #include "gui/lineedit.h" -#include "gui/raisedmessagebox.h" -#include "gui/torrentcontentfiltermodel.h" -#include "gui/torrentcontentmodel.h" #include "gui/uithememanager.h" #include "gui/utils.h" #include "downloadedpiecesbar.h" #include "peerlistwidget.h" #include "pieceavailabilitybar.h" -#include "proplistdelegate.h" #include "proptabbar.h" #include "speedwidget.h" #include "trackerlistwidget.h" #include "ui_propertieswidget.h" -#ifdef Q_OS_MACOS -#include "gui/macutilities.h" -#endif - PropertiesWidget::PropertiesWidget(QWidget *parent) : QWidget(parent) - , m_ui(new Ui::PropertiesWidget()) + , m_ui {new Ui::PropertiesWidget} { m_ui->setupUi(this); #ifndef Q_OS_MACOS @@ -83,40 +74,23 @@ PropertiesWidget::PropertiesWidget(QWidget *parent) m_state = VISIBLE; - // Files list - m_ui->filesList->header()->setContextMenuPolicy(Qt::CustomContextMenu); - - // Set Properties list model - m_propListModel = new TorrentContentFilterModel(this); - m_ui->filesList->setModel(m_propListModel); - m_propListDelegate = new PropListDelegate(this); - m_ui->filesList->setItemDelegate(m_propListDelegate); - m_ui->filesList->setSortingEnabled(true); - // Torrent content filtering m_contentFilterLine = new LineEdit(this); m_contentFilterLine->setPlaceholderText(tr("Filter files...")); m_contentFilterLine->setFixedWidth(300); - connect(m_contentFilterLine, &LineEdit::textChanged, this, &PropertiesWidget::filterText); + connect(m_contentFilterLine, &LineEdit::textChanged, m_ui->filesList, &TorrentContentWidget::setFilterPattern); m_ui->contentFilterLayout->insertWidget(3, m_contentFilterLine); + m_ui->filesList->setDoubleClickAction(TorrentContentWidget::DoubleClickAction::Open); + // SIGNAL/SLOTS - connect(m_ui->selectAllButton, &QPushButton::clicked, m_propListModel, &TorrentContentFilterModel::selectAll); - connect(m_ui->selectNoneButton, &QPushButton::clicked, m_propListModel, &TorrentContentFilterModel::selectNone); - connect(m_propListModel, &TorrentContentFilterModel::filteredFilesChanged, this, &PropertiesWidget::filteredFilesChanged); + connect(m_ui->selectAllButton, &QPushButton::clicked, m_ui->filesList, &TorrentContentWidget::checkAll); + connect(m_ui->selectNoneButton, &QPushButton::clicked, m_ui->filesList, &TorrentContentWidget::checkNone); connect(m_ui->listWebSeeds, &QWidget::customContextMenuRequested, this, &PropertiesWidget::displayWebSeedListMenu); - connect(m_propListDelegate, &PropListDelegate::filteredFilesChanged, this, &PropertiesWidget::filteredFilesChanged); connect(m_ui->stackedProperties, &QStackedWidget::currentChanged, this, &PropertiesWidget::loadDynamicData); connect(BitTorrent::Session::instance(), &BitTorrent::Session::torrentSavePathChanged, this, &PropertiesWidget::updateSavePath); connect(BitTorrent::Session::instance(), &BitTorrent::Session::torrentMetadataReceived, this, &PropertiesWidget::updateTorrentInfos); - connect(m_ui->filesList, &QAbstractItemView::clicked - , m_ui->filesList, qOverload(&QAbstractItemView::edit)); - connect(m_ui->filesList, &QWidget::customContextMenuRequested, this, &PropertiesWidget::displayFilesListMenu); - connect(m_ui->filesList, &QAbstractItemView::doubleClicked, this, &PropertiesWidget::openItem); - connect(m_ui->filesList->header(), &QWidget::customContextMenuRequested, this, &PropertiesWidget::displayColumnHeaderMenu); - connect(m_ui->filesList->header(), &QHeaderView::sectionMoved, this, &PropertiesWidget::saveSettings); - connect(m_ui->filesList->header(), &QHeaderView::sectionResized, this, &PropertiesWidget::saveSettings); - connect(m_ui->filesList->header(), &QHeaderView::sortIndicatorChanged, this, &PropertiesWidget::saveSettings); + connect(m_ui->filesList, &TorrentContentWidget::stateChanged, this, &PropertiesWidget::saveSettings); // set bar height relative to screen dpi const int barHeight = 18; @@ -162,13 +136,6 @@ PropertiesWidget::PropertiesWidget(QWidget *parent) connect(deleteWebSeedsHotkey, &QShortcut::activated, this, &PropertiesWidget::deleteSelectedUrlSeeds); connect(m_ui->listWebSeeds, &QListWidget::doubleClicked, this, &PropertiesWidget::editWebSeed); - const auto *renameFileHotkey = new QShortcut(Qt::Key_F2, m_ui->filesList, nullptr, nullptr, Qt::WidgetShortcut); - connect(renameFileHotkey, &QShortcut::activated, this, [this]() { m_ui->filesList->renameSelectedFile(*m_torrent); }); - const auto *openFileHotkeyReturn = new QShortcut(Qt::Key_Return, m_ui->filesList, nullptr, nullptr, Qt::WidgetShortcut); - connect(openFileHotkeyReturn, &QShortcut::activated, this, &PropertiesWidget::openSelectedFile); - const auto *openFileHotkeyEnter = new QShortcut(Qt::Key_Enter, m_ui->filesList, nullptr, nullptr, Qt::WidgetShortcut); - connect(openFileHotkeyEnter, &QShortcut::activated, this, &PropertiesWidget::openSelectedFile); - configure(); connect(Preferences::instance(), &Preferences::changed, this, &PropertiesWidget::configure); } @@ -179,47 +146,6 @@ PropertiesWidget::~PropertiesWidget() delete m_ui; } -void PropertiesWidget::displayColumnHeaderMenu() -{ - QMenu *menu = new QMenu(this); - menu->setAttribute(Qt::WA_DeleteOnClose); - menu->setTitle(tr("Column visibility")); - menu->setToolTipsVisible(true); - - for (int i = 0; i < TorrentContentModelItem::TreeItemColumns::NB_COL; ++i) - { - const auto columnName = m_propListModel->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString(); - QAction *action = menu->addAction(columnName, this, [this, i](const bool checked) - { - m_ui->filesList->setColumnHidden(i, !checked); - - if (checked && (m_ui->filesList->columnWidth(i) <= 5)) - m_ui->filesList->resizeColumnToContents(i); - - saveSettings(); - }); - action->setCheckable(true); - action->setChecked(!m_ui->filesList->isColumnHidden(i)); - - if (i == TorrentContentModelItem::TreeItemColumns::COL_NAME) - action->setEnabled(false); - } - - menu->addSeparator(); - QAction *resizeAction = menu->addAction(tr("Resize columns"), this, [this]() - { - for (int i = 0, count = m_ui->filesList->header()->count(); i < count; ++i) - { - if (!m_ui->filesList->isColumnHidden(i)) - m_ui->filesList->resizeColumnToContents(i); - } - saveSettings(); - }); - resizeAction->setToolTip(tr("Resize all non-hidden columns to the size of their contents")); - - menu->popup(QCursor::pos()); -} - void PropertiesWidget::showPiecesAvailability(bool show) { m_ui->labelPiecesAvailability->setVisible(show); @@ -309,7 +235,6 @@ void PropertiesWidget::clear() m_piecesAvailability->clear(); m_peerList->clear(); m_contentFilterLine->clear(); - m_propListModel->model()->clear(); } BitTorrent::Torrent *PropertiesWidget::getCurrentTorrent() const @@ -356,14 +281,15 @@ void PropertiesWidget::loadTorrentInfos(BitTorrent::Torrent *const torrent) m_torrent = torrent; m_downloadedPieces->setTorrent(m_torrent); m_piecesAvailability->setTorrent(m_torrent); - if (!m_torrent) return; + m_ui->filesList->setContentHandler(m_torrent); + if (!m_torrent) + return; // Save path updateSavePath(m_torrent); // Info hashes m_ui->labelInfohash1Val->setText(m_torrent->infoHash().v1().isValid() ? m_torrent->infoHash().v1().toString() : tr("N/A")); m_ui->labelInfohash2Val->setText(m_torrent->infoHash().v2().isValid() ? m_torrent->infoHash().v2().toString() : tr("N/A")); - m_propListModel->model()->clear(); if (m_torrent->hasMetadata()) { // Creation date @@ -545,55 +471,7 @@ void PropertiesWidget::loadDynamicData() m_peerList->loadPeers(m_torrent); break; case PropTabBar::FilesTab: - // Files progress - if (m_torrent->hasMetadata()) - { - qDebug("Updating priorities in files tab"); - m_ui->filesList->setUpdatesEnabled(false); - - using TorrentPtr = QPointer; - m_torrent->fetchFilesProgress([this, torrent = TorrentPtr(m_torrent)](const QVector &filesProgress) - { - if (torrent == m_torrent) - m_propListModel->model()->updateFilesProgress(filesProgress); - }); - - m_torrent->fetchAvailableFileFractions([this, torrent = TorrentPtr(m_torrent)](const QVector &availableFileFractions) - { - if (torrent == m_torrent) - m_propListModel->model()->updateFilesAvailability(availableFileFractions); - }); - - // Load torrent content if not yet done so - const bool isContentInitialized = m_propListModel->model()->hasIndex(0, 0); - if (!isContentInitialized) - { - // List files in torrent - m_propListModel->model()->setupModelData(*m_torrent); - // Load file priorities - m_propListModel->model()->updateFilesPriorities(m_torrent->filePriorities()); - - // Expand single-item folders recursively. - // This will trigger sorting and filtering so do it after all relevant data is loaded. - QModelIndex currentIndex; - while (m_propListModel->rowCount(currentIndex) == 1) - { - currentIndex = m_propListModel->index(0, 0, currentIndex); - m_ui->filesList->setExpanded(currentIndex, true); - } - } - else - { - // Torrent content was loaded already, only make some updates - - // XXX: We don't update file priorities regularly for performance - // reasons. This means that priorities will not be updated if - // set from the Web UI. - // m_propListModel->model()->updateFilesPriorities(m_torrent->filePriorities()); - } - - m_ui->filesList->setUpdatesEnabled(true); - } + m_ui->filesList->refresh(); break; default:; } @@ -621,149 +499,6 @@ void PropertiesWidget::loadUrlSeeds() }); } -Path PropertiesWidget::getFullPath(const QModelIndex &index) const -{ - if (m_propListModel->itemType(index) == TorrentContentModelItem::FileType) - { - const int fileIdx = m_propListModel->getFileIndex(index); - const Path fullPath = m_torrent->actualStorageLocation() / m_torrent->actualFilePath(fileIdx); - return fullPath; - } - - // folder type - const QModelIndex nameIndex {index.sibling(index.row(), TorrentContentModelItem::COL_NAME)}; - Path folderPath {nameIndex.data().toString()}; - for (QModelIndex modelIdx = m_propListModel->parent(nameIndex); modelIdx.isValid(); modelIdx = modelIdx.parent()) - folderPath = Path(modelIdx.data().toString()) / folderPath; - - const Path fullPath = m_torrent->actualStorageLocation() / folderPath; - return fullPath; -} - -void PropertiesWidget::openItem(const QModelIndex &index) const -{ - if (!index.isValid()) - return; - - m_torrent->flushCache(); // Flush data - Utils::Gui::openPath(getFullPath(index)); -} - -void PropertiesWidget::openParentFolder(const QModelIndex &index) const -{ - const Path path = getFullPath(index); - m_torrent->flushCache(); // Flush data -#ifdef Q_OS_MACOS - MacUtils::openFiles({path}); -#else - Utils::Gui::openFolderSelect(path); -#endif -} - -void PropertiesWidget::displayFilesListMenu() -{ - if (!m_torrent) return; - - const QModelIndexList selectedRows = m_ui->filesList->selectionModel()->selectedRows(0); - if (selectedRows.empty()) return; - - QMenu *menu = new QMenu(this); - menu->setAttribute(Qt::WA_DeleteOnClose); - - if (selectedRows.size() == 1) - { - const QModelIndex index = selectedRows[0]; - - menu->addAction(UIThemeManager::instance()->getIcon(u"folder-documents"_qs), tr("Open") - , this, [this, index]() { openItem(index); }); - menu->addAction(UIThemeManager::instance()->getIcon(u"directory"_qs), tr("Open containing folder") - , this, [this, index]() { openParentFolder(index); }); - menu->addAction(UIThemeManager::instance()->getIcon(u"edit-rename"_qs), tr("Rename...") - , this, [this]() { m_ui->filesList->renameSelectedFile(*m_torrent); }); - menu->addSeparator(); - } - - const auto applyPriorities = [this](const BitTorrent::DownloadPriority prio) - { - const QModelIndexList selectedRows = m_ui->filesList->selectionModel()->selectedRows(0); - for (const QModelIndex &index : selectedRows) - { - m_propListModel->setData(index.sibling(index.row(), PRIORITY) - , static_cast(prio)); - } - - // Save changes - this->applyPriorities(); - }; - - QMenu *subMenu = menu->addMenu(tr("Priority")); - - subMenu->addAction(tr("Do not download"), subMenu, [applyPriorities]() - { - applyPriorities(BitTorrent::DownloadPriority::Ignored); - }); - subMenu->addAction(tr("Normal"), subMenu, [applyPriorities]() - { - applyPriorities(BitTorrent::DownloadPriority::Normal); - }); - subMenu->addAction(tr("High"), subMenu, [applyPriorities]() - { - applyPriorities(BitTorrent::DownloadPriority::High); - }); - subMenu->addAction(tr("Maximum"), subMenu, [applyPriorities]() - { - applyPriorities(BitTorrent::DownloadPriority::Maximum); - }); - subMenu->addSeparator(); - subMenu->addAction(tr("By shown file order"), subMenu, [this]() - { - // Equally distribute the selected items into groups and for each group assign - // a download priority that will apply to each item. The number of groups depends on how - // many "download priority" are available to be assigned - - const QModelIndexList selectedRows = m_ui->filesList->selectionModel()->selectedRows(0); - - const qsizetype priorityGroups = 3; - const auto priorityGroupSize = std::max((selectedRows.length() / priorityGroups), 1); - - for (qsizetype i = 0; i < selectedRows.length(); ++i) - { - auto priority = BitTorrent::DownloadPriority::Ignored; - switch (i / priorityGroupSize) - { - case 0: - priority = BitTorrent::DownloadPriority::Maximum; - break; - case 1: - priority = BitTorrent::DownloadPriority::High; - break; - default: - case 2: - priority = BitTorrent::DownloadPriority::Normal; - break; - } - - const QModelIndex &index = selectedRows[i]; - m_propListModel->setData(index.sibling(index.row(), PRIORITY) - , static_cast(priority)); - - // Save changes - this->applyPriorities(); - } - }); - - // The selected torrent might have disappeared during exec() - // so we just close menu when an appropriate model is reset - connect(m_ui->filesList->model(), &QAbstractItemModel::modelAboutToBeReset - , menu, [menu]() - { - menu->setActiveAction(nullptr); - menu->close(); - }); - - menu->popup(QCursor::pos()); -} - void PropertiesWidget::displayWebSeedListMenu() { if (!m_torrent) return; @@ -789,14 +524,6 @@ void PropertiesWidget::displayWebSeedListMenu() menu->popup(QCursor::pos()); } -void PropertiesWidget::openSelectedFile() -{ - const QModelIndexList selectedIndexes = m_ui->filesList->selectionModel()->selectedRows(0); - if (selectedIndexes.size() != 1) - return; - openItem(selectedIndexes.first()); -} - void PropertiesWidget::configure() { // Speed widget @@ -845,9 +572,7 @@ void PropertiesWidget::askWebSeed() qDebug("Adding %s web seed", qUtf8Printable(urlSeed)); if (!m_ui->listWebSeeds->findItems(urlSeed, Qt::MatchFixedString).empty()) { - QMessageBox::warning(this, u"qBittorrent"_qs, - tr("This URL seed is already in the list."), - QMessageBox::Ok); + QMessageBox::warning(this, u"qBittorrent"_qs, tr("This URL seed is already in the list."), QMessageBox::Ok); return; } if (m_torrent) @@ -909,29 +634,3 @@ void PropertiesWidget::editWebSeed() m_torrent->addUrlSeeds({newSeed}); loadUrlSeeds(); } - -void PropertiesWidget::applyPriorities() -{ - m_torrent->prioritizeFiles(m_propListModel->model()->getFilePriorities()); -} - -void PropertiesWidget::filteredFilesChanged() -{ - if (m_torrent) - applyPriorities(); -} - -void PropertiesWidget::filterText(const QString &filter) -{ - const QString pattern = Utils::String::wildcardToRegexPattern(filter); - m_propListModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption)); - if (filter.isEmpty()) - { - m_ui->filesList->collapseAll(); - m_ui->filesList->expand(m_propListModel->index(0, 0)); - } - else - { - m_ui->filesList->expandAll(); - } -} diff --git a/src/gui/properties/propertieswidget.h b/src/gui/properties/propertieswidget.h index 8dcff78f4..d28e74e70 100644 --- a/src/gui/properties/propertieswidget.h +++ b/src/gui/properties/propertieswidget.h @@ -1,5 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2022 Vladimir Golovnev * Copyright (C) 2006 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -40,9 +41,7 @@ class DownloadedPiecesBar; class LineEdit; class PeerListWidget; class PieceAvailabilityBar; -class PropListDelegate; class PropTabBar; -class TorrentContentFilterModel; class TrackerListWidget; namespace BitTorrent @@ -83,7 +82,6 @@ public slots: void readSettings(); void saveSettings(); void reloadPreferences(); - void openItem(const QModelIndex &index) const; void loadTrackers(BitTorrent::Torrent *const torrent); protected slots: @@ -93,30 +91,20 @@ protected slots: void deleteSelectedUrlSeeds(); void copySelectedWebSeedsToClipboard() const; void editWebSeed(); - void displayFilesListMenu(); void displayWebSeedListMenu(); - void filteredFilesChanged(); void showPiecesDownloaded(bool show); void showPiecesAvailability(bool show); - void openSelectedFile(); private slots: void configure(); - void displayColumnHeaderMenu(); - void filterText(const QString &filter); void updateSavePath(BitTorrent::Torrent *const torrent); private: QPushButton *getButtonFromIndex(int index); - void applyPriorities(); - void openParentFolder(const QModelIndex &index) const; - Path getFullPath(const QModelIndex &index) const; Ui::PropertiesWidget *m_ui = nullptr; BitTorrent::Torrent *m_torrent = nullptr; SlideState m_state; - TorrentContentFilterModel *m_propListModel = nullptr; - PropListDelegate *m_propListDelegate = nullptr; PeerListWidget *m_peerList = nullptr; TrackerListWidget *m_trackerList = nullptr; QWidget *m_speedWidget = nullptr; diff --git a/src/gui/properties/propertieswidget.ui b/src/gui/properties/propertieswidget.ui index 63dee3bc4..01123af83 100644 --- a/src/gui/properties/propertieswidget.ui +++ b/src/gui/properties/propertieswidget.ui @@ -1082,7 +1082,7 @@
- + Qt::CustomContextMenu @@ -1121,9 +1121,9 @@ - TorrentContentTreeView + TorrentContentWidget QTreeView -
gui/torrentcontenttreeview.h
+
gui/torrentcontentwidget.h
diff --git a/src/gui/torrentcontentfiltermodel.cpp b/src/gui/torrentcontentfiltermodel.cpp index 9ff5043a1..357ba217b 100644 --- a/src/gui/torrentcontentfiltermodel.cpp +++ b/src/gui/torrentcontentfiltermodel.cpp @@ -1,5 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2022 Vladimir Golovnev * Copyright (C) 2006-2012 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -32,10 +33,7 @@ TorrentContentFilterModel::TorrentContentFilterModel(QObject *parent) : QSortFilterProxyModel(parent) - , m_model(new TorrentContentModel(this)) { - connect(m_model, &TorrentContentModel::filteredFilesChanged, this, &TorrentContentFilterModel::filteredFilesChanged); - setSourceModel(m_model); // Filter settings setFilterKeyColumn(TorrentContentModelItem::COL_NAME); setFilterRole(TorrentContentModel::UnderlyingDataRole); @@ -44,9 +42,10 @@ TorrentContentFilterModel::TorrentContentFilterModel(QObject *parent) setSortRole(TorrentContentModel::UnderlyingDataRole); } -TorrentContentModel *TorrentContentFilterModel::model() const +void TorrentContentFilterModel::setSourceModel(TorrentContentModel *model) { - return m_model; + m_model = model; + QSortFilterProxyModel::setSourceModel(m_model); } TorrentContentModelItem::ItemType TorrentContentFilterModel::itemType(const QModelIndex &index) const @@ -61,10 +60,12 @@ int TorrentContentFilterModel::getFileIndex(const QModelIndex &index) const QModelIndex TorrentContentFilterModel::parent(const QModelIndex &child) const { - if (!child.isValid()) return {}; + if (!child.isValid()) + return {}; QModelIndex sourceParent = m_model->parent(mapToSource(child)); - if (!sourceParent.isValid()) return {}; + if (!sourceParent.isValid()) + return {}; return mapFromSource(sourceParent); } @@ -85,7 +86,7 @@ bool TorrentContentFilterModel::lessThan(const QModelIndex &left, const QModelIn switch (sortColumn()) { case TorrentContentModelItem::COL_NAME: - { + { const TorrentContentModelItem::ItemType leftType = m_model->itemType(m_model->index(left.row(), 0, left.parent())); const TorrentContentModelItem::ItemType rightType = m_model->itemType(m_model->index(right.row(), 0, right.parent())); @@ -95,6 +96,7 @@ bool TorrentContentFilterModel::lessThan(const QModelIndex &left, const QModelIn const QString strR = right.data().toString(); return m_naturalLessThan(strL, strR); } + if ((leftType == TorrentContentModelItem::FolderType) && (sortOrder() == Qt::AscendingOrder)) { return true; @@ -102,23 +104,12 @@ bool TorrentContentFilterModel::lessThan(const QModelIndex &left, const QModelIn return false; } + default: return QSortFilterProxyModel::lessThan(left, right); }; } -void TorrentContentFilterModel::selectAll() -{ - for (int i = 0; i < rowCount(); ++i) - setData(index(i, TorrentContentModelItem::COL_NAME), Qt::Checked, Qt::CheckStateRole); -} - -void TorrentContentFilterModel::selectNone() -{ - for (int i = 0; i < rowCount(); ++i) - setData(index(i, TorrentContentModelItem::COL_NAME), Qt::Unchecked, Qt::CheckStateRole); -} - bool TorrentContentFilterModel::hasFiltered(const QModelIndex &folder) const { // this should be called only with folders @@ -126,6 +117,7 @@ bool TorrentContentFilterModel::hasFiltered(const QModelIndex &folder) const QString name = folder.data().toString(); if (name.contains(filterRegularExpression())) return true; + for (int child = 0; child < m_model->rowCount(folder); ++child) { QModelIndex childIndex = m_model->index(child, 0, folder); @@ -133,8 +125,10 @@ bool TorrentContentFilterModel::hasFiltered(const QModelIndex &folder) const { if (hasFiltered(childIndex)) return true; + continue; } + name = childIndex.data().toString(); if (name.contains(filterRegularExpression())) return true; diff --git a/src/gui/torrentcontentfiltermodel.h b/src/gui/torrentcontentfiltermodel.h index a70bcab14..2f31823aa 100644 --- a/src/gui/torrentcontentfiltermodel.h +++ b/src/gui/torrentcontentfiltermodel.h @@ -1,5 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2022 Vladimir Golovnev * Copyright (C) 2006-2012 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -41,25 +42,17 @@ class TorrentContentFilterModel final : public QSortFilterProxyModel Q_DISABLE_COPY_MOVE(TorrentContentFilterModel) public: - TorrentContentFilterModel(QObject *parent = nullptr); + explicit TorrentContentFilterModel(QObject *parent = nullptr); - TorrentContentModel *model() const; + void setSourceModel(TorrentContentModel *model); TorrentContentModelItem::ItemType itemType(const QModelIndex &index) const; int getFileIndex(const QModelIndex &index) const; QModelIndex parent(const QModelIndex &child) const override; -public slots: - void selectAll(); - void selectNone(); - -signals: - void filteredFilesChanged(); - -protected: +private: + using QSortFilterProxyModel::setSourceModel; bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; - -private: bool hasFiltered(const QModelIndex &folder) const; TorrentContentModel *m_model = nullptr; diff --git a/src/gui/properties/proplistdelegate.cpp b/src/gui/torrentcontentitemdelegate.cpp similarity index 78% rename from src/gui/properties/proplistdelegate.cpp rename to src/gui/torrentcontentitemdelegate.cpp index 8056224b8..4bc015301 100644 --- a/src/gui/properties/proplistdelegate.cpp +++ b/src/gui/torrentcontentitemdelegate.cpp @@ -1,5 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2022 Vladimir Golovnev * Copyright (C) 2006 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -26,7 +27,7 @@ * exception statement from your version. */ -#include "proplistdelegate.h" +#include "torrentcontentitemdelegate.h" #include #include @@ -36,15 +37,13 @@ #include "base/bittorrent/downloadpriority.h" #include "base/bittorrent/torrent.h" #include "gui/torrentcontentmodel.h" -#include "propertieswidget.h" -PropListDelegate::PropListDelegate(PropertiesWidget *properties) - : QStyledItemDelegate {properties} - , m_properties {properties} +TorrentContentItemDelegate::TorrentContentItemDelegate(QWidget *parent) + : QStyledItemDelegate(parent) { } -void PropListDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +void TorrentContentItemDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const { auto *combobox = static_cast(editor); // Set combobox index @@ -69,18 +68,11 @@ void PropListDelegate::setEditorData(QWidget *editor, const QModelIndex &index) } } -QWidget *PropListDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &, const QModelIndex &index) const +QWidget *TorrentContentItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &, const QModelIndex &index) const { - if (index.column() != PRIORITY) + if (index.column() != TorrentContentModelItem::COL_PRIO) return nullptr; - if (m_properties) - { - const BitTorrent::Torrent *torrent = m_properties->getCurrentTorrent(); - if (!torrent || !torrent->hasMetadata()) - return nullptr; - } - auto *editor = new QComboBox(parent); editor->setFocusPolicy(Qt::StrongFocus); editor->addItem(tr("Do not download", "Do not download (priority)")); @@ -97,13 +89,13 @@ QWidget *PropListDelegate::createEditor(QWidget *parent, const QStyleOptionViewI connect(editor, qOverload(&QComboBox::currentIndexChanged), this, [this, editor]() { - emit const_cast(this)->commitData(editor); + emit const_cast(this)->commitData(editor); }); return editor; } -void PropListDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const +void TorrentContentItemDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const { const auto *combobox = static_cast(editor); @@ -130,23 +122,22 @@ void PropListDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, if (newPriority != previousPriority) { model->setData(index, newPriority); - emit filteredFilesChanged(); } } -void PropListDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &) const +void TorrentContentItemDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &) const { editor->setGeometry(option.rect); } -void PropListDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const +void TorrentContentItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { switch (index.column()) { - case PropColumn::PROGRESS: + case TorrentContentModelItem::COL_PROGRESS: { const int progress = static_cast(index.data(TorrentContentModel::UnderlyingDataRole).toReal()); - const int priority = index.sibling(index.row(), PropColumn::PRIORITY).data(TorrentContentModel::UnderlyingDataRole).toInt(); + const int priority = index.sibling(index.row(), TorrentContentModelItem::COL_PRIO).data(TorrentContentModel::UnderlyingDataRole).toInt(); const bool isEnabled = static_cast(priority) != BitTorrent::DownloadPriority::Ignored; QStyleOptionViewItem customOption {option}; diff --git a/src/gui/properties/proplistdelegate.h b/src/gui/torrentcontentitemdelegate.h similarity index 84% rename from src/gui/properties/proplistdelegate.h rename to src/gui/torrentcontentitemdelegate.h index 1db613224..c9d0db7f1 100644 --- a/src/gui/properties/proplistdelegate.h +++ b/src/gui/torrentcontentitemdelegate.h @@ -1,5 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2022 Vladimir Golovnev * Copyright (C) 2006 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -36,26 +37,13 @@ class QAbstractItemModel; class QModelIndex; class QStyleOptionViewItem; -class PropertiesWidget; - -// Defines for properties list columns -enum PropColumn -{ - NAME, - PCSIZE, - PROGRESS, - PRIORITY, - REMAINING, - AVAILABILITY -}; - -class PropListDelegate final : public QStyledItemDelegate +class TorrentContentItemDelegate final : public QStyledItemDelegate { Q_OBJECT - Q_DISABLE_COPY_MOVE(PropListDelegate) + Q_DISABLE_COPY_MOVE(TorrentContentItemDelegate) public: - explicit PropListDelegate(PropertiesWidget *properties); + explicit TorrentContentItemDelegate(QWidget *parent = nullptr); void setEditorData(QWidget *editor, const QModelIndex &index) const override; QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override; @@ -65,10 +53,6 @@ public slots: void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const override; -signals: - void filteredFilesChanged() const; - private: - PropertiesWidget *m_properties = nullptr; ProgressBarPainter m_progressBarPainter; }; diff --git a/src/gui/torrentcontentmodel.cpp b/src/gui/torrentcontentmodel.cpp index 0ae77fd57..ff76d6833 100644 --- a/src/gui/torrentcontentmodel.cpp +++ b/src/gui/torrentcontentmodel.cpp @@ -1,5 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2022 Vladimir Golovnev * Copyright (C) 2006-2012 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -33,6 +34,8 @@ #include #include #include +#include +#include #if defined(Q_OS_WIN) #include @@ -50,8 +53,9 @@ #include #endif -#include "base/bittorrent/abstractfilestorage.h" #include "base/bittorrent/downloadpriority.h" +#include "base/bittorrent/torrentcontenthandler.h" +#include "base/exceptions.h" #include "base/global.h" #include "base/path.h" #include "base/utils/fs.h" @@ -208,62 +212,89 @@ TorrentContentModel::~TorrentContentModel() delete m_rootItem; } -void TorrentContentModel::updateFilesProgress(const QVector &fp) +void TorrentContentModel::updateFilesProgress() { - Q_ASSERT(m_filesIndex.size() == fp.size()); - // XXX: Why is this necessary? - if (m_filesIndex.size() != fp.size()) return; + Q_ASSERT(m_contentHandler && m_contentHandler->hasMetadata()); - emit layoutAboutToBeChanged(); - for (int i = 0; i < fp.size(); ++i) - m_filesIndex[i]->setProgress(fp[i]); - // Update folders progress in the tree - m_rootItem->recalculateProgress(); - m_rootItem->recalculateAvailability(); - - const QVector columns = + using HandlerPtr = QPointer; + m_contentHandler->fetchFilesProgress([this, handler = HandlerPtr(m_contentHandler)](const QVector &filesProgress) { - {TorrentContentModelItem::COL_PROGRESS, TorrentContentModelItem::COL_PROGRESS} - }; - notifySubtreeUpdated(index(0, 0), columns); + if (handler != m_contentHandler) + return; + + Q_ASSERT(m_filesIndex.size() == filesProgress.size()); + // XXX: Why is this necessary? + if (Q_UNLIKELY(m_filesIndex.size() != filesProgress.size())) + return; + + for (int i = 0; i < filesProgress.size(); ++i) + m_filesIndex[i]->setProgress(filesProgress[i]); + // Update folders progress in the tree + m_rootItem->recalculateProgress(); + m_rootItem->recalculateAvailability(); + }); } -void TorrentContentModel::updateFilesPriorities(const QVector &fprio) +void TorrentContentModel::updateFilesPriorities() { + Q_ASSERT(m_contentHandler && m_contentHandler->hasMetadata()); + + const QVector fprio = m_contentHandler->filePriorities(); Q_ASSERT(m_filesIndex.size() == fprio.size()); // XXX: Why is this necessary? if (m_filesIndex.size() != fprio.size()) return; - emit layoutAboutToBeChanged(); for (int i = 0; i < fprio.size(); ++i) m_filesIndex[i]->setPriority(static_cast(fprio[i])); +} - const QVector columns = +void TorrentContentModel::updateFilesAvailability() +{ + Q_ASSERT(m_contentHandler && m_contentHandler->hasMetadata()); + + using HandlerPtr = QPointer; + m_contentHandler->fetchAvailableFileFractions([this, handler = HandlerPtr(m_contentHandler)](const QVector &availableFileFractions) { - {TorrentContentModelItem::COL_NAME, TorrentContentModelItem::COL_NAME}, - {TorrentContentModelItem::COL_PRIO, TorrentContentModelItem::COL_PRIO} - }; - notifySubtreeUpdated(index(0, 0), columns); + if (handler != m_contentHandler) + return; + + Q_ASSERT(m_filesIndex.size() == availableFileFractions.size()); + // XXX: Why is this necessary? + if (Q_UNLIKELY(m_filesIndex.size() != availableFileFractions.size())) + return; + + for (int i = 0; i < m_filesIndex.size(); ++i) + m_filesIndex[i]->setAvailability(availableFileFractions[i]); + // Update folders progress in the tree + m_rootItem->recalculateProgress(); + }); } -void TorrentContentModel::updateFilesAvailability(const QVector &fa) +bool TorrentContentModel::setItemPriority(const QModelIndex &index, BitTorrent::DownloadPriority priority) { - Q_ASSERT(m_filesIndex.size() == fa.size()); - // XXX: Why is this necessary? - if (m_filesIndex.size() != fa.size()) return; + Q_ASSERT(index.isValid()); + + auto *item = static_cast(index.internalPointer()); + const BitTorrent::DownloadPriority currentPriority = item->priority(); + if (currentPriority == priority) + return false; + + item->setPriority(priority); + m_contentHandler->prioritizeFiles(getFilePriorities()); - emit layoutAboutToBeChanged(); - for (int i = 0; i < m_filesIndex.size(); ++i) - m_filesIndex[i]->setAvailability(fa[i]); // Update folders progress in the tree m_rootItem->recalculateProgress(); + m_rootItem->recalculateAvailability(); const QVector columns = { - {TorrentContentModelItem::COL_AVAILABILITY, TorrentContentModelItem::COL_AVAILABILITY} + {TorrentContentModelItem::COL_NAME, TorrentContentModelItem::COL_NAME}, + {TorrentContentModelItem::COL_PRIO, TorrentContentModelItem::COL_PRIO} }; - notifySubtreeUpdated(index(0, 0), columns); + notifySubtreeUpdated(index, columns); + + return true; } QVector TorrentContentModel::getFilePriorities() const @@ -275,14 +306,6 @@ QVector TorrentContentModel::getFilePriorities() c return prio; } -bool TorrentContentModel::allFiltered() const -{ - return std::all_of(m_filesIndex.cbegin(), m_filesIndex.cend(), [](const TorrentContentModelFile *fileItem) - { - return (fileItem->priority() == BitTorrent::DownloadPriority::Ignored); - }); -} - int TorrentContentModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent); @@ -296,9 +319,6 @@ bool TorrentContentModel::setData(const QModelIndex &index, const QVariant &valu if ((index.column() == TorrentContentModelItem::COL_NAME) && (role == Qt::CheckStateRole)) { - auto *item = static_cast(index.internalPointer()); - - const BitTorrent::DownloadPriority currentPrio = item->priority(); const auto checkState = static_cast(value.toInt()); const BitTorrent::DownloadPriority newPrio = (checkState == Qt::PartiallyChecked) ? BitTorrent::DownloadPriority::Mixed @@ -306,23 +326,7 @@ bool TorrentContentModel::setData(const QModelIndex &index, const QVariant &valu ? BitTorrent::DownloadPriority::Ignored : BitTorrent::DownloadPriority::Normal); - if (currentPrio != newPrio) - { - item->setPriority(newPrio); - // Update folders progress in the tree - m_rootItem->recalculateProgress(); - m_rootItem->recalculateAvailability(); - - const QVector columns = - { - {TorrentContentModelItem::COL_NAME, TorrentContentModelItem::COL_NAME}, - {TorrentContentModelItem::COL_PRIO, TorrentContentModelItem::COL_PRIO} - }; - notifySubtreeUpdated(index, columns); - emit filteredFilesChanged(); - - return true; - } + return setItemPriority(index, newPrio); } if (role == Qt::EditRole) @@ -337,6 +341,23 @@ bool TorrentContentModel::setData(const QModelIndex &index, const QVariant &valu const QString newName = value.toString(); if (currentName != newName) { + try + { + const Path parentPath = getItemPath(index.parent()); + const Path oldPath = parentPath / Path(currentName); + const Path newPath = parentPath / Path(newName); + + if (item->itemType() == TorrentContentModelItem::FileType) + m_contentHandler->renameFile(oldPath, newPath); + else + m_contentHandler->renameFolder(oldPath, newPath); + } + catch (const RuntimeError &error) + { + emit renameFailed(error.message()); + return false; + } + item->setName(newName); emit dataChanged(index, index); return true; @@ -346,27 +367,8 @@ bool TorrentContentModel::setData(const QModelIndex &index, const QVariant &valu case TorrentContentModelItem::COL_PRIO: { - const BitTorrent::DownloadPriority currentPrio = item->priority(); const auto newPrio = static_cast(value.toInt()); - if (currentPrio != newPrio) - { - item->setPriority(newPrio); - - const QVector columns = - { - {TorrentContentModelItem::COL_NAME, TorrentContentModelItem::COL_NAME}, - {TorrentContentModelItem::COL_PRIO, TorrentContentModelItem::COL_PRIO} - }; - notifySubtreeUpdated(index, columns); - - if ((newPrio == BitTorrent::DownloadPriority::Ignored) - || (currentPrio == BitTorrent::DownloadPriority::Ignored)) - { - emit filteredFilesChanged(); - } - - return true; - } + return setItemPriority(index, newPrio); } break; @@ -383,16 +385,23 @@ TorrentContentModelItem::ItemType TorrentContentModel::itemType(const QModelInde return static_cast(index.internalPointer())->itemType(); } -int TorrentContentModel::getFileIndex(const QModelIndex &index) +int TorrentContentModel::getFileIndex(const QModelIndex &index) const { auto *item = static_cast(index.internalPointer()); if (item->itemType() == TorrentContentModelItem::FileType) return static_cast(item)->fileIndex(); - Q_ASSERT(item->itemType() == TorrentContentModelItem::FileType); return -1; } +Path TorrentContentModel::getItemPath(const QModelIndex &index) const +{ + Path path; + for (QModelIndex i = index; i.isValid(); i = i.parent()) + path = Path(i.data().toString()) / path; + return path; +} + QVariant TorrentContentModel::data(const QModelIndex &index, const int role) const { if (!index.isValid()) @@ -403,29 +412,45 @@ QVariant TorrentContentModel::data(const QModelIndex &index, const int role) con switch (role) { case Qt::DecorationRole: - { - if (index.column() != TorrentContentModelItem::COL_NAME) - return {}; + if (index.column() != TorrentContentModelItem::COL_NAME) + return {}; + + if (item->itemType() == TorrentContentModelItem::FolderType) + return m_fileIconProvider->icon(QFileIconProvider::Folder); + + return m_fileIconProvider->icon(QFileInfo(item->name())); - if (item->itemType() == TorrentContentModelItem::FolderType) - return m_fileIconProvider->icon(QFileIconProvider::Folder); - return m_fileIconProvider->icon(QFileInfo(item->name())); - } case Qt::CheckStateRole: + if (index.column() != TorrentContentModelItem::COL_NAME) + return {}; + + if (item->priority() == BitTorrent::DownloadPriority::Ignored) + return Qt::Unchecked; + + if (item->priority() == BitTorrent::DownloadPriority::Mixed) { - if (index.column() != TorrentContentModelItem::COL_NAME) - return {}; + Q_ASSERT(item->itemType() == TorrentContentModelItem::FolderType); + + const auto *folder = static_cast(item); + const auto childItems = folder->children(); + const bool hasIgnored = std::any_of(childItems.cbegin(), childItems.cend() + , [](const TorrentContentModelItem *childItem) + { + return (childItem->priority() == BitTorrent::DownloadPriority::Ignored); + }); - if (item->priority() == BitTorrent::DownloadPriority::Ignored) - return Qt::Unchecked; - if (item->priority() == BitTorrent::DownloadPriority::Mixed) - return Qt::PartiallyChecked; - return Qt::Checked; + return hasIgnored ? Qt::PartiallyChecked : Qt::Checked; } + + return Qt::Checked; + case Qt::TextAlignmentRole: if ((index.column() == TorrentContentModelItem::COL_SIZE) || (index.column() == TorrentContentModelItem::COL_REMAINING)) + { return QVariant {Qt::AlignRight | Qt::AlignVCenter}; + } + return {}; case Qt::DisplayRole: @@ -469,7 +494,10 @@ QVariant TorrentContentModel::headerData(int section, Qt::Orientation orientatio case Qt::TextAlignmentRole: if ((section == TorrentContentModelItem::COL_SIZE) || (section == TorrentContentModelItem::COL_REMAINING)) + { return QVariant {Qt::AlignRight | Qt::AlignVCenter}; + } + return {}; default: @@ -525,26 +553,11 @@ int TorrentContentModel::rowCount(const QModelIndex &parent) const return parentItem ? parentItem->childCount() : 0; } -void TorrentContentModel::clear() -{ - qDebug("clear called"); - beginResetModel(); - m_filesIndex.clear(); - m_rootItem->deleteAllChildren(); - endResetModel(); -} - -void TorrentContentModel::setupModelData(const BitTorrent::AbstractFileStorage &info) +void TorrentContentModel::populate() { - qDebug("setup model data called"); - const int filesCount = info.filesCount(); - if (filesCount <= 0) - return; - - beginResetModel(); + Q_ASSERT(m_contentHandler && m_contentHandler->hasMetadata()); - // Initialize files_index array - qDebug("Torrent contains %d files", filesCount); + const int filesCount = m_contentHandler->filesCount(); m_filesIndex.reserve(filesCount); QHash> folderMap; @@ -553,7 +566,7 @@ void TorrentContentModel::setupModelData(const BitTorrent::AbstractFileStorage & // Iterate over files for (int i = 0; i < filesCount; ++i) { - const QString path = info.filePath(i).data(); + const QString path = m_contentHandler->filePath(i).data(); // Iterate of parts of the path to create necessary folders QList pathFolders = QStringView(path).split(u'/', Qt::SkipEmptyParts); @@ -584,13 +597,64 @@ void TorrentContentModel::setupModelData(const BitTorrent::AbstractFileStorage & } // Actually create the file - TorrentContentModelFile *fileItem = new TorrentContentModelFile( - fileName, info.fileSize(i), lastParent, i); + auto *fileItem = new TorrentContentModelFile(fileName, m_contentHandler->fileSize(i), lastParent, i); lastParent->appendChild(fileItem); m_filesIndex.push_back(fileItem); } - endResetModel(); + updateFilesProgress(); + updateFilesPriorities(); + updateFilesAvailability(); +} + +void TorrentContentModel::setContentHandler(BitTorrent::TorrentContentHandler *contentHandler) +{ + beginResetModel(); + [[maybe_unused]] const auto modelResetGuard = qScopeGuard([this] { endResetModel(); }); + + if (m_contentHandler) + { + m_filesIndex.clear(); + m_rootItem->deleteAllChildren(); + } + + m_contentHandler = contentHandler; + + if (m_contentHandler && m_contentHandler->hasMetadata()) + populate(); +} + +BitTorrent::TorrentContentHandler *TorrentContentModel::contentHandler() const +{ + return m_contentHandler; +} + +void TorrentContentModel::refresh() +{ + if (!m_contentHandler || !m_contentHandler->hasMetadata()) + return; + + if (!m_filesIndex.isEmpty()) + { + updateFilesProgress(); + updateFilesPriorities(); + updateFilesAvailability(); + + const QVector columns = + { + {TorrentContentModelItem::COL_NAME, TorrentContentModelItem::COL_NAME}, + {TorrentContentModelItem::COL_PROGRESS, TorrentContentModelItem::COL_PROGRESS}, + {TorrentContentModelItem::COL_PRIO, TorrentContentModelItem::COL_PRIO}, + {TorrentContentModelItem::COL_AVAILABILITY, TorrentContentModelItem::COL_AVAILABILITY} + }; + notifySubtreeUpdated(index(0, 0), columns); + } + else + { + beginResetModel(); + populate(); + endResetModel(); + } } void TorrentContentModel::notifySubtreeUpdated(const QModelIndex &index, const QVector &columns) diff --git a/src/gui/torrentcontentmodel.h b/src/gui/torrentcontentmodel.h index 481cf2ead..d18577453 100644 --- a/src/gui/torrentcontentmodel.h +++ b/src/gui/torrentcontentmodel.h @@ -1,5 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2022 Vladimir Golovnev * Copyright (C) 2006-2012 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -32,6 +33,7 @@ #include #include "base/indexrange.h" +#include "base/pathfwd.h" #include "torrentcontentmodelitem.h" class QFileIconProvider; @@ -42,7 +44,7 @@ class TorrentContentModelFile; namespace BitTorrent { - class AbstractFileStorage; + class TorrentContentHandler; } class TorrentContentModel final : public QAbstractItemModel @@ -56,35 +58,42 @@ public: UnderlyingDataRole = Qt::UserRole }; - TorrentContentModel(QObject *parent = nullptr); + explicit TorrentContentModel(QObject *parent = nullptr); ~TorrentContentModel() override; - void updateFilesProgress(const QVector &fp); - void updateFilesPriorities(const QVector &fprio); - void updateFilesAvailability(const QVector &fa); + void setContentHandler(BitTorrent::TorrentContentHandler *contentHandler); + BitTorrent::TorrentContentHandler *contentHandler() const; + + void refresh(); + QVector getFilePriorities() const; - bool allFiltered() const; + TorrentContentModelItem::ItemType itemType(const QModelIndex &index) const; + int getFileIndex(const QModelIndex &index) const; + Path getItemPath(const QModelIndex &index) const; + int columnCount(const QModelIndex &parent = {}) const override; bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; - TorrentContentModelItem::ItemType itemType(const QModelIndex &index) const; - int getFileIndex(const QModelIndex &index); QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; Qt::ItemFlags flags(const QModelIndex &index) const override; QVariant headerData(int section, Qt::Orientation orientation, int role) const override; QModelIndex index(int row, int column, const QModelIndex &parent = {}) const override; QModelIndex parent(const QModelIndex &index) const override; int rowCount(const QModelIndex &parent = {}) const override; - void clear(); - void setupModelData(const BitTorrent::AbstractFileStorage &info); signals: - void filteredFilesChanged(); + void renameFailed(const QString &errorMessage); private: using ColumnInterval = IndexInterval; + void populate(); + void updateFilesProgress(); + void updateFilesPriorities(); + void updateFilesAvailability(); + bool setItemPriority(const QModelIndex &index, BitTorrent::DownloadPriority priority); void notifySubtreeUpdated(const QModelIndex &index, const QVector &columns); + BitTorrent::TorrentContentHandler *m_contentHandler = nullptr; TorrentContentModelFolder *m_rootItem = nullptr; QVector m_filesIndex; QFileIconProvider *m_fileIconProvider = nullptr; diff --git a/src/gui/torrentcontentmodelfolder.h b/src/gui/torrentcontentmodelfolder.h index d200a12c1..c82c1008e 100644 --- a/src/gui/torrentcontentmodelfolder.h +++ b/src/gui/torrentcontentmodelfolder.h @@ -62,5 +62,5 @@ public: int childCount() const; private: - QVector m_childItems; + QVector m_childItems; }; diff --git a/src/gui/torrentcontenttreeview.cpp b/src/gui/torrentcontenttreeview.cpp deleted file mode 100644 index 81a90e2ee..000000000 --- a/src/gui/torrentcontenttreeview.cpp +++ /dev/null @@ -1,165 +0,0 @@ -/* - * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2014 Ivan Sorokin - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * - * In addition, as a special exception, the copyright holders give permission to - * link this program with the OpenSSL project's "OpenSSL" library (or with - * modified versions of it that use the same license as the "OpenSSL" library), - * and distribute the linked executables. You must obey the GNU General Public - * License in all respects for all of the code used other than "OpenSSL". If you - * modify file(s), you may extend this exception to your version of the file(s), - * but you are not obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - */ - -#include "torrentcontenttreeview.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "base/bittorrent/abstractfilestorage.h" -#include "base/bittorrent/common.h" -#include "base/bittorrent/session.h" -#include "base/bittorrent/torrent.h" -#include "base/bittorrent/torrentinfo.h" -#include "base/exceptions.h" -#include "base/global.h" -#include "base/path.h" -#include "base/utils/fs.h" -#include "autoexpandabledialog.h" -#include "raisedmessagebox.h" -#include "torrentcontentfiltermodel.h" -#include "torrentcontentmodelitem.h" - -namespace -{ - Path getFullPath(const QModelIndex &idx) - { - Path path; - for (QModelIndex i = idx; i.isValid(); i = i.parent()) - path = Path(i.data().toString()) / path; - return path; - } -} - -TorrentContentTreeView::TorrentContentTreeView(QWidget *parent) - : QTreeView(parent) -{ - setExpandsOnDoubleClick(false); - header()->setFirstSectionMovable(true); -} - -void TorrentContentTreeView::keyPressEvent(QKeyEvent *event) -{ - if ((event->key() != Qt::Key_Space) && (event->key() != Qt::Key_Select)) - { - QTreeView::keyPressEvent(event); - return; - } - - event->accept(); - - const QVariant value = currentNameCell().data(Qt::CheckStateRole); - if (!value.isValid()) - { - Q_ASSERT(false); - return; - } - - const Qt::CheckState state = (static_cast(value.toInt()) == Qt::Checked) - ? Qt::Unchecked : Qt::Checked; - const QModelIndexList selection = selectionModel()->selectedRows(TorrentContentModelItem::COL_NAME); - - for (const QModelIndex &index : selection) - model()->setData(index, state, Qt::CheckStateRole); -} - -void TorrentContentTreeView::renameSelectedFile(BitTorrent::AbstractFileStorage &fileStorage) -{ - const QModelIndexList selectedIndexes = selectionModel()->selectedRows(0); - if (selectedIndexes.size() != 1) return; - - const QPersistentModelIndex modelIndex = selectedIndexes.first(); - if (!modelIndex.isValid()) return; - - auto model = dynamic_cast(TorrentContentTreeView::model()); - if (!model) return; - - const bool isFile = (model->itemType(modelIndex) == TorrentContentModelItem::FileType); - - // Ask for new name - bool ok = false; - QString newName = AutoExpandableDialog::getText(this, tr("Renaming"), tr("New name:"), QLineEdit::Normal - , modelIndex.data().toString(), &ok, isFile).trimmed(); - if (!ok || !modelIndex.isValid()) return; - - const QString oldName = modelIndex.data().toString(); - if (newName == oldName) - return; // Name did not change - - const Path parentPath = getFullPath(modelIndex.parent()); - const Path oldPath = parentPath / Path(oldName); - const Path newPath = parentPath / Path(newName); - - try - { - if (isFile) - fileStorage.renameFile(oldPath, newPath); - else - fileStorage.renameFolder(oldPath, newPath); - - model->setData(modelIndex, newName); - } - catch (const RuntimeError &error) - { - RaisedMessageBox::warning(this, tr("Rename error"), error.message(), QMessageBox::Ok); - } -} - -QModelIndex TorrentContentTreeView::currentNameCell() const -{ - const QModelIndex current = currentIndex(); - if (!current.isValid()) - { - Q_ASSERT(false); - return {}; - } - - return current.siblingAtColumn(TorrentContentModelItem::COL_NAME); -} - -void TorrentContentTreeView::wheelEvent(QWheelEvent *event) -{ - if (event->modifiers() & Qt::ShiftModifier) - { - // Shift + scroll = horizontal scroll - event->accept(); - QWheelEvent scrollHEvent {event->position(), event->globalPosition() - , event->pixelDelta(), event->angleDelta().transposed(), event->buttons() - , event->modifiers(), event->phase(), event->inverted(), event->source()}; - QTreeView::wheelEvent(&scrollHEvent); - return; - } - - QTreeView::wheelEvent(event); // event delegated to base class -} diff --git a/src/gui/torrentcontentwidget.cpp b/src/gui/torrentcontentwidget.cpp new file mode 100644 index 000000000..58a2fef29 --- /dev/null +++ b/src/gui/torrentcontentwidget.cpp @@ -0,0 +1,489 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2022 Vladimir Golovnev + * Copyright (C) 2014 Ivan Sorokin + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#include "torrentcontentwidget.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "base/bittorrent/torrentcontenthandler.h" +#include "base/path.h" +#include "base/utils/string.h" +#include "autoexpandabledialog.h" +#include "raisedmessagebox.h" +#include "torrentcontentfiltermodel.h" +#include "torrentcontentitemdelegate.h" +#include "torrentcontentmodel.h" +#include "torrentcontentmodelitem.h" +#include "uithememanager.h" +#include "utils.h" + +#ifdef Q_OS_MACOS +#include "gui/macutilities.h" +#endif + +TorrentContentWidget::TorrentContentWidget(QWidget *parent) + : QTreeView(parent) +{ + setExpandsOnDoubleClick(false); + setSortingEnabled(true); + header()->setSortIndicator(0, Qt::AscendingOrder); + header()->setFirstSectionMovable(true); + header()->setContextMenuPolicy(Qt::CustomContextMenu); + + m_model = new TorrentContentModel(this); + connect(m_model, &TorrentContentModel::renameFailed, this, [this](const QString &errorMessage) + { + RaisedMessageBox::warning(this, tr("Rename error"), errorMessage, QMessageBox::Ok); + }); + + m_filterModel = new TorrentContentFilterModel(this); + m_filterModel->setSourceModel(m_model); + QTreeView::setModel(m_filterModel); + + auto itemDelegate = new TorrentContentItemDelegate(this); + setItemDelegate(itemDelegate); + + connect(this, &QAbstractItemView::clicked, this, qOverload(&QAbstractItemView::edit)); + connect(this, &QAbstractItemView::doubleClicked, this, &TorrentContentWidget::onItemDoubleClicked); + connect(this, &QWidget::customContextMenuRequested, this, &TorrentContentWidget::displayContextMenu); + connect(header(), &QWidget::customContextMenuRequested, this, &TorrentContentWidget::displayColumnHeaderMenu); + connect(header(), &QHeaderView::sectionMoved, this, &TorrentContentWidget::stateChanged); + connect(header(), &QHeaderView::sectionResized, this, &TorrentContentWidget::stateChanged); + connect(header(), &QHeaderView::sortIndicatorChanged, this, &TorrentContentWidget::stateChanged); + + const auto *renameFileHotkey = new QShortcut(Qt::Key_F2, this, nullptr, nullptr, Qt::WidgetShortcut); + connect(renameFileHotkey, &QShortcut::activated, this, &TorrentContentWidget::renameSelectedFile); + const auto *openFileHotkeyReturn = new QShortcut(Qt::Key_Return, this, nullptr, nullptr, Qt::WidgetShortcut); + connect(openFileHotkeyReturn, &QShortcut::activated, this, &TorrentContentWidget::openSelectedFile); + const auto *openFileHotkeyEnter = new QShortcut(Qt::Key_Enter, this, nullptr, nullptr, Qt::WidgetShortcut); + connect(openFileHotkeyEnter, &QShortcut::activated, this, &TorrentContentWidget::openSelectedFile); + + connect(model(), &QAbstractItemModel::modelReset, this, &TorrentContentWidget::expandRecursively); +} + +void TorrentContentWidget::setContentHandler(BitTorrent::TorrentContentHandler *contentHandler) +{ + m_model->setContentHandler(contentHandler); + if (!contentHandler) + return; + + expandRecursively(); +} + +BitTorrent::TorrentContentHandler *TorrentContentWidget::contentHandler() const +{ + return m_model->contentHandler(); +} + +void TorrentContentWidget::refresh() +{ + setUpdatesEnabled(false); + m_model->refresh(); + setUpdatesEnabled(true); +} + +TorrentContentWidget::DoubleClickAction TorrentContentWidget::doubleClickAction() const +{ + return m_doubleClickAction; +} + +void TorrentContentWidget::setDoubleClickAction(DoubleClickAction action) +{ + m_doubleClickAction = action; +} + +TorrentContentWidget::ColumnsVisibilityMode TorrentContentWidget::columnsVisibilityMode() const +{ + return m_columnsVisibilityMode; +} + +void TorrentContentWidget::setColumnsVisibilityMode(ColumnsVisibilityMode mode) +{ + m_columnsVisibilityMode = mode; +} + +int TorrentContentWidget::getFileIndex(const QModelIndex &index) const +{ + return m_filterModel->getFileIndex(index); +} + +Path TorrentContentWidget::getItemPath(const QModelIndex &index) const +{ + Path path; + for (QModelIndex i = index; i.isValid(); i = i.parent()) + path = Path(i.data().toString()) / path; + return path; +} + +void TorrentContentWidget::setFilterPattern(const QString &patternText) +{ + const QString pattern = Utils::String::wildcardToRegexPattern(patternText); + m_filterModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption)); + if (patternText.isEmpty()) + { + collapseAll(); + expand(m_filterModel->index(0, 0)); + } + else + { + expandAll(); + } +} + +void TorrentContentWidget::checkAll() +{ + for (int i = 0; i < model()->rowCount(); ++i) + model()->setData(model()->index(i, TorrentContentModelItem::COL_NAME), Qt::Checked, Qt::CheckStateRole); +} + +void TorrentContentWidget::checkNone() +{ + for (int i = 0; i < model()->rowCount(); ++i) + model()->setData(model()->index(i, TorrentContentModelItem::COL_NAME), Qt::Unchecked, Qt::CheckStateRole); +} + +void TorrentContentWidget::keyPressEvent(QKeyEvent *event) +{ + if ((event->key() != Qt::Key_Space) && (event->key() != Qt::Key_Select)) + { + QTreeView::keyPressEvent(event); + return; + } + + event->accept(); + + const QVariant value = currentNameCell().data(Qt::CheckStateRole); + if (!value.isValid()) + { + Q_ASSERT(false); + return; + } + + const Qt::CheckState state = (static_cast(value.toInt()) == Qt::Checked) + ? Qt::Unchecked : Qt::Checked; + const QModelIndexList selection = selectionModel()->selectedRows(TorrentContentModelItem::COL_NAME); + + for (const QModelIndex &index : selection) + model()->setData(index, state, Qt::CheckStateRole); +} + +void TorrentContentWidget::renameSelectedFile() +{ + const QModelIndexList selectedIndexes = selectionModel()->selectedRows(0); + if (selectedIndexes.size() != 1) + return; + + const QPersistentModelIndex modelIndex = selectedIndexes.first(); + if (!modelIndex.isValid()) + return; + + // Ask for new name + const bool isFile = (m_filterModel->itemType(modelIndex) == TorrentContentModelItem::FileType); + bool ok = false; + QString newName = AutoExpandableDialog::getText(this, tr("Renaming"), tr("New name:"), QLineEdit::Normal + , modelIndex.data().toString(), &ok, isFile).trimmed(); + if (!ok || !modelIndex.isValid()) + return; + + model()->setData(modelIndex, newName); +} + +void TorrentContentWidget::applyPriorities(const BitTorrent::DownloadPriority priority) +{ + const QModelIndexList selectedRows = selectionModel()->selectedRows(0); + for (const QModelIndex &index : selectedRows) + { + model()->setData(index.sibling(index.row(), Priority), static_cast(priority)); + } +} + +void TorrentContentWidget::applyPrioritiesByOrder() +{ + // Equally distribute the selected items into groups and for each group assign + // a download priority that will apply to each item. The number of groups depends on how + // many "download priority" are available to be assigned + + const QModelIndexList selectedRows = selectionModel()->selectedRows(0); + + const qsizetype priorityGroups = 3; + const auto priorityGroupSize = std::max((selectedRows.length() / priorityGroups), 1); + + for (qsizetype i = 0; i < selectedRows.length(); ++i) + { + auto priority = BitTorrent::DownloadPriority::Ignored; + switch (i / priorityGroupSize) + { + case 0: + priority = BitTorrent::DownloadPriority::Maximum; + break; + case 1: + priority = BitTorrent::DownloadPriority::High; + break; + default: + case 2: + priority = BitTorrent::DownloadPriority::Normal; + break; + } + + const QModelIndex &index = selectedRows[i]; + model()->setData(index.sibling(index.row(), Priority), static_cast(priority)); + } +} + +void TorrentContentWidget::openSelectedFile() +{ + const QModelIndexList selectedIndexes = selectionModel()->selectedRows(0); + if (selectedIndexes.size() != 1) + return; + openItem(selectedIndexes.first()); +} + +void TorrentContentWidget::setModel([[maybe_unused]] QAbstractItemModel *model) +{ + Q_ASSERT_X(false, Q_FUNC_INFO, "Changing the model of TorrentContentWidget is not allowed."); +} + +QModelIndex TorrentContentWidget::currentNameCell() const +{ + const QModelIndex current = currentIndex(); + if (!current.isValid()) + { + Q_ASSERT(false); + return {}; + } + + return current.siblingAtColumn(TorrentContentModelItem::COL_NAME); +} + +void TorrentContentWidget::displayColumnHeaderMenu() +{ + QMenu *menu = new QMenu(this); + menu->setAttribute(Qt::WA_DeleteOnClose); + menu->setToolTipsVisible(true); + + if (m_columnsVisibilityMode == ColumnsVisibilityMode::Editable) + { + menu->setTitle(tr("Column visibility")); + for (int i = 0; i < TorrentContentModelItem::NB_COL; ++i) + { + const auto columnName = model()->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString(); + QAction *action = menu->addAction(columnName, this, [this, i](bool checked) + { + setColumnHidden(i, !checked); + + if (checked && (columnWidth(i) <= 5)) + resizeColumnToContents(i); + + emit stateChanged(); + }); + action->setCheckable(true); + action->setChecked(!isColumnHidden(i)); + + if (i == TorrentContentModelItem::COL_NAME) + action->setEnabled(false); + } + + menu->addSeparator(); + } + + QAction *resizeAction = menu->addAction(tr("Resize columns"), this, [this]() + { + for (int i = 0, count = header()->count(); i < count; ++i) + { + if (!isColumnHidden(i)) + resizeColumnToContents(i); + } + + emit stateChanged(); + }); + resizeAction->setToolTip(tr("Resize all non-hidden columns to the size of their contents")); + + menu->popup(QCursor::pos()); +} + +void TorrentContentWidget::displayContextMenu() +{ + const QModelIndexList selectedRows = selectionModel()->selectedRows(0); + if (selectedRows.empty()) + return; + + QMenu *menu = new QMenu(this); + menu->setAttribute(Qt::WA_DeleteOnClose); + + if (selectedRows.size() == 1) + { + const QModelIndex index = selectedRows[0]; + + if (!contentHandler()->actualStorageLocation().isEmpty()) + { + menu->addAction(UIThemeManager::instance()->getIcon(u"folder-documents"_qs), tr("Open") + , this, [this, index]() { openItem(index); }); + menu->addAction(UIThemeManager::instance()->getIcon(u"directory"_qs), tr("Open containing folder") + , this, [this, index]() { openParentFolder(index); }); + } + menu->addAction(UIThemeManager::instance()->getIcon(u"edit-rename"_qs), tr("Rename...") + , this, &TorrentContentWidget::renameSelectedFile); + menu->addSeparator(); + + QMenu *subMenu = menu->addMenu(tr("Priority")); + + subMenu->addAction(tr("Do not download"), this, [this] + { + applyPriorities(BitTorrent::DownloadPriority::Ignored); + }); + subMenu->addAction(tr("Normal"), this, [this] + { + applyPriorities(BitTorrent::DownloadPriority::Normal); + }); + subMenu->addAction(tr("High"), this, [this] + { + applyPriorities(BitTorrent::DownloadPriority::High); + }); + subMenu->addAction(tr("Maximum"), this, [this] + { + applyPriorities(BitTorrent::DownloadPriority::Maximum); + }); + subMenu->addSeparator(); + subMenu->addAction(tr("By shown file order"), this, &TorrentContentWidget::applyPrioritiesByOrder); + } + else + { + menu->addAction(tr("Do not download"), this, [this] + { + applyPriorities(BitTorrent::DownloadPriority::Ignored); + }); + menu->addAction(tr("Normal priority"), this, [this] + { + applyPriorities(BitTorrent::DownloadPriority::Normal); + }); + menu->addAction(tr("High priority"), this, [this] + { + applyPriorities(BitTorrent::DownloadPriority::High); + }); + menu->addAction(tr("Maximum priority"), this, [this] + { + applyPriorities(BitTorrent::DownloadPriority::Maximum); + }); + menu->addSeparator(); + menu->addAction(tr("Priority by shown file order"), this, &TorrentContentWidget::applyPrioritiesByOrder); + } + + // The selected torrent might have disappeared during exec() + // so we just close menu when an appropriate model is reset + connect(model(), &QAbstractItemModel::modelAboutToBeReset, menu, [menu]() + { + menu->setActiveAction(nullptr); + menu->close(); + }); + + menu->popup(QCursor::pos()); +} + +void TorrentContentWidget::openItem(const QModelIndex &index) const +{ + if (!index.isValid()) + return; + + m_model->contentHandler()->flushCache(); // Flush data + Utils::Gui::openPath(getFullPath(index)); +} + +void TorrentContentWidget::openParentFolder(const QModelIndex &index) const +{ + const Path path = getFullPath(index); + m_model->contentHandler()->flushCache(); // Flush data +#ifdef Q_OS_MACOS + MacUtils::openFiles({path}); +#else + Utils::Gui::openFolderSelect(path); +#endif +} + +Path TorrentContentWidget::getFullPath(const QModelIndex &index) const +{ + const auto contentHandler = m_model->contentHandler(); + if (const int fileIdx = getFileIndex(index); fileIdx >= 0) + { + const Path fullPath = contentHandler->actualStorageLocation() / contentHandler->actualFilePath(fileIdx); + return fullPath; + } + + // folder type + const Path fullPath = contentHandler->actualStorageLocation() / getItemPath(index); + return fullPath; +} + +void TorrentContentWidget::onItemDoubleClicked(const QModelIndex &index) +{ + const auto contentHandler = m_model->contentHandler(); + Q_ASSERT(contentHandler && contentHandler->hasMetadata()); + + if (Q_UNLIKELY(!contentHandler || !contentHandler->hasMetadata())) + return; + + if (m_doubleClickAction == DoubleClickAction::Rename) + renameSelectedFile(); + else + openItem(index); +} + +void TorrentContentWidget::expandRecursively() +{ + QModelIndex currentIndex; + while (model()->rowCount(currentIndex) == 1) + { + currentIndex = model()->index(0, 0, currentIndex); + setExpanded(currentIndex, true); + } +} + +void TorrentContentWidget::wheelEvent(QWheelEvent *event) +{ + if (event->modifiers() & Qt::ShiftModifier) + { + // Shift + scroll = horizontal scroll + event->accept(); + QWheelEvent scrollHEvent {event->position(), event->globalPosition() + , event->pixelDelta(), event->angleDelta().transposed(), event->buttons() + , event->modifiers(), event->phase(), event->inverted(), event->source()}; + QTreeView::wheelEvent(&scrollHEvent); + return; + } + + QTreeView::wheelEvent(event); // event delegated to base class +} diff --git a/src/gui/torrentcontentwidget.h b/src/gui/torrentcontentwidget.h new file mode 100644 index 000000000..35620ee42 --- /dev/null +++ b/src/gui/torrentcontentwidget.h @@ -0,0 +1,121 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2022 Vladimir Golovnev + * Copyright (C) 2014 Ivan Sorokin + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#pragma once + +#include + +#include "base/bittorrent/downloadpriority.h" +#include "base/pathfwd.h" + +namespace BitTorrent +{ + class Torrent; + class TorrentContentHandler; + class TorrentInfo; +} + +class TorrentContentFilterModel; +class TorrentContentModel; + +class TorrentContentWidget final : public QTreeView +{ + Q_OBJECT + Q_DISABLE_COPY_MOVE(TorrentContentWidget) + +public: + enum Column + { + Name, + Size, + Progress, + Priority, + Remaining, + Availability + }; + + enum class DoubleClickAction + { + Open, + Rename + }; + + enum class ColumnsVisibilityMode + { + Editable, + Locked + }; + + explicit TorrentContentWidget(QWidget *parent = nullptr); + + void setContentHandler(BitTorrent::TorrentContentHandler *contentHandler); + BitTorrent::TorrentContentHandler *contentHandler() const; + void refresh(); + + DoubleClickAction doubleClickAction() const; + void setDoubleClickAction(DoubleClickAction action); + + ColumnsVisibilityMode columnsVisibilityMode() const; + void setColumnsVisibilityMode(ColumnsVisibilityMode mode); + + int getFileIndex(const QModelIndex &index) const; + Path getItemPath(const QModelIndex &index) const; + + void setFilterPattern(const QString &patternText); + + void checkAll(); + void checkNone(); + +signals: + void stateChanged(); + +private: + void setModel(QAbstractItemModel *model) override; + void keyPressEvent(QKeyEvent *event) override; + void wheelEvent(QWheelEvent *event) override; + QModelIndex currentNameCell() const; + void displayColumnHeaderMenu(); + void displayContextMenu(); + void openItem(const QModelIndex &index) const; + void openParentFolder(const QModelIndex &index) const; + void openSelectedFile(); + void renameSelectedFile(); + void applyPriorities(BitTorrent::DownloadPriority priority); + void applyPrioritiesByOrder(); + Path getFullPath(const QModelIndex &index) const; + void onItemDoubleClicked(const QModelIndex &index); + // Expand single-item folders recursively. + // This will trigger sorting and filtering so do it after all relevant data is loaded. + void expandRecursively(); + + TorrentContentModel *m_model; + TorrentContentFilterModel *m_filterModel; + DoubleClickAction m_doubleClickAction = DoubleClickAction::Rename; + ColumnsVisibilityMode m_columnsVisibilityMode = ColumnsVisibilityMode::Editable; +};