From 838099ca9e3d4e2b0ff79c712d78a61ba965421f Mon Sep 17 00:00:00 2001 From: Alexey Rochev Date: Sat, 4 Oct 2025 22:19:18 +0300 Subject: [PATCH 01/12] Move the logic for building torrent's files tree to a separate class --- src/CMakeLists.txt | 1 + src/ui/itemmodels/torrentfilesmodelentry.cpp | 21 ++++-- src/ui/itemmodels/torrentfilesmodelentry.h | 18 ++++- src/ui/itemmodels/torrentfilestreebuilder.h | 66 +++++++++++++++++++ .../addtorrent/localtorrentfilesmodel.cpp | 63 +++++------------- .../torrentproperties/torrentfilesmodel.cpp | 43 ++++-------- 6 files changed, 126 insertions(+), 86 deletions(-) create mode 100644 src/ui/itemmodels/torrentfilestreebuilder.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7b82ff74..a80143cb 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -177,6 +177,7 @@ target_sources(tremotesf_objects ui/itemmodels/stringlistmodel.h ui/itemmodels/torrentfilesmodelentry.h ui/itemmodels/torrentfilesproxymodel.h + ui/itemmodels/torrentfilestreebuilder.h ui/notificationscontroller.h ui/savewindowstatedispatcher.h ui/screens/aboutdialog.h diff --git a/src/ui/itemmodels/torrentfilesmodelentry.cpp b/src/ui/itemmodels/torrentfilesmodelentry.cpp index 33705b9c..1287806a 100644 --- a/src/ui/itemmodels/torrentfilesmodelentry.cpp +++ b/src/ui/itemmodels/torrentfilesmodelentry.cpp @@ -152,9 +152,11 @@ namespace tremotesf { return mChildrenHash; } - TorrentFilesModelFile* TorrentFilesModelDirectory::addFile(int id, const QString& name, long long size) { + TorrentFilesModelFile* TorrentFilesModelDirectory::addFile( + int id, const QString& name, long long size, long long completedSize, bool wanted, Priority priority + ) { const int row = static_cast(mChildren.size()); - auto file = std::make_unique(row, this, id, name, size); + auto file = std::make_unique(row, this, id, name, size, completedSize, wanted, priority); auto* filePtr = file.get(); addChild(std::move(file)); return filePtr; @@ -200,13 +202,20 @@ namespace tremotesf { } TorrentFilesModelFile::TorrentFilesModelFile( - int row, TorrentFilesModelDirectory* parentDirectory, int id, const QString& name, long long size + int row, + TorrentFilesModelDirectory* parentDirectory, + int id, + const QString& name, + long long size, + long long completedSize, + bool wanted, + Priority priority ) : TorrentFilesModelEntry(row, parentDirectory, name), mSize(size), - mCompletedSize(0), - mWantedState(WantedState::Unwanted), - mPriority(Priority::Normal), + mCompletedSize(completedSize), + mWantedState(wanted ? WantedState::Wanted : WantedState::Unwanted), + mPriority(priority), mId(id), mInitializedIcon(false), mChanged(false) {} diff --git a/src/ui/itemmodels/torrentfilesmodelentry.h b/src/ui/itemmodels/torrentfilesmodelentry.h index a7b5b3e2..ab31a28a 100644 --- a/src/ui/itemmodels/torrentfilesmodelentry.h +++ b/src/ui/itemmodels/torrentfilesmodelentry.h @@ -84,7 +84,14 @@ namespace tremotesf { const std::vector>& children() const; const std::unordered_map& childrenHash() const; - TorrentFilesModelFile* addFile(int id, const QString& name, long long size); + TorrentFilesModelFile* addFile( + int id, + const QString& name, + long long size, + long long completedSize, + bool wanted, + TorrentFilesModelEntry::Priority priority + ); TorrentFilesModelDirectory* addDirectory(const QString& name); void clearChildren(); @@ -104,7 +111,14 @@ namespace tremotesf { class TorrentFilesModelFile final : public TorrentFilesModelEntry { public: explicit TorrentFilesModelFile( - int row, TorrentFilesModelDirectory* parentDirectory, int id, const QString& name, long long size + int row, + TorrentFilesModelDirectory* parentDirectory, + int id, + const QString& name, + long long size, + long long completedSize, + bool wanted, + TorrentFilesModelEntry::Priority priority ); bool isDirectory() const override; diff --git a/src/ui/itemmodels/torrentfilestreebuilder.h b/src/ui/itemmodels/torrentfilestreebuilder.h new file mode 100644 index 00000000..5695433a --- /dev/null +++ b/src/ui/itemmodels/torrentfilestreebuilder.h @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: 2015-2025 Alexey Rochev +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef TREMOTESF_TORRENTFILESTREEBUILDER_H +#define TREMOTESF_TORRENTFILESTREEBUILDER_H + +#include +#include + +#include "torrentfilesmodelentry.h" + +namespace tremotesf { + class TorrentFilesTreeBuilder final { + public: + explicit TorrentFilesTreeBuilder(TorrentFilesModelDirectory* rootDirectory, size_t reserveFilesCount) + : rootDirectory(rootDirectory) { + files.reserve(reserveFilesCount); + } + + ~TorrentFilesTreeBuilder() = default; + Q_DISABLE_COPY_MOVE(TorrentFilesTreeBuilder) + + template + requires( + std::ranges::forward_range + && std::ranges::sized_range + && std::same_as> + ) + void addFile( + PathParts&& pathParts, + long long size, + long long completedSize, + bool wanted, + TorrentFilesModelEntry::Priority priority + ) { + TorrentFilesModelDirectory* currentDirectory = rootDirectory; + + const auto lastPartIndex = pathParts.size() - 1; + + for (auto&& [index, part] : pathParts | std::views::enumerate) { + if (static_cast(index) == lastPartIndex) { + auto* const file = currentDirectory->addFile(mFileId, part, size, completedSize, wanted, priority); + files.push_back(file); + ++mFileId; + } else { + const auto& childrenHash = currentDirectory->childrenHash(); + const auto found = childrenHash.find(part); + if (found != childrenHash.end()) { + currentDirectory = static_cast(found->second); + } else { + currentDirectory = currentDirectory->addDirectory(part); + } + } + } + } + + TorrentFilesModelDirectory* rootDirectory; + std::vector files{}; + + private: + int mFileId{}; + }; +} + +#endif // TREMOTESF_TORRENTFILESTREEBUILDER_H diff --git a/src/ui/screens/addtorrent/localtorrentfilesmodel.cpp b/src/ui/screens/addtorrent/localtorrentfilesmodel.cpp index eb28390a..880205e4 100644 --- a/src/ui/screens/addtorrent/localtorrentfilesmodel.cpp +++ b/src/ui/screens/addtorrent/localtorrentfilesmodel.cpp @@ -6,6 +6,7 @@ #include "coroutines/threadpool.h" #include "ui/itemmodels/torrentfilesmodelentry.h" +#include "ui/itemmodels/torrentfilestreebuilder.h" #include "torrentfileparser.h" namespace tremotesf { @@ -17,54 +18,24 @@ namespace tremotesf { CreateTreeResult createTree(TorrentMetainfoFile torrentFile) { auto rootDirectory = std::make_shared(); - std::vector files; - - if (torrentFile.isSingleFile()) { - auto* file = rootDirectory->addFile(0, torrentFile.rootFileName, torrentFile.singleFileSize()); - file->setWanted(true); - file->setPriority(TorrentFilesModelEntry::Priority::Normal); - file->setChanged(false); - files.push_back(file); - } else { - const auto torrentDirectoryName = torrentFile.rootFileName; - - auto* torrentDirectory = rootDirectory->addDirectory(torrentDirectoryName); - - auto torrentFiles = torrentFile.files(); - files.reserve(torrentFiles.size()); - int fileIndex = -1; - for (TorrentMetainfoFile::File file : torrentFiles) { - ++fileIndex; - - TorrentFilesModelDirectory* currentDirectory = torrentDirectory; - - auto pathParts = file.path(); - - int partIndex = -1; - const int lastPartIndex = static_cast(pathParts.size()) - 1; - - for (const QString& part : pathParts) { - ++partIndex; - if (partIndex == lastPartIndex) { - auto* childFile = currentDirectory->addFile(fileIndex, part, file.size); - childFile->setWanted(true); - childFile->setPriority(TorrentFilesModelEntry::Priority::Normal); - childFile->setChanged(false); - files.push_back(childFile); - } else { - const auto& childrenHash = currentDirectory->childrenHash(); - const auto found = childrenHash.find(part); - if (found != childrenHash.end()) { - currentDirectory = static_cast(found->second); - } else { - currentDirectory = currentDirectory->addDirectory(part); - } - } - } - } + if (torrentFile.isSingleFile() == 1) { + auto* const file = rootDirectory->addFile( + 0, + torrentFile.rootFileName, + torrentFile.singleFileSize(), + 0, + true, + TorrentFilesModelEntry::Priority::Normal + ); + return {.rootDirectory = std::move(rootDirectory), .files = {file}}; } - return {.rootDirectory = std::move(rootDirectory), .files = std::move(files)}; + const auto files = torrentFile.files(); + TorrentFilesTreeBuilder builder(rootDirectory->addDirectory(torrentFile.rootFileName), files.size()); + for (TorrentMetainfoFile::File file : files) { + builder.addFile(file.path(), file.size, 0, true, TorrentFilesModelEntry::Priority::Normal); + } + return {.rootDirectory = std::move(rootDirectory), .files = std::move(builder.files)}; } } diff --git a/src/ui/screens/torrentproperties/torrentfilesmodel.cpp b/src/ui/screens/torrentproperties/torrentfilesmodel.cpp index b22ea0c3..e82198cc 100644 --- a/src/ui/screens/torrentproperties/torrentfilesmodel.cpp +++ b/src/ui/screens/torrentproperties/torrentfilesmodel.cpp @@ -12,6 +12,7 @@ #include "rpc/torrent.h" #include "rpc/serversettings.h" #include "rpc/rpc.h" +#include "ui/itemmodels/torrentfilestreebuilder.h" using namespace Qt::StringLiterals; @@ -55,39 +56,17 @@ namespace tremotesf { std::pair, std::vector> doCreateTree(const std::vector& files) { auto rootDirectory = std::make_shared(); - std::vector treeFiles; - treeFiles.reserve(files.size()); - - for (size_t fileIndex = 0, filesCount = files.size(); fileIndex < filesCount; ++fileIndex) { - const TorrentFile& file = files[fileIndex]; - - TorrentFilesModelDirectory* currentDirectory = rootDirectory.get(); - - const std::vector parts(file.path); - - for (size_t partIndex = 0, partsCount = parts.size(), lastPartIndex = partsCount - 1; - partIndex < partsCount; - ++partIndex) { - const QString& part = parts[partIndex]; - - if (partIndex == lastPartIndex) { - auto* childFile = currentDirectory->addFile(static_cast(fileIndex), part, file.size); - updateFile(childFile, file); - childFile->setChanged(false); - treeFiles.push_back(childFile); - } else { - const auto& childrenHash = currentDirectory->childrenHash(); - const auto found = childrenHash.find(part); - if (found != childrenHash.end()) { - currentDirectory = static_cast(found->second); - } else { - currentDirectory = currentDirectory->addDirectory(part); - } - } - } + TorrentFilesTreeBuilder builder(rootDirectory.get(), files.size()); + for (const TorrentFile& file : files) { + builder.addFile( + file.path, + file.size, + file.completedSize, + file.wanted, + TorrentFilesModelEntry::fromFilePriority(file.priority) + ); } - - return {std::move(rootDirectory), std::move(treeFiles)}; + return {std::move(rootDirectory), std::move(builder.files)}; } } From 21d87bf913eb103869dd931005a1183f9ed54026 Mon Sep 17 00:00:00 2001 From: Alexey Rochev Date: Sat, 4 Oct 2025 22:33:41 +0300 Subject: [PATCH 02/12] Replace unnecessary shared_ptr with unique_ptr --- src/ui/itemmodels/basetorrentfilesmodel.h | 2 +- src/ui/screens/addtorrent/localtorrentfilesmodel.cpp | 4 ++-- src/ui/screens/torrentproperties/torrentfilesmodel.cpp | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ui/itemmodels/basetorrentfilesmodel.h b/src/ui/itemmodels/basetorrentfilesmodel.h index 81571627..8b968ed9 100644 --- a/src/ui/itemmodels/basetorrentfilesmodel.h +++ b/src/ui/itemmodels/basetorrentfilesmodel.h @@ -42,7 +42,7 @@ namespace tremotesf { protected: void updateDirectoryChildren(const QModelIndex& parent = QModelIndex()); - std::shared_ptr mRootDirectory; + std::unique_ptr mRootDirectory; private: const std::vector mColumns; diff --git a/src/ui/screens/addtorrent/localtorrentfilesmodel.cpp b/src/ui/screens/addtorrent/localtorrentfilesmodel.cpp index 880205e4..208d6f54 100644 --- a/src/ui/screens/addtorrent/localtorrentfilesmodel.cpp +++ b/src/ui/screens/addtorrent/localtorrentfilesmodel.cpp @@ -12,12 +12,12 @@ namespace tremotesf { namespace { struct CreateTreeResult { - std::shared_ptr rootDirectory; + std::unique_ptr rootDirectory; std::vector files; }; CreateTreeResult createTree(TorrentMetainfoFile torrentFile) { - auto rootDirectory = std::make_shared(); + auto rootDirectory = std::make_unique(); if (torrentFile.isSingleFile() == 1) { auto* const file = rootDirectory->addFile( 0, diff --git a/src/ui/screens/torrentproperties/torrentfilesmodel.cpp b/src/ui/screens/torrentproperties/torrentfilesmodel.cpp index e82198cc..5f1d76c6 100644 --- a/src/ui/screens/torrentproperties/torrentfilesmodel.cpp +++ b/src/ui/screens/torrentproperties/torrentfilesmodel.cpp @@ -53,9 +53,9 @@ namespace tremotesf { return ids; } - std::pair, std::vector> + std::pair, std::vector> doCreateTree(const std::vector& files) { - auto rootDirectory = std::make_shared(); + auto rootDirectory = std::make_unique(); TorrentFilesTreeBuilder builder(rootDirectory.get(), files.size()); for (const TorrentFile& file : files) { builder.addFile( From 213f13b992a051e1a901b4a4085c59a5c808316d Mon Sep 17 00:00:00 2001 From: Alexey Rochev Date: Sat, 4 Oct 2025 23:53:15 +0300 Subject: [PATCH 03/12] Move hash map of child directory names to directory pointers from TorrentFilesModelDirectory to TorrentFilesTreeBuilder so that it can be disposed of after building the tree --- src/ui/itemmodels/torrentfilesmodelentry.cpp | 10 +-------- src/ui/itemmodels/torrentfilesmodelentry.h | 3 --- src/ui/itemmodels/torrentfilestreebuilder.h | 21 +++++++++++++++---- .../torrentproperties/torrentfilesmodel.cpp | 6 +++++- 4 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/ui/itemmodels/torrentfilesmodelentry.cpp b/src/ui/itemmodels/torrentfilesmodelentry.cpp index 1287806a..0395aefc 100644 --- a/src/ui/itemmodels/torrentfilesmodelentry.cpp +++ b/src/ui/itemmodels/torrentfilesmodelentry.cpp @@ -148,10 +148,6 @@ namespace tremotesf { return mChildren; } - const std::unordered_map& TorrentFilesModelDirectory::childrenHash() const { - return mChildrenHash; - } - TorrentFilesModelFile* TorrentFilesModelDirectory::addFile( int id, const QString& name, long long size, long long completedSize, bool wanted, Priority priority ) { @@ -170,10 +166,7 @@ namespace tremotesf { return directoryPtr; } - void TorrentFilesModelDirectory::clearChildren() { - mChildren.clear(); - mChildrenHash.clear(); - } + void TorrentFilesModelDirectory::clearChildren() { mChildren.clear(); } std::vector TorrentFilesModelDirectory::childrenIds() const { std::vector ids{}; @@ -197,7 +190,6 @@ namespace tremotesf { } void TorrentFilesModelDirectory::addChild(std::unique_ptr&& child) { - mChildrenHash.emplace(child->name(), child.get()); mChildren.push_back(std::move(child)); } diff --git a/src/ui/itemmodels/torrentfilesmodelentry.h b/src/ui/itemmodels/torrentfilesmodelentry.h index ab31a28a..3cbb6a9f 100644 --- a/src/ui/itemmodels/torrentfilesmodelentry.h +++ b/src/ui/itemmodels/torrentfilesmodelentry.h @@ -7,7 +7,6 @@ #include #include -#include #include #include @@ -82,7 +81,6 @@ namespace tremotesf { void setPriority(Priority priority) override; const std::vector>& children() const; - const std::unordered_map& childrenHash() const; TorrentFilesModelFile* addFile( int id, @@ -105,7 +103,6 @@ namespace tremotesf { void addChild(std::unique_ptr&& child); std::vector> mChildren; - std::unordered_map mChildrenHash; }; class TorrentFilesModelFile final : public TorrentFilesModelEntry { diff --git a/src/ui/itemmodels/torrentfilestreebuilder.h b/src/ui/itemmodels/torrentfilestreebuilder.h index 5695433a..d322ffde 100644 --- a/src/ui/itemmodels/torrentfilestreebuilder.h +++ b/src/ui/itemmodels/torrentfilestreebuilder.h @@ -7,6 +7,7 @@ #include #include +#include #include "torrentfilesmodelentry.h" @@ -35,6 +36,7 @@ namespace tremotesf { TorrentFilesModelEntry::Priority priority ) { TorrentFilesModelDirectory* currentDirectory = rootDirectory; + DirectoryCacheEntry* currentDirectoryCacheEntry = &mRootDirectoryCacheEntry; const auto lastPartIndex = pathParts.size() - 1; @@ -44,12 +46,17 @@ namespace tremotesf { files.push_back(file); ++mFileId; } else { - const auto& childrenHash = currentDirectory->childrenHash(); - const auto found = childrenHash.find(part); - if (found != childrenHash.end()) { - currentDirectory = static_cast(found->second); + const auto found = currentDirectoryCacheEntry->childDirectoriesCache.find(part); + if (found != currentDirectoryCacheEntry->childDirectoriesCache.constEnd()) { + currentDirectory = found.value().directory; + currentDirectoryCacheEntry = &found.value(); } else { currentDirectory = currentDirectory->addDirectory(part); + const auto inserted = currentDirectoryCacheEntry->childDirectoriesCache.emplace( + part, + DirectoryCacheEntry{.directory = currentDirectory} + ); + currentDirectoryCacheEntry = &inserted.value(); } } } @@ -59,6 +66,12 @@ namespace tremotesf { std::vector files{}; private: + struct DirectoryCacheEntry { + TorrentFilesModelDirectory* directory; + QHash childDirectoriesCache{}; + }; + DirectoryCacheEntry mRootDirectoryCacheEntry{}; + int mFileId{}; }; } diff --git a/src/ui/screens/torrentproperties/torrentfilesmodel.cpp b/src/ui/screens/torrentproperties/torrentfilesmodel.cpp index 5f1d76c6..2076b5bb 100644 --- a/src/ui/screens/torrentproperties/torrentfilesmodel.cpp +++ b/src/ui/screens/torrentproperties/torrentfilesmodel.cpp @@ -142,7 +142,11 @@ namespace tremotesf { TorrentFilesModelEntry* entry = mRootDirectory.get(); const auto parts = path.split('/', Qt::SkipEmptyParts); for (const QString& part : parts) { - entry = static_cast(entry)->childrenHash().at(part); + if (!entry->isDirectory()) return; + const auto& children = static_cast(entry)->children(); + const auto found = std::ranges::find(children, part, &TorrentFilesModelEntry::name); + if (found == children.end()) return; + entry = found->get(); } BaseTorrentFilesModel::fileRenamed(entry, newName); } From bed56e15a4a2821fa4bda3c672812b16bec99d20 Mon Sep 17 00:00:00 2001 From: Alexey Rochev Date: Sun, 5 Oct 2025 01:26:50 +0300 Subject: [PATCH 04/12] Allow non-sized ranges for path parts in TorrentFilesTreeBuilder --- src/ui/itemmodels/torrentfilestreebuilder.h | 24 +++++++++++---------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/ui/itemmodels/torrentfilestreebuilder.h b/src/ui/itemmodels/torrentfilestreebuilder.h index d322ffde..d92eb848 100644 --- a/src/ui/itemmodels/torrentfilestreebuilder.h +++ b/src/ui/itemmodels/torrentfilestreebuilder.h @@ -24,9 +24,7 @@ namespace tremotesf { template requires( - std::ranges::forward_range - && std::ranges::sized_range - && std::same_as> + std::ranges::input_range && std::same_as> ) void addFile( PathParts&& pathParts, @@ -38,14 +36,12 @@ namespace tremotesf { TorrentFilesModelDirectory* currentDirectory = rootDirectory; DirectoryCacheEntry* currentDirectoryCacheEntry = &mRootDirectoryCacheEntry; - const auto lastPartIndex = pathParts.size() - 1; - - for (auto&& [index, part] : pathParts | std::views::enumerate) { - if (static_cast(index) == lastPartIndex) { - auto* const file = currentDirectory->addFile(mFileId, part, size, completedSize, wanted, priority); - files.push_back(file); - ++mFileId; - } else { + auto iter = pathParts.begin(); + while (true) { + auto part = *iter; + ++iter; + if (iter != pathParts.end()) { + // This was not the last part, therefore a directory name const auto found = currentDirectoryCacheEntry->childDirectoriesCache.find(part); if (found != currentDirectoryCacheEntry->childDirectoriesCache.constEnd()) { currentDirectory = found.value().directory; @@ -58,6 +54,12 @@ namespace tremotesf { ); currentDirectoryCacheEntry = &inserted.value(); } + } else { + // This was the last part, therefore a file name + auto* const file = currentDirectory->addFile(mFileId, part, size, completedSize, wanted, priority); + files.push_back(file); + ++mFileId; + break; } } } From 4a5057bfce1ade1866268bd1a4fc46c6bb9be71c Mon Sep 17 00:00:00 2001 From: Alexey Rochev Date: Sun, 5 Oct 2025 01:28:33 +0300 Subject: [PATCH 05/12] Allow ranges of QStringView in TorrentFilesTreeBuilder --- src/ui/itemmodels/torrentfilestreebuilder.h | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/ui/itemmodels/torrentfilestreebuilder.h b/src/ui/itemmodels/torrentfilestreebuilder.h index d92eb848..edc27d19 100644 --- a/src/ui/itemmodels/torrentfilestreebuilder.h +++ b/src/ui/itemmodels/torrentfilestreebuilder.h @@ -12,6 +12,14 @@ #include "torrentfilesmodelentry.h" namespace tremotesf { + namespace impl { + template + concept QStringOrView = std::same_as || std::same_as; + + inline QString toString(QStringView view) { return view.toString(); } + inline const QString& toString(const QString& string) { return string; } + } + class TorrentFilesTreeBuilder final { public: explicit TorrentFilesTreeBuilder(TorrentFilesModelDirectory* rootDirectory, size_t reserveFilesCount) @@ -23,9 +31,7 @@ namespace tremotesf { Q_DISABLE_COPY_MOVE(TorrentFilesTreeBuilder) template - requires( - std::ranges::input_range && std::same_as> - ) + requires(std::ranges::input_range && impl::QStringOrView>) void addFile( PathParts&& pathParts, long long size, @@ -47,16 +53,18 @@ namespace tremotesf { currentDirectory = found.value().directory; currentDirectoryCacheEntry = &found.value(); } else { - currentDirectory = currentDirectory->addDirectory(part); + const auto& partString = impl::toString(part); + currentDirectory = currentDirectory->addDirectory(partString); const auto inserted = currentDirectoryCacheEntry->childDirectoriesCache.emplace( - part, + partString, DirectoryCacheEntry{.directory = currentDirectory} ); currentDirectoryCacheEntry = &inserted.value(); } } else { // This was the last part, therefore a file name - auto* const file = currentDirectory->addFile(mFileId, part, size, completedSize, wanted, priority); + auto* const file = + currentDirectory->addFile(mFileId, impl::toString(part), size, completedSize, wanted, priority); files.push_back(file); ++mFileId; break; From 553f58c9d51b68ca48014b952da4b49ca6fa6213 Mon Sep 17 00:00:00 2001 From: Alexey Rochev Date: Sun, 5 Oct 2025 01:28:57 +0300 Subject: [PATCH 06/12] Don't allocate vector when splitting file path from RPC --- src/rpc/torrentfile.cpp | 7 +------ src/rpc/torrentfile.h | 5 +++-- src/ui/screens/torrentproperties/torrentfilesmodel.cpp | 2 +- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/rpc/torrentfile.cpp b/src/rpc/torrentfile.cpp index 31f6aaa8..e7af8e94 100644 --- a/src/rpc/torrentfile.cpp +++ b/src/rpc/torrentfile.cpp @@ -24,12 +24,7 @@ namespace tremotesf { } TorrentFile::TorrentFile(int id, const QJsonObject& fileMap, const QJsonObject& fileStatsMap) - : id(id), size(fileMap.value("length"_L1).toInteger()) { - auto p = fileMap.value("name"_L1).toString().split('/', Qt::SkipEmptyParts); - path.reserve(static_cast(p.size())); - for (QString& part : p) { - path.push_back(std::move(part)); - } + : id(id), path(fileMap.value("name"_L1).toString()), size(fileMap.value("length"_L1).toInteger()) { update(fileStatsMap); } diff --git a/src/rpc/torrentfile.h b/src/rpc/torrentfile.h index f5fb0092..4d2c9f96 100644 --- a/src/rpc/torrentfile.h +++ b/src/rpc/torrentfile.h @@ -5,7 +5,6 @@ #ifndef TREMOTESF_RPC_TORRENTFILE_H #define TREMOTESF_RPC_TORRENTFILE_H -#include #include #include @@ -23,11 +22,13 @@ namespace tremotesf { int id{}; - std::vector path{}; + QString path{}; qint64 size{}; qint64 completedSize{}; Priority priority{}; bool wanted{}; + + auto pathParts() const { return path.tokenize(u'/', Qt::SkipEmptyParts); } }; } diff --git a/src/ui/screens/torrentproperties/torrentfilesmodel.cpp b/src/ui/screens/torrentproperties/torrentfilesmodel.cpp index 2076b5bb..a9f9b3bc 100644 --- a/src/ui/screens/torrentproperties/torrentfilesmodel.cpp +++ b/src/ui/screens/torrentproperties/torrentfilesmodel.cpp @@ -59,7 +59,7 @@ namespace tremotesf { TorrentFilesTreeBuilder builder(rootDirectory.get(), files.size()); for (const TorrentFile& file : files) { builder.addFile( - file.path, + file.pathParts(), file.size, file.completedSize, file.wanted, From 77f9b3370791b7cd889b9240a27bf3996b0e2fac Mon Sep 17 00:00:00 2001 From: Alexey Rochev Date: Sun, 5 Oct 2025 01:33:18 +0300 Subject: [PATCH 07/12] Remove unused id field from TorrentFile --- src/rpc/torrent.cpp | 2 +- src/rpc/torrentfile.cpp | 4 ++-- src/rpc/torrentfile.h | 4 +--- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/rpc/torrent.cpp b/src/rpc/torrent.cpp index 3a328b09..cb7ad781 100644 --- a/src/rpc/torrent.cpp +++ b/src/rpc/torrent.cpp @@ -802,7 +802,7 @@ namespace tremotesf { mFiles.reserve(static_cast(count)); changed.reserve(static_cast(count)); for (QJsonArray::size_type i = 0; i < count; ++i) { - mFiles.emplace_back(i, fileJsons[i].toObject(), fileStats[i].toObject()); + mFiles.emplace_back(fileJsons[i].toObject(), fileStats[i].toObject()); changed.push_back(static_cast(i)); } } else { diff --git a/src/rpc/torrentfile.cpp b/src/rpc/torrentfile.cpp index e7af8e94..d05ee7da 100644 --- a/src/rpc/torrentfile.cpp +++ b/src/rpc/torrentfile.cpp @@ -23,8 +23,8 @@ namespace tremotesf { ); } - TorrentFile::TorrentFile(int id, const QJsonObject& fileMap, const QJsonObject& fileStatsMap) - : id(id), path(fileMap.value("name"_L1).toString()), size(fileMap.value("length"_L1).toInteger()) { + TorrentFile::TorrentFile(const QJsonObject& fileMap, const QJsonObject& fileStatsMap) + : path(fileMap.value("name"_L1).toString()), size(fileMap.value("length"_L1).toInteger()) { update(fileStatsMap); } diff --git a/src/rpc/torrentfile.h b/src/rpc/torrentfile.h index 4d2c9f96..8694793b 100644 --- a/src/rpc/torrentfile.h +++ b/src/rpc/torrentfile.h @@ -17,11 +17,9 @@ namespace tremotesf { enum class Priority { Low, Normal, High }; Q_ENUM(Priority) - explicit TorrentFile(int id, const QJsonObject& fileMap, const QJsonObject& fileStatsMap); + explicit TorrentFile(const QJsonObject& fileMap, const QJsonObject& fileStatsMap); bool update(const QJsonObject& fileStatsMap); - int id{}; - QString path{}; qint64 size{}; qint64 completedSize{}; From fde9d4aa8fb2fd485faedc399e29b820c97a5889 Mon Sep 17 00:00:00 2001 From: Alexey Rochev Date: Fri, 3 Apr 2026 23:48:54 +0300 Subject: [PATCH 08/12] Add TorrentFilesModelTest --- src/CMakeLists.txt | 4 + src/ui/itemmodels/basetorrentfilesmodel.cpp | 25 + src/ui/itemmodels/basetorrentfilesmodel.h | 7 +- src/ui/itemmodels/torrentfilesmodel_test.cpp | 652 ++++++++++++++++++ .../addtorrent/localtorrentfilesmodel.h | 1 - .../torrentproperties/torrentfilesmodel.cpp | 37 +- .../torrentproperties/torrentfilesmodel.h | 1 - 7 files changed, 694 insertions(+), 33 deletions(-) create mode 100644 src/ui/itemmodels/torrentfilesmodel_test.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a80143cb..7f0fec46 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -494,6 +494,10 @@ if (BUILD_TESTING) add_executable(torrentfileparser_test torrentfileparser_test.cpp) add_test(NAME torrentfileparser_test COMMAND torrentfileparser_test WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/test-torrents") target_link_libraries(torrentfileparser_test PRIVATE tremotesf_objects Qt::Test) + + add_executable(torrentfilesmodel_test ui/itemmodels/torrentfilesmodel_test.cpp) + add_test(NAME torrentfilesmodel_test COMMAND torrentfilesmodel_test) + target_link_libraries(torrentfilesmodel_test PRIVATE tremotesf_objects Qt::Test) endif () set_common_options_on_targets() diff --git a/src/ui/itemmodels/basetorrentfilesmodel.cpp b/src/ui/itemmodels/basetorrentfilesmodel.cpp index 3c68d11d..46c18fb4 100644 --- a/src/ui/itemmodels/basetorrentfilesmodel.cpp +++ b/src/ui/itemmodels/basetorrentfilesmodel.cpp @@ -231,4 +231,29 @@ namespace tremotesf { } changedBatchProcessor.commitIfNeeded(); } + + void BaseTorrentFilesModel::updateFiles( + std::span changedFiles, std::function&& updateFile + ) { + if (!changedFiles.empty()) { + auto changedIter(changedFiles.begin()); + int changedIndex = *changedIter; + const auto changedEnd(changedFiles.end()); + + for (int i = 0, max = static_cast(mFiles.size()); i < max; ++i) { + const auto& file = mFiles[static_cast(i)]; + file->setChanged(false); + if (i == changedIndex) { + updateFile(static_cast(i), file); + ++changedIter; + if (changedIter == changedEnd) { + changedIndex = -1; + } else { + changedIndex = *changedIter; + } + } + } + updateDirectoryChildren(); + } + } } diff --git a/src/ui/itemmodels/basetorrentfilesmodel.h b/src/ui/itemmodels/basetorrentfilesmodel.h index 8b968ed9..8db9f453 100644 --- a/src/ui/itemmodels/basetorrentfilesmodel.h +++ b/src/ui/itemmodels/basetorrentfilesmodel.h @@ -5,6 +5,7 @@ #ifndef TREMOTESF_BASETORRENTFILESMODEL_H #define TREMOTESF_BASETORRENTFILESMODEL_H +#include #include #include #include @@ -41,8 +42,12 @@ namespace tremotesf { protected: void updateDirectoryChildren(const QModelIndex& parent = QModelIndex()); + void updateFiles( + std::span changedFiles, std::function&& updateFile + ); - std::unique_ptr mRootDirectory; + std::unique_ptr mRootDirectory{}; + std::vector mFiles{}; private: const std::vector mColumns; diff --git a/src/ui/itemmodels/torrentfilesmodel_test.cpp b/src/ui/itemmodels/torrentfilesmodel_test.cpp new file mode 100644 index 00000000..28c94fa2 --- /dev/null +++ b/src/ui/itemmodels/torrentfilesmodel_test.cpp @@ -0,0 +1,652 @@ +// SPDX-FileCopyrightText: 2015-2025 Alexey Rochev +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#define QTEST_THROW_ON_FAIL + +#include +#include + +#include +#include + +#include "basetorrentfilesmodel.h" +#include "torrentfilestreebuilder.h" +#include "formatutils.h" +#include "log/log.h" + +#include + +SPECIALIZE_FORMATTER_FOR_QDEBUG(QVariant) + +using namespace Qt::StringLiterals; + +namespace tremotesf { + using formatutils::formatByteSize; + using formatutils::formatProgress; + + namespace { + QModelIndex indexForPath(QLatin1String path, QAbstractItemModel& model) { + QModelIndex index{}; + for (const auto& part : path.tokenize(u'/')) { + const auto childIndexes = + std::views::iota(0, model.rowCount(index)) | std::views::transform([&](int row) { + return model.index(row, static_cast(BaseTorrentFilesModel::Column::Name), index); + }); + const auto found = std::ranges::find(childIndexes, part, [&](const QModelIndex& childIndex) { + return childIndex.data().toString(); + }); + if (found == childIndexes.end()) { + warning().log("Did not find child with name {}", part); + QFAIL("Nope"); + } + index = *found; + } + return index; + } + + std::pair + expectedDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { + return {topLeft.siblingAtColumn(0), bottomRight.siblingAtColumn(bottomRight.model()->columnCount() - 1)}; + } + + std::pair expectedDataChanged(const QModelIndex& index) { + return expectedDataChanged(index, index); + } + + std::set> actualDataChanged(QSignalSpy& spy) { + return spy + | std::views::transform([](QList args) { + return std::pair{args.at(0).toModelIndex(), args.at(1).toModelIndex()}; + }) + | std::ranges::to(); + } + } + + class TestTorrentFilesModel : public BaseTorrentFilesModel { + Q_OBJECT + public: + TestTorrentFilesModel() + : BaseTorrentFilesModel( + {Column::Name, Column::Size, Column::ProgressBar, Column::Progress, Column::Priority} + ) {} + + void renameFile(const QModelIndex&, const QString&) override {} + + struct File { + QString path; + qint64 size; + qint64 completedSize; + TorrentFilesModelEntry::Priority priority; + bool wanted; + }; + + void populate() { + const std::array files{ + File{ + .path = "topdir/subdir1/subsubddir/file1", + .size = 666, + .completedSize = 0, + .priority = TorrentFilesModelEntry::Priority::Normal, + .wanted = true + }, + File{ + .path = "topdir/subdir1/subsubddir/file2", + .size = 100000, + .completedSize = 4234, + .priority = TorrentFilesModelEntry::Priority::Low, + .wanted = false + }, + File{ + .path = "topdir/subdir2/file1", + .size = 3333333, + .completedSize = 0, + .priority = TorrentFilesModelEntry::Priority::Normal, + .wanted = true + }, + File{ + .path = "topdir/subdir2/file2", + .size = 111, + .completedSize = 0, + .priority = TorrentFilesModelEntry::Priority::Normal, + .wanted = true + } + }; + mRootDirectory = std::make_unique(); + TorrentFilesTreeBuilder builder(mRootDirectory.get(), files.size()); + for (const File& file : files) { + builder.addFile( + file.path.tokenize(u'/', Qt::SkipEmptyParts), + file.size, + file.completedSize, + file.wanted, + file.priority + ); + } + mFiles = std::move(builder.files); + } + + using BaseTorrentFilesModel::updateFiles; + }; + + class TorrentFilesModelTest : public QObject { + Q_OBJECT + + private slots: + void checkInitialState() { + TestTorrentFilesModel model{}; + model.populate(); + + checkTree( + model, + {.name = "topdir"_L1, + .size = 3434110, + .progress = 4234.0 / 3434110.0, + .priority = TorrentFilesModelEntry::Priority::Mixed, + .checkState = Qt::CheckState::PartiallyChecked, + + .children = { + {.name = "subdir1"_L1, + .size = 100666, + .progress = 4234.0 / 100666.0, + .priority = TorrentFilesModelEntry::Priority::Mixed, + .checkState = Qt::CheckState::PartiallyChecked, + + .children = + {{.name = "subsubddir"_L1, + .size = 100666, + .progress = 4234.0 / 100666.0, + .priority = TorrentFilesModelEntry::Priority::Mixed, + .checkState = Qt::CheckState::PartiallyChecked, + + .children = + {{.name = "file1"_L1, + .size = 666, + .progress = 0.0, + .priority = TorrentFilesModelEntry::Priority::Normal, + .checkState = Qt::CheckState::Checked}, + {.name = "file2"_L1, + .size = 100000, + .progress = 4234.0 / 100666.0, + .priority = TorrentFilesModelEntry::Priority::Low, + .checkState = Qt::CheckState::Unchecked}}}}}, + + {.name = "subdir2"_L1, + .size = 3333444, + .progress = 0.0, + .priority = TorrentFilesModelEntry::Priority::Normal, + .checkState = Qt::CheckState::Checked, + + .children = { + {.name = "file1"_L1, + .size = 3333333, + .progress = 0.0, + .priority = TorrentFilesModelEntry::Priority::Normal, + .checkState = Qt::CheckState::Checked}, + {.name = "file2"_L1, + .size = 111, + .progress = 0.0, + .priority = TorrentFilesModelEntry::Priority::Normal, + .checkState = Qt::CheckState::Checked} + }} + }} + ); + } + + void checkSetFilesWantedFromRoot() { + TestTorrentFilesModel model{}; + model.populate(); + + QSignalSpy dataChanged(&model, &QAbstractItemModel::dataChanged); + + model.setFilesWanted({model.index(0, 0)}, false); + + const auto actualSignals = actualDataChanged(dataChanged); + const auto expectedSignals = std::set{ + expectedDataChanged(indexForPath("topdir/subdir1/subsubddir/file1"_L1, model)), + expectedDataChanged(indexForPath("topdir/subdir1/subsubddir"_L1, model)), + expectedDataChanged( + indexForPath("topdir/subdir2/file1"_L1, model), + indexForPath("topdir/subdir2/file2"_L1, model) + ), + expectedDataChanged(indexForPath("topdir/subdir1"_L1, model), indexForPath("topdir/subdir2"_L1, model)), + expectedDataChanged(indexForPath("topdir"_L1, model)) + }; + + QCOMPARE(actualSignals, expectedSignals); + + checkTree( + model, + {.name = "topdir"_L1, + .size = 3434110, + .progress = 4234.0 / 3434110.0, + .priority = TorrentFilesModelEntry::Priority::Mixed, + .checkState = Qt::CheckState::Unchecked, + + .children = { + {.name = "subdir1"_L1, + .size = 100666, + .progress = 4234.0 / 100666.0, + .priority = TorrentFilesModelEntry::Priority::Mixed, + .checkState = Qt::CheckState::Unchecked, + + .children = + {{.name = "subsubddir"_L1, + .size = 100666, + .progress = 4234.0 / 100666.0, + .priority = TorrentFilesModelEntry::Priority::Mixed, + .checkState = Qt::CheckState::Unchecked, + + .children = + {{.name = "file1"_L1, + .size = 666, + .progress = 0.0, + .priority = TorrentFilesModelEntry::Priority::Normal, + .checkState = Qt::CheckState::Unchecked}, + {.name = "file2"_L1, + .size = 100000, + .progress = 4234.0 / 100666.0, + .priority = TorrentFilesModelEntry::Priority::Low, + .checkState = Qt::CheckState::Unchecked}}}}}, + + {.name = "subdir2"_L1, + .size = 3333444, + .progress = 0.0, + .priority = TorrentFilesModelEntry::Priority::Normal, + .checkState = Qt::CheckState::Unchecked, + + .children = { + {.name = "file1"_L1, + .size = 3333333, + .progress = 0.0, + .priority = TorrentFilesModelEntry::Priority::Normal, + .checkState = Qt::CheckState::Unchecked}, + {.name = "file2"_L1, + .size = 111, + .progress = 0.0, + .priority = TorrentFilesModelEntry::Priority::Normal, + .checkState = Qt::CheckState::Unchecked} + }} + }} + ); + } + + void checkSetFilesWantedFromFile() { + TestTorrentFilesModel model{}; + model.populate(); + + QSignalSpy dataChanged(&model, &QAbstractItemModel::dataChanged); + + model.setFilesWanted({indexForPath("topdir/subdir1/subsubddir/file2"_L1, model)}, true); + + const auto actualSignals = actualDataChanged(dataChanged); + const auto expectedSignals = std::set{ + expectedDataChanged(indexForPath("topdir/subdir1/subsubddir/file2"_L1, model)), + expectedDataChanged(indexForPath("topdir/subdir1/subsubddir"_L1, model)), + expectedDataChanged(indexForPath("topdir/subdir1"_L1, model)), + expectedDataChanged(indexForPath("topdir"_L1, model)) + }; + QCOMPARE(actualSignals, expectedSignals); + + checkTree( + model, + {.name = "topdir"_L1, + .size = 3434110, + .progress = 4234.0 / 3434110.0, + .priority = TorrentFilesModelEntry::Priority::Mixed, + .checkState = Qt::CheckState::Checked, + + .children = { + {.name = "subdir1"_L1, + .size = 100666, + .progress = 4234.0 / 100666.0, + .priority = TorrentFilesModelEntry::Priority::Mixed, + .checkState = Qt::CheckState::Checked, + + .children = + {{.name = "subsubddir"_L1, + .size = 100666, + .progress = 4234.0 / 100666.0, + .priority = TorrentFilesModelEntry::Priority::Mixed, + .checkState = Qt::CheckState::Checked, + + .children = + {{.name = "file1"_L1, + .size = 666, + .progress = 0.0, + .priority = TorrentFilesModelEntry::Priority::Normal, + .checkState = Qt::CheckState::Checked}, + {.name = "file2"_L1, + .size = 100000, + .progress = 4234.0 / 100666.0, + .priority = TorrentFilesModelEntry::Priority::Low, + .checkState = Qt::CheckState::Checked}}}}}, + + {.name = "subdir2"_L1, + .size = 3333444, + .progress = 0.0, + .priority = TorrentFilesModelEntry::Priority::Normal, + .checkState = Qt::CheckState::Checked, + + .children = { + {.name = "file1"_L1, + .size = 3333333, + .progress = 0.0, + .priority = TorrentFilesModelEntry::Priority::Normal, + .checkState = Qt::CheckState::Checked}, + {.name = "file2"_L1, + .size = 111, + .progress = 0.0, + .priority = TorrentFilesModelEntry::Priority::Normal, + .checkState = Qt::CheckState::Checked} + }} + }} + ); + } + + void checkSetFilesPriorityFromRoot() { + TestTorrentFilesModel model{}; + model.populate(); + + QSignalSpy dataChanged(&model, &QAbstractItemModel::dataChanged); + + model.setFilesPriority({model.index(0, 0)}, TorrentFilesModelEntry::Priority::Low); + + const auto actualSignals = actualDataChanged(dataChanged); + const auto expectedSignals = std::set{ + expectedDataChanged(indexForPath("topdir/subdir1/subsubddir/file1"_L1, model)), + expectedDataChanged(indexForPath("topdir/subdir1/subsubddir"_L1, model)), + expectedDataChanged( + indexForPath("topdir/subdir2/file1"_L1, model), + indexForPath("topdir/subdir2/file2"_L1, model) + ), + expectedDataChanged(indexForPath("topdir/subdir1"_L1, model), indexForPath("topdir/subdir2"_L1, model)), + expectedDataChanged(indexForPath("topdir"_L1, model)) + }; + QCOMPARE(actualSignals, expectedSignals); + + checkTree( + model, + {.name = "topdir"_L1, + .size = 3434110, + .progress = 4234.0 / 3434110.0, + .priority = TorrentFilesModelEntry::Priority::Low, + .checkState = Qt::CheckState::PartiallyChecked, + + .children = { + {.name = "subdir1"_L1, + .size = 100666, + .progress = 4234.0 / 100666.0, + .priority = TorrentFilesModelEntry::Priority::Low, + .checkState = Qt::CheckState::PartiallyChecked, + + .children = + {{.name = "subsubddir"_L1, + .size = 100666, + .progress = 4234.0 / 100666.0, + .priority = TorrentFilesModelEntry::Priority::Low, + .checkState = Qt::CheckState::PartiallyChecked, + + .children = + {{.name = "file1"_L1, + .size = 666, + .progress = 0.0, + .priority = TorrentFilesModelEntry::Priority::Low, + .checkState = Qt::CheckState::Checked}, + {.name = "file2"_L1, + .size = 100000, + .progress = 4234.0 / 100666.0, + .priority = TorrentFilesModelEntry::Priority::Low, + .checkState = Qt::CheckState::Unchecked}}}}}, + + {.name = "subdir2"_L1, + .size = 3333444, + .progress = 0.0, + .priority = TorrentFilesModelEntry::Priority::Low, + .checkState = Qt::CheckState::Checked, + + .children = { + {.name = "file1"_L1, + .size = 3333333, + .progress = 0.0, + .priority = TorrentFilesModelEntry::Priority::Low, + .checkState = Qt::CheckState::Checked}, + {.name = "file2"_L1, + .size = 111, + .progress = 0.0, + .priority = TorrentFilesModelEntry::Priority::Low, + .checkState = Qt::CheckState::Checked} + }} + }} + ); + } + + void checkSetFilesPriorityFromFile() { + TestTorrentFilesModel model{}; + model.populate(); + + QSignalSpy dataChanged(&model, &QAbstractItemModel::dataChanged); + + model.setFilesPriority( + {indexForPath("topdir/subdir1/subsubddir/file2"_L1, model)}, + TorrentFilesModelEntry::Priority::Normal + ); + + const auto actualSignals = actualDataChanged(dataChanged); + const auto expectedSignals = std::set{ + expectedDataChanged(indexForPath("topdir/subdir1/subsubddir/file2"_L1, model)), + expectedDataChanged(indexForPath("topdir/subdir1/subsubddir"_L1, model)), + expectedDataChanged(indexForPath("topdir/subdir1"_L1, model)), + expectedDataChanged(indexForPath("topdir"_L1, model)) + }; + QCOMPARE(actualSignals, expectedSignals); + + checkTree( + model, + {.name = "topdir"_L1, + .size = 3434110, + .progress = 4234.0 / 3434110.0, + .priority = TorrentFilesModelEntry::Priority::Normal, + .checkState = Qt::CheckState::PartiallyChecked, + + .children = { + {.name = "subdir1"_L1, + .size = 100666, + .progress = 4234.0 / 100666.0, + .priority = TorrentFilesModelEntry::Priority::Normal, + .checkState = Qt::CheckState::PartiallyChecked, + + .children = + {{.name = "subsubddir"_L1, + .size = 100666, + .progress = 4234.0 / 100666.0, + .priority = TorrentFilesModelEntry::Priority::Normal, + .checkState = Qt::CheckState::PartiallyChecked, + + .children = + {{.name = "file1"_L1, + .size = 666, + .progress = 0.0, + .priority = TorrentFilesModelEntry::Priority::Normal, + .checkState = Qt::CheckState::Checked}, + {.name = "file2"_L1, + .size = 100000, + .progress = 4234.0 / 100666.0, + .priority = TorrentFilesModelEntry::Priority::Normal, + .checkState = Qt::CheckState::Unchecked}}}}}, + + {.name = "subdir2"_L1, + .size = 3333444, + .progress = 0.0, + .priority = TorrentFilesModelEntry::Priority::Normal, + .checkState = Qt::CheckState::Checked, + + .children = { + {.name = "file1"_L1, + .size = 3333333, + .progress = 0.0, + .priority = TorrentFilesModelEntry::Priority::Normal, + .checkState = Qt::CheckState::Checked}, + {.name = "file2"_L1, + .size = 111, + .progress = 0.0, + .priority = TorrentFilesModelEntry::Priority::Normal, + .checkState = Qt::CheckState::Checked} + }} + }} + ); + } + + void checkUpdateFiles() { + TestTorrentFilesModel model{}; + model.populate(); + + std::array changed{TestTorrentFilesModel::File{}}; + + QSignalSpy dataChanged(&model, &QAbstractItemModel::dataChanged); + + model.updateFiles(std::array{0, 1}, [](size_t index, TorrentFilesModelFile* file) { + switch (index) { + case 0: + file->setWanted(true); + file->setCompletedSize(0); + file->setPriority(TorrentFilesModelEntry::Priority::High); + break; + case 1: + file->setWanted(true); + file->setCompletedSize(0); + file->setPriority(TorrentFilesModelEntry::Priority::Normal); + break; + } + }); + + const auto actualSignals = actualDataChanged(dataChanged); + const auto expectedSignals = std::set{ + expectedDataChanged( + indexForPath("topdir/subdir1/subsubddir/file1"_L1, model), + indexForPath("topdir/subdir1/subsubddir/file2"_L1, model) + ), + expectedDataChanged(indexForPath("topdir/subdir1/subsubddir"_L1, model)), + expectedDataChanged(indexForPath("topdir/subdir1"_L1, model)), + expectedDataChanged(indexForPath("topdir"_L1, model)) + }; + QCOMPARE(actualSignals, expectedSignals); + + checkTree( + model, + {.name = "topdir"_L1, + .size = 3434110, + .progress = 0.0, + .priority = TorrentFilesModelEntry::Priority::Mixed, + .checkState = Qt::CheckState::Checked, + + .children = { + {.name = "subdir1"_L1, + .size = 100666, + .progress = 0.0, + .priority = TorrentFilesModelEntry::Priority::Mixed, + .checkState = Qt::CheckState::Checked, + + .children = + {{.name = "subsubddir"_L1, + .size = 100666, + .progress = 0.0, + .priority = TorrentFilesModelEntry::Priority::Mixed, + .checkState = Qt::CheckState::Checked, + + .children = + {{.name = "file1"_L1, + .size = 666, + .progress = 0.0, + .priority = TorrentFilesModelEntry::Priority::High, + .checkState = Qt::CheckState::Checked}, + {.name = "file2"_L1, + .size = 100000, + .progress = 0.0, + .priority = TorrentFilesModelEntry::Priority::Normal, + .checkState = Qt::CheckState::Checked}}}}}, + + {.name = "subdir2"_L1, + .size = 3333444, + .progress = 0.0, + .priority = TorrentFilesModelEntry::Priority::Normal, + .checkState = Qt::CheckState::Checked, + + .children = { + {.name = "file1"_L1, + .size = 3333333, + .progress = 0.0, + .priority = TorrentFilesModelEntry::Priority::Normal, + .checkState = Qt::CheckState::Checked}, + {.name = "file2"_L1, + .size = 111, + .progress = 0.0, + .priority = TorrentFilesModelEntry::Priority::Normal, + .checkState = Qt::CheckState::Checked} + }} + }} + ); + } + + private: + struct ExpectedData { + QLatin1String name; + long long size; + double progress; + TorrentFilesModelEntry::Priority priority; + Qt::CheckState checkState; + + std::vector children{}; + }; + + void checkTree(BaseTorrentFilesModel& model, const ExpectedData& expectedRootEntry) { + checkEntryPresentation(model, {}, 0, expectedRootEntry, 0); + } + + void checkEntryPresentation( + BaseTorrentFilesModel& model, const QModelIndex& parent, int row, const ExpectedData& expected, int depth + ) { + info().log("{}* {}", u" "_s.repeated(depth), expected.name); + + const auto data = [&](BaseTorrentFilesModel::Column column, int role = Qt::DisplayRole) { + return model.index(row, static_cast(column), parent).data(role); + }; + + QCOMPARE(data(BaseTorrentFilesModel::Column::Name).toString(), expected.name); + QCOMPARE(data(BaseTorrentFilesModel::Column::Size).toString(), formatByteSize(expected.size)); + QCOMPARE(data(BaseTorrentFilesModel::Column::Progress).toString(), formatProgress(expected.progress)); + QCOMPARE( + data(BaseTorrentFilesModel::Column::Name, Qt::CheckStateRole).value(), + expected.checkState + ); + const auto expectedPriorityString = [&] { + switch (expected.priority) { + case TorrentFilesModelEntry::Priority::Low: + return qApp->translate("tremotesf", "Low"); + case TorrentFilesModelEntry::Priority::Normal: + return qApp->translate("tremotesf", "Normal"); + case TorrentFilesModelEntry::Priority::High: + return qApp->translate("tremotesf", "High"); + case TorrentFilesModelEntry::Priority::Mixed: + return qApp->translate("tremotesf", "Mixed"); + } + return QString{}; + }(); + QCOMPARE(data(BaseTorrentFilesModel::Column::Priority).toString(), expectedPriorityString); + + const auto thisIndex = model.index(row, 0, parent); + QCOMPARE(model.rowCount(thisIndex), static_cast(expected.children.size())); + if (!expected.children.empty()) { + int i = 0; + for (const auto& child : expected.children) { + checkEntryPresentation(model, thisIndex, i, child, depth + 1); + ++i; + } + } + } + }; +} + +QTEST_GUILESS_MAIN(tremotesf::TorrentFilesModelTest) + +#include "torrentfilesmodel_test.moc" diff --git a/src/ui/screens/addtorrent/localtorrentfilesmodel.h b/src/ui/screens/addtorrent/localtorrentfilesmodel.h index f7ce429c..f5d48798 100644 --- a/src/ui/screens/addtorrent/localtorrentfilesmodel.h +++ b/src/ui/screens/addtorrent/localtorrentfilesmodel.h @@ -36,7 +36,6 @@ namespace tremotesf { void renameFile(const QModelIndex& index, const QString& newName) override; private: - std::vector mFiles{}; bool mLoaded{}; std::map mRenamedFiles{}; diff --git a/src/ui/screens/torrentproperties/torrentfilesmodel.cpp b/src/ui/screens/torrentproperties/torrentfilesmodel.cpp index a9f9b3bc..aded713f 100644 --- a/src/ui/screens/torrentproperties/torrentfilesmodel.cpp +++ b/src/ui/screens/torrentproperties/torrentfilesmodel.cpp @@ -18,13 +18,6 @@ using namespace Qt::StringLiterals; namespace tremotesf { namespace { - void updateFile(TorrentFilesModelFile* treeFile, const TorrentFile& file) { - treeFile->setChanged(false); - treeFile->setCompletedSize(file.completedSize); - treeFile->setWanted(file.wanted); - treeFile->setPriority(TorrentFilesModelEntry::fromFilePriority(file.priority)); - } - std::vector idsFromIndex(const QModelIndex& index) { auto entry = static_cast(index.internalPointer()); if (entry->isDirectory()) { @@ -212,29 +205,13 @@ namespace tremotesf { } void TorrentFilesModel::updateTree(std::span changed) { - if (!changed.empty()) { - const auto& torrentFiles = mTorrent->files(); - - auto changedIter(changed.begin()); - int changedIndex = *changedIter; - const auto changedEnd(changed.end()); - - for (int i = 0, max = static_cast(mFiles.size()); i < max; ++i) { - const auto& file = mFiles[static_cast(i)]; - if (i == changedIndex) { - updateFile(file, torrentFiles.at(static_cast(changedIndex))); - ++changedIter; - if (changedIter == changedEnd) { - changedIndex = -1; - } else { - changedIndex = *changedIter; - } - } else { - file->setChanged(false); - } - } - updateDirectoryChildren(); - } + const auto& torrentFiles = mTorrent->files(); + updateFiles(changed, [&](size_t index, TorrentFilesModelFile* file) { + const auto& json = torrentFiles.at(index); + file->setCompletedSize(json.completedSize); + file->setWanted(json.wanted); + file->setPriority(TorrentFilesModelEntry::fromFilePriority(json.priority)); + }); } void TorrentFilesModel::setLoaded(bool loaded) { mLoaded = loaded; } diff --git a/src/ui/screens/torrentproperties/torrentfilesmodel.h b/src/ui/screens/torrentproperties/torrentfilesmodel.h index 3c6657de..964a750b 100644 --- a/src/ui/screens/torrentproperties/torrentfilesmodel.h +++ b/src/ui/screens/torrentproperties/torrentfilesmodel.h @@ -50,7 +50,6 @@ namespace tremotesf { Torrent* mTorrent{}; Rpc* mRpc{}; - std::vector mFiles{}; bool mCreatingTree{}; bool mLoaded{}; CoroutineScope mCoroutineScope{}; From a518d47e324f6f8987b70a74d1c2887024284478 Mon Sep 17 00:00:00 2001 From: Alexey Rochev Date: Sat, 4 Apr 2026 00:01:36 +0300 Subject: [PATCH 09/12] Fix dataChanged signal not being emitted when files' priority is changed from UI --- src/ui/itemmodels/torrentfilesmodelentry.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ui/itemmodels/torrentfilesmodelentry.cpp b/src/ui/itemmodels/torrentfilesmodelentry.cpp index 0395aefc..e782ef03 100644 --- a/src/ui/itemmodels/torrentfilesmodelentry.cpp +++ b/src/ui/itemmodels/torrentfilesmodelentry.cpp @@ -245,6 +245,7 @@ namespace tremotesf { void TorrentFilesModelFile::setPriority(Priority priority) { if (priority != mPriority) { mPriority = priority; + mChanged = true; } } From 577dcdbaed73b96d937e8b19628c134e0d47f1db Mon Sep 17 00:00:00 2001 From: Alexey Rochev Date: Sat, 4 Apr 2026 00:06:36 +0300 Subject: [PATCH 10/12] Calculate directory properties only when they change and store them, and remove changed flag from TorrentFilesModelFile --- src/ui/itemmodels/basetorrentfilesmodel.cpp | 211 ++++++++++++------ src/ui/itemmodels/basetorrentfilesmodel.h | 3 - src/ui/itemmodels/torrentfilesmodel_test.cpp | 9 +- src/ui/itemmodels/torrentfilesmodelentry.cpp | 204 ++++++----------- src/ui/itemmodels/torrentfilesmodelentry.h | 98 ++++---- src/ui/itemmodels/torrentfilestreebuilder.h | 26 +++ .../addtorrent/localtorrentfilesmodel.cpp | 1 + .../torrentproperties/torrentfilesmodel.cpp | 29 +-- .../torrentproperties/torrentfilesmodel.h | 3 - 9 files changed, 288 insertions(+), 296 deletions(-) diff --git a/src/ui/itemmodels/basetorrentfilesmodel.cpp b/src/ui/itemmodels/basetorrentfilesmodel.cpp index 46c18fb4..f10ccf60 100644 --- a/src/ui/itemmodels/basetorrentfilesmodel.cpp +++ b/src/ui/itemmodels/basetorrentfilesmodel.cpp @@ -4,11 +4,14 @@ #include "basetorrentfilesmodel.h" +#include + #include +#include #include +#include #include "desktoputils.h" -#include "itemlistupdater.h" #include "formatutils.h" namespace tremotesf { @@ -120,7 +123,7 @@ namespace tremotesf { return false; } if (static_cast(index.column()) == Column::Name && role == Qt::CheckStateRole) { - setFileWanted(index, (value.toInt() == Qt::Checked)); + setFilesWanted({index}, (value.toInt() == Qt::Checked)); return true; } return false; @@ -164,37 +167,135 @@ namespace tremotesf { return 0; } - void BaseTorrentFilesModel::setFileWanted(const QModelIndex& index, bool wanted) { - if (index.isValid()) { - static_cast(index.internalPointer())->setWanted(wanted); - updateDirectoryChildren(); + namespace { + class DataChangedDispatcher final { + public: + void add(const QModelIndex& parent, int row) { + // NOLINTNEXTLINE(clazy-detaching-member) + if (const auto found = mPendingSignals.find(parent); found != mPendingSignals.end()) { + found->push_back(row); + } else { + mPendingSignals.emplace(parent, std::vector{row}); + } + } + void dispatchSignals(QAbstractItemModel& model) { + for (auto&& [parent, rows] : mPendingSignals.asKeyValueRange()) { + if (rows.size() == 1) { + const int row = rows.front(); + emit model.dataChanged( + model.index(row, 0, parent), + model.index(row, model.columnCount() - 1, parent) + ); + continue; + } + std::ranges::sort(rows); + int firstRow = rows.front(); + int lastRow = firstRow; + const auto emitForRange = [&] { + emit model.dataChanged( + model.index(firstRow, 0, parent), + model.index(lastRow, model.columnCount() - 1, parent) + ); + }; + for (int row : rows | std::views::drop(1)) { + if (row != (lastRow + 1)) { + emitForRange(); + firstRow = row; + } + lastRow = row; + } + emitForRange(); + } + } + + private: + QHash> mPendingSignals{}; + }; + + void recalculateDirectoryAndItsParents( + TorrentFilesModelDirectory* directory, QModelIndex index, DataChangedDispatcher& dispatcher + ) { + while (index.isValid()) { + if (!directory->recalculateFromChildren()) { + break; + } + dispatcher.add(index.parent(), index.row()); + directory = directory->parentDirectory(); + index = index.parent(); + } } - } - void BaseTorrentFilesModel::setFilesWanted(const QModelIndexList& indexes, bool wanted) { - for (const QModelIndex& index : indexes) { - if (index.isValid()) { - static_cast(index.internalPointer())->setWanted(wanted); + template UpdateState> + requires std::same_as, bool> + void updateDirectoryChildrenRecursively( + TorrentFilesModelDirectory* directory, + const QModelIndex& directoryIndex, + UpdateState updateState, + BaseTorrentFilesModel& model, + DataChangedDispatcher& dispatcher + ) { + for (const auto& entry : directory->children()) { + if (updateState(entry.get())) { + dispatcher.add(directoryIndex, entry->row()); + } + if (entry->isDirectory()) { + auto* const directory = static_cast(entry.get()); + updateDirectoryChildrenRecursively( + directory, + model.index(directory->row(), 0, directoryIndex), + updateState, + model, + dispatcher + ); + } } } - updateDirectoryChildren(); - } - void BaseTorrentFilesModel::setFilePriority(const QModelIndex& index, TorrentFilesModelEntry::Priority priority) { - if (index.isValid()) { - static_cast(index.internalPointer())->setPriority(priority); - updateDirectoryChildren(); + template UpdateState> + requires std::same_as, bool> + void + setWantedOrPriority(const QModelIndexList& indexes, UpdateState updateState, BaseTorrentFilesModel& model) { + if (indexes.empty()) return; + if (!std::ranges::all_of(indexes, &QModelIndex::isValid)) return; + + DataChangedDispatcher dispatcher{}; + QSet> parentDirectoriesToRecalculate{}; + + for (const auto& index : indexes) { + auto* const entry = static_cast(index.internalPointer()); + if (updateState(entry)) { + dispatcher.add(index.parent(), index.row()); + if (entry->isDirectory()) { + updateDirectoryChildrenRecursively( + static_cast(entry), + index, + updateState, + model, + dispatcher + ); + } + parentDirectoriesToRecalculate.insert({entry->parentDirectory(), index.parent()}); + } + } + + for (const auto& [directory, index] : parentDirectoriesToRecalculate) { + recalculateDirectoryAndItsParents(directory, index, dispatcher); + } + + dispatcher.dispatchSignals(model); } } + void BaseTorrentFilesModel::setFilesWanted(const QModelIndexList& indexes, bool wanted) { + setWantedOrPriority(indexes, [&](TorrentFilesModelEntry* entry) { return entry->setWanted(wanted); }, *this); + } void BaseTorrentFilesModel::setFilesPriority(const QModelIndexList& indexes, TorrentFilesModelEntry::Priority priority) { - for (const QModelIndex& index : indexes) { - if (index.isValid()) { - static_cast(index.internalPointer())->setPriority(priority); - } - } - updateDirectoryChildren(); + setWantedOrPriority( + indexes, + [&](TorrentFilesModelEntry* entry) { return entry->setPriority(priority); }, + *this + ); } void BaseTorrentFilesModel::fileRenamed(TorrentFilesModelEntry* entry, const QString& newName) { @@ -202,58 +303,28 @@ namespace tremotesf { emit dataChanged(createIndex(entry->row(), 0, entry), createIndex(entry->row(), columnCount() - 1, entry)); } - void BaseTorrentFilesModel::updateDirectoryChildren(const QModelIndex& parent) { - const TorrentFilesModelDirectory* directory{}; - if (parent.isValid()) { - directory = static_cast(parent.internalPointer()); - } else if (mRootDirectory) { - directory = mRootDirectory.get(); - } else { - return; - } - - auto changedBatchProcessor = ItemBatchProcessor([&](size_t first, size_t last) { - emit dataChanged( - index(static_cast(first), 0, parent), - index(static_cast(last) - 1, columnCount() - 1, parent) - ); - }); - - for (auto& child : directory->children()) { - if (child->isChanged()) { - changedBatchProcessor.nextIndex(static_cast(child->row())); - if (child->isDirectory()) { - updateDirectoryChildren(index(child->row(), 0, parent)); - } else { - static_cast(child.get())->setChanged(false); - } - } - } - changedBatchProcessor.commitIfNeeded(); - } - void BaseTorrentFilesModel::updateFiles( std::span changedFiles, std::function&& updateFile ) { - if (!changedFiles.empty()) { - auto changedIter(changedFiles.begin()); - int changedIndex = *changedIter; - const auto changedEnd(changedFiles.end()); + if (changedFiles.empty()) return; - for (int i = 0, max = static_cast(mFiles.size()); i < max; ++i) { - const auto& file = mFiles[static_cast(i)]; - file->setChanged(false); - if (i == changedIndex) { - updateFile(static_cast(i), file); - ++changedIter; - if (changedIter == changedEnd) { - changedIndex = -1; - } else { - changedIndex = *changedIter; - } + DataChangedDispatcher dispatcher{}; + + for (int index : changedFiles) { + const auto sIndex = static_cast(index); + auto* const file = mFiles.at(sIndex); + updateFile(sIndex, file); + auto* const parent = file->parentDirectory(); + const auto parentIndex = [&] { + if (parent == mRootDirectory.get()) { + return QModelIndex{}; } - } - updateDirectoryChildren(); + return createIndex(parent->row(), 0, parent); + }(); + dispatcher.add(parentIndex, file->row()); + recalculateDirectoryAndItsParents(parent, parentIndex, dispatcher); } + + dispatcher.dispatchSignals(*this); } } diff --git a/src/ui/itemmodels/basetorrentfilesmodel.h b/src/ui/itemmodels/basetorrentfilesmodel.h index 8db9f453..3a54b908 100644 --- a/src/ui/itemmodels/basetorrentfilesmodel.h +++ b/src/ui/itemmodels/basetorrentfilesmodel.h @@ -32,16 +32,13 @@ namespace tremotesf { QModelIndex parent(const QModelIndex& child) const override; int rowCount(const QModelIndex& parent = {}) const override; - virtual void setFileWanted(const QModelIndex& index, bool wanted); virtual void setFilesWanted(const QModelIndexList& indexes, bool wanted); - virtual void setFilePriority(const QModelIndex& index, TorrentFilesModelEntry::Priority priority); virtual void setFilesPriority(const QModelIndexList& indexes, TorrentFilesModelEntry::Priority priority); virtual void renameFile(const QModelIndex& index, const QString& newName) = 0; void fileRenamed(TorrentFilesModelEntry* entry, const QString& newName); protected: - void updateDirectoryChildren(const QModelIndex& parent = QModelIndex()); void updateFiles( std::span changedFiles, std::function&& updateFile ); diff --git a/src/ui/itemmodels/torrentfilesmodel_test.cpp b/src/ui/itemmodels/torrentfilesmodel_test.cpp index 28c94fa2..c213bdc2 100644 --- a/src/ui/itemmodels/torrentfilesmodel_test.cpp +++ b/src/ui/itemmodels/torrentfilesmodel_test.cpp @@ -123,6 +123,7 @@ namespace tremotesf { file.priority ); } + builder.calculateDirectoriesRecursively(); mFiles = std::move(builder.files); } @@ -508,14 +509,10 @@ namespace tremotesf { model.updateFiles(std::array{0, 1}, [](size_t index, TorrentFilesModelFile* file) { switch (index) { case 0: - file->setWanted(true); - file->setCompletedSize(0); - file->setPriority(TorrentFilesModelEntry::Priority::High); + file->update(true, TorrentFilesModelEntry::Priority::High, 0); break; case 1: - file->setWanted(true); - file->setCompletedSize(0); - file->setPriority(TorrentFilesModelEntry::Priority::Normal); + file->update(true, TorrentFilesModelEntry::Priority::Normal, 0); break; } }); diff --git a/src/ui/itemmodels/torrentfilesmodelentry.cpp b/src/ui/itemmodels/torrentfilesmodelentry.cpp index e782ef03..dcc19415 100644 --- a/src/ui/itemmodels/torrentfilesmodelentry.cpp +++ b/src/ui/itemmodels/torrentfilesmodelentry.cpp @@ -2,12 +2,11 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -#include - #include #include #include "desktoputils.h" +#include "stdutils.h" #include "torrentfilesmodelentry.h" namespace tremotesf { @@ -37,16 +36,22 @@ namespace tremotesf { } } - TorrentFilesModelEntry::TorrentFilesModelEntry(int row, TorrentFilesModelDirectory* parentDirectory, QString name) - : mRow(row), mParentDirectory(parentDirectory), mName(std::move(name)) {} - - int TorrentFilesModelEntry::row() const { return mRow; } - - TorrentFilesModelDirectory* TorrentFilesModelEntry::parentDirectory() const { return mParentDirectory; } - - QString TorrentFilesModelEntry::name() const { return mName; } - - void TorrentFilesModelEntry::setName(const QString& name) { mName = name; } + TorrentFilesModelEntry::TorrentFilesModelEntry( + int row, + TorrentFilesModelDirectory* parentDirectory, + QString name, + long long size, + long long completedSize, + bool wanted, + Priority priority + ) + : mRow(row), + mParentDirectory(parentDirectory), + mName(std::move(name)), + mSize(size), + mCompletedSize(completedSize), + mWantedState(wanted ? WantedState::Wanted : WantedState::Unwanted), + mPriority(priority) {} QString TorrentFilesModelEntry::path() const { QString path(mName); @@ -59,6 +64,20 @@ namespace tremotesf { return path; } + bool TorrentFilesModelEntry::setWanted(bool wanted) { + WantedState wantedState{}; + if (wanted) { + wantedState = WantedState::Wanted; + } else { + wantedState = WantedState::Unwanted; + } + if (wantedState != mWantedState) { + mWantedState = wantedState; + return true; + } + return false; + } + QString TorrentFilesModelEntry::priorityString() const { switch (priority()) { case Priority::Low: @@ -77,76 +96,30 @@ namespace tremotesf { return {}; } - TorrentFilesModelDirectory::TorrentFilesModelDirectory( - int row, TorrentFilesModelDirectory* parentDirectory, const QString& name - ) - : TorrentFilesModelEntry(row, parentDirectory, name) {} - - bool TorrentFilesModelDirectory::isDirectory() const { return true; } - - long long TorrentFilesModelDirectory::size() const { - long long bytes = 0; - for (const auto& child : mChildren) { - bytes += child->size(); - } - return bytes; - } - - long long TorrentFilesModelDirectory::completedSize() const { - long long bytes = 0; - for (const auto& child : mChildren) { - bytes += child->completedSize(); - } - return bytes; - } - - double TorrentFilesModelDirectory::progress() const { - const long long bytes = size(); - if (bytes > 0) { - return static_cast(completedSize()) / static_cast(bytes); - } - return 0; - } - - TorrentFilesModelEntry::WantedState TorrentFilesModelDirectory::wantedState() const { - const TorrentFilesModelEntry::WantedState first = mChildren.front()->wantedState(); - if (mChildren.size() > 1) { - for (const auto& child : mChildren | std::views::drop(1)) { - if (child->wantedState() != first) { - return WantedState::Mixed; - } - } - } - return first; - } - - void TorrentFilesModelDirectory::setWanted(bool wanted) { - for (auto& child : mChildren) { - child->setWanted(wanted); + bool TorrentFilesModelEntry::setPriority(Priority priority) { + if (priority != mPriority) { + mPriority = priority; + return true; } + return false; } - TorrentFilesModelEntry::Priority TorrentFilesModelDirectory::priority() const { - const Priority first = mChildren.front()->priority(); - if (mChildren.size() > 1) { - for (const auto& child : mChildren | std::views::drop(1)) { - if (child->priority() != first) { - return Priority::Mixed; - } - } - } - return first; + bool TorrentFilesModelEntry::update(bool wanted, Priority priority, long long completedSize) { + return update(wanted ? WantedState::Wanted : WantedState::Unwanted, priority, completedSize); } - void TorrentFilesModelDirectory::setPriority(Priority priority) { - for (const auto& child : mChildren) { - child->setPriority(priority); - } + bool TorrentFilesModelEntry::update(WantedState wantedState, Priority priority, long long completedSize) { + bool changed = false; + setChanged(mWantedState, wantedState, changed); + setChanged(mPriority, priority, changed); + setChanged(mCompletedSize, completedSize, changed); + return changed; } - const std::vector>& TorrentFilesModelDirectory::children() const { - return mChildren; - } + TorrentFilesModelDirectory::TorrentFilesModelDirectory( + int row, TorrentFilesModelDirectory* parentDirectory, QString name + ) + : TorrentFilesModelEntry(row, parentDirectory, std::move(name), 0, 0, true, Priority::Normal) {} TorrentFilesModelFile* TorrentFilesModelDirectory::addFile( int id, const QString& name, long long size, long long completedSize, bool wanted, Priority priority @@ -185,8 +158,21 @@ namespace tremotesf { QIcon tremotesf::TorrentFilesModelDirectory::icon() const { return desktoputils::standardDirIcon(); } - bool TorrentFilesModelDirectory::isChanged() const { - return std::ranges::any_of(mChildren, [](const auto& child) { return child->isChanged(); }); + bool TorrentFilesModelDirectory::recalculateFromChildren() { + if (mChildren.empty()) return false; + long long completedSize{}; + WantedState wantedState = mChildren.front()->wantedState(); + Priority priority = mChildren.front()->priority(); + for (const auto& child : mChildren) { + completedSize += child->completedSize(); + if (wantedState != TorrentFilesModelEntry::WantedState::Mixed && child->wantedState() != wantedState) { + wantedState = TorrentFilesModelEntry::WantedState::Mixed; + } + if (priority != TorrentFilesModelEntry::Priority::Mixed && child->priority() != priority) { + priority = TorrentFilesModelEntry::Priority::Mixed; + } + } + return update(wantedState, priority, completedSize); } void TorrentFilesModelDirectory::addChild(std::unique_ptr&& child) { @@ -197,57 +183,14 @@ namespace tremotesf { int row, TorrentFilesModelDirectory* parentDirectory, int id, - const QString& name, + QString name, long long size, long long completedSize, bool wanted, Priority priority ) - : TorrentFilesModelEntry(row, parentDirectory, name), - mSize(size), - mCompletedSize(completedSize), - mWantedState(wanted ? WantedState::Wanted : WantedState::Unwanted), - mPriority(priority), - mId(id), - mInitializedIcon(false), - mChanged(false) {} - - bool TorrentFilesModelFile::isDirectory() const { return false; } - - long long TorrentFilesModelFile::size() const { return mSize; } - - long long TorrentFilesModelFile::completedSize() const { return mCompletedSize; } - - double TorrentFilesModelFile::progress() const { - if (mSize > 0) { - return static_cast(mCompletedSize) / static_cast(mSize); - } - return 0; - } - - TorrentFilesModelEntry::WantedState TorrentFilesModelFile::wantedState() const { return mWantedState; } - - void TorrentFilesModelFile::setWanted(bool wanted) { - WantedState wantedState{}; - if (wanted) { - wantedState = WantedState::Wanted; - } else { - wantedState = WantedState::Unwanted; - } - if (wantedState != mWantedState) { - mWantedState = wantedState; - mChanged = true; - } - } - - TorrentFilesModelEntry::Priority TorrentFilesModelFile::priority() const { return mPriority; } - - void TorrentFilesModelFile::setPriority(Priority priority) { - if (priority != mPriority) { - mPriority = priority; - mChanged = true; - } - } + : TorrentFilesModelEntry(row, parentDirectory, std::move(name), size, completedSize, wanted, priority), + mId(id) {} namespace { QIcon determineFileIcon(const QString& fileName) { @@ -273,19 +216,4 @@ namespace tremotesf { } return mIcon; } - - bool TorrentFilesModelFile::isChanged() const { return mChanged; } - - void TorrentFilesModelFile::setChanged(bool changed) { mChanged = changed; } - - int TorrentFilesModelFile::id() const { return mId; } - - void TorrentFilesModelFile::setSize(long long size) { mSize = size; } - - void TorrentFilesModelFile::setCompletedSize(long long completedSize) { - if (completedSize != mCompletedSize) { - mCompletedSize = completedSize; - mChanged = true; - } - } } diff --git a/src/ui/itemmodels/torrentfilesmodelentry.h b/src/ui/itemmodels/torrentfilesmodelentry.h index 3cbb6a9f..b72acdb5 100644 --- a/src/ui/itemmodels/torrentfilesmodelentry.h +++ b/src/ui/itemmodels/torrentfilesmodelentry.h @@ -29,39 +29,57 @@ namespace tremotesf { static TorrentFile::Priority toFilePriority(Priority priority); TorrentFilesModelEntry() = default; - explicit TorrentFilesModelEntry(int row, TorrentFilesModelDirectory* parentDirectory, QString name); + explicit TorrentFilesModelEntry( + int row, + TorrentFilesModelDirectory* parentDirectory, + QString name, + long long size, + long long completedSize, + bool wanted, + TorrentFilesModelEntry::Priority priority + ); virtual ~TorrentFilesModelEntry() = default; Q_DISABLE_COPY_MOVE(TorrentFilesModelEntry) - int row() const; - TorrentFilesModelDirectory* parentDirectory() const; + inline int row() const { return mRow; } + inline TorrentFilesModelDirectory* parentDirectory() const { return mParentDirectory; } - QString name() const; - void setName(const QString& name); + inline QString name() const { return mName; } + inline void setName(const QString& name) { mName = name; } QString path() const; - virtual bool isDirectory() const = 0; + inline long long size() const { return mSize; } + void setSize(long long size) { mSize = size; } + + inline long long completedSize() const { return mCompletedSize; } + inline double progress() const { + return mSize > 0 ? static_cast(mCompletedSize) / static_cast(mSize) : 0.0; + } - virtual long long size() const = 0; - virtual long long completedSize() const = 0; - virtual double progress() const = 0; + inline WantedState wantedState() const { return mWantedState; } - virtual WantedState wantedState() const = 0; - virtual void setWanted(bool wanted) = 0; + bool setWanted(bool wanted); - virtual Priority priority() const = 0; + inline Priority priority() const { return mPriority; } QString priorityString() const; - virtual void setPriority(Priority priority) = 0; - virtual QIcon icon() const = 0; + bool setPriority(Priority priority); - virtual bool isChanged() const = 0; + bool update(bool wanted, Priority priority, long long completedSize); + bool update(WantedState wantedState, Priority priority, long long completedSize); - private: - int mRow = 0; - TorrentFilesModelDirectory* mParentDirectory = nullptr; + virtual bool isDirectory() const = 0; + virtual QIcon icon() const = 0; + + protected: + int mRow{}; + TorrentFilesModelDirectory* mParentDirectory{}; QString mName; + long long mSize; + long long mCompletedSize; + WantedState mWantedState; + Priority mPriority; }; class TorrentFilesModelFile; @@ -69,18 +87,11 @@ namespace tremotesf { class TorrentFilesModelDirectory final : public TorrentFilesModelEntry { public: TorrentFilesModelDirectory() = default; - explicit TorrentFilesModelDirectory(int row, TorrentFilesModelDirectory* parentDirectory, const QString& name); + explicit TorrentFilesModelDirectory(int row, TorrentFilesModelDirectory* parentDirectory, QString name); - bool isDirectory() const override; - long long size() const override; - long long completedSize() const override; - double progress() const override; - WantedState wantedState() const override; - void setWanted(bool wanted) override; - Priority priority() const override; - void setPriority(Priority priority) override; + inline bool isDirectory() const override { return true; } - const std::vector>& children() const; + inline const std::vector>& children() const { return mChildren; } TorrentFilesModelFile* addFile( int id, @@ -97,7 +108,7 @@ namespace tremotesf { QIcon icon() const override; - bool isChanged() const override; + bool recalculateFromChildren(); private: void addChild(std::unique_ptr&& child); @@ -111,40 +122,23 @@ namespace tremotesf { int row, TorrentFilesModelDirectory* parentDirectory, int id, - const QString& name, + QString name, long long size, long long completedSize, bool wanted, TorrentFilesModelEntry::Priority priority ); - bool isDirectory() const override; - long long size() const override; - long long completedSize() const override; - double progress() const override; - WantedState wantedState() const override; - void setWanted(bool wanted) override; - Priority priority() const override; - void setPriority(Priority priority) override; - QIcon icon() const override; + inline bool isDirectory() const override { return false; } - bool isChanged() const override; - void setChanged(bool changed); + QIcon icon() const override; - int id() const; - void setSize(long long size); - void setCompletedSize(long long completedSize); + inline int id() const { return mId; } private: - long long mSize; - long long mCompletedSize; - WantedState mWantedState; - Priority mPriority; - mutable QIcon mIcon; + mutable QIcon mIcon{}; + mutable bool mInitializedIcon{}; int mId; - mutable bool mInitializedIcon; - - bool mChanged; }; } diff --git a/src/ui/itemmodels/torrentfilestreebuilder.h b/src/ui/itemmodels/torrentfilestreebuilder.h index edc27d19..565b65eb 100644 --- a/src/ui/itemmodels/torrentfilestreebuilder.h +++ b/src/ui/itemmodels/torrentfilestreebuilder.h @@ -72,10 +72,36 @@ namespace tremotesf { } } + void calculateDirectoriesRecursively() { calculateFromChildrenRecursively(rootDirectory); } + TorrentFilesModelDirectory* rootDirectory; std::vector files{}; private: + void calculateFromChildrenRecursively(TorrentFilesModelDirectory* directory) { + const auto& children = directory->children(); + if (children.empty()) return; + long long size{}; + long long completedSize{}; + TorrentFilesModelEntry::WantedState wantedState = children.front()->wantedState(); + TorrentFilesModelEntry::Priority priority = children.front()->priority(); + for (const auto& child : children) { + if (child->isDirectory()) { + calculateFromChildrenRecursively(static_cast(child.get())); + } + size += child->size(); + completedSize += child->completedSize(); + if (wantedState != TorrentFilesModelEntry::WantedState::Mixed && child->wantedState() != wantedState) { + wantedState = TorrentFilesModelEntry::WantedState::Mixed; + } + if (priority != TorrentFilesModelEntry::Priority::Mixed && child->priority() != priority) { + priority = TorrentFilesModelEntry::Priority::Mixed; + } + } + directory->setSize(size); + directory->update(wantedState, priority, completedSize); + } + struct DirectoryCacheEntry { TorrentFilesModelDirectory* directory; QHash childDirectoriesCache{}; diff --git a/src/ui/screens/addtorrent/localtorrentfilesmodel.cpp b/src/ui/screens/addtorrent/localtorrentfilesmodel.cpp index 208d6f54..c23db490 100644 --- a/src/ui/screens/addtorrent/localtorrentfilesmodel.cpp +++ b/src/ui/screens/addtorrent/localtorrentfilesmodel.cpp @@ -35,6 +35,7 @@ namespace tremotesf { for (TorrentMetainfoFile::File file : files) { builder.addFile(file.path(), file.size, 0, true, TorrentFilesModelEntry::Priority::Normal); } + builder.calculateDirectoriesRecursively(); return {.rootDirectory = std::move(rootDirectory), .files = std::move(builder.files)}; } } diff --git a/src/ui/screens/torrentproperties/torrentfilesmodel.cpp b/src/ui/screens/torrentproperties/torrentfilesmodel.cpp index aded713f..17ed6011 100644 --- a/src/ui/screens/torrentproperties/torrentfilesmodel.cpp +++ b/src/ui/screens/torrentproperties/torrentfilesmodel.cpp @@ -18,14 +18,6 @@ using namespace Qt::StringLiterals; namespace tremotesf { namespace { - std::vector idsFromIndex(const QModelIndex& index) { - auto entry = static_cast(index.internalPointer()); - if (entry->isDirectory()) { - return static_cast(entry)->childrenIds(); - } - return {static_cast(entry)->id()}; - } - std::vector idsFromIndexes(const QList& indexes) { std::vector ids{}; // at least indexes.size(), but may be more @@ -59,6 +51,7 @@ namespace tremotesf { TorrentFilesModelEntry::fromFilePriority(file.priority) ); } + builder.calculateDirectoriesRecursively(); return {std::move(rootDirectory), std::move(builder.files)}; } } @@ -103,21 +96,11 @@ namespace tremotesf { void TorrentFilesModel::setRpc(Rpc* rpc) { mRpc = rpc; } - void TorrentFilesModel::setFileWanted(const QModelIndex& index, bool wanted) { - BaseTorrentFilesModel::setFileWanted(index, wanted); - mTorrent->setFilesWanted(idsFromIndex(index), wanted); - } - void TorrentFilesModel::setFilesWanted(const QModelIndexList& indexes, bool wanted) { BaseTorrentFilesModel::setFilesWanted(indexes, wanted); mTorrent->setFilesWanted(idsFromIndexes(indexes), wanted); } - void TorrentFilesModel::setFilePriority(const QModelIndex& index, TorrentFilesModelEntry::Priority priority) { - BaseTorrentFilesModel::setFilePriority(index, priority); - mTorrent->setFilesPriority(idsFromIndex(index), TorrentFilesModelEntry::toFilePriority(priority)); - } - void TorrentFilesModel::setFilesPriority(const QModelIndexList& indexes, TorrentFilesModelEntry::Priority priority) { BaseTorrentFilesModel::setFilesPriority(indexes, priority); @@ -205,12 +188,10 @@ namespace tremotesf { } void TorrentFilesModel::updateTree(std::span changed) { - const auto& torrentFiles = mTorrent->files(); - updateFiles(changed, [&](size_t index, TorrentFilesModelFile* file) { - const auto& json = torrentFiles.at(index); - file->setCompletedSize(json.completedSize); - file->setWanted(json.wanted); - file->setPriority(TorrentFilesModelEntry::fromFilePriority(json.priority)); + const auto& jsons = mTorrent->files(); + updateFiles(changed, [&](size_t i, TorrentFilesModelFile* file) { + const auto& json = jsons.at(i); + file->update(json.wanted, TorrentFilesModelEntry::fromFilePriority(json.priority), json.completedSize); }); } diff --git a/src/ui/screens/torrentproperties/torrentfilesmodel.h b/src/ui/screens/torrentproperties/torrentfilesmodel.h index 964a750b..b3942618 100644 --- a/src/ui/screens/torrentproperties/torrentfilesmodel.h +++ b/src/ui/screens/torrentproperties/torrentfilesmodel.h @@ -6,7 +6,6 @@ #define TREMOTESF_TORRENTFILESMODEL_H #include -#include #include "coroutines/scope.h" #include "ui/itemmodels/basetorrentfilesmodel.h" @@ -29,9 +28,7 @@ namespace tremotesf { Rpc* rpc() const; void setRpc(Rpc* rpc); - void setFileWanted(const QModelIndex& index, bool wanted) override; void setFilesWanted(const QModelIndexList& indexes, bool wanted) override; - void setFilePriority(const QModelIndex& index, TorrentFilesModelEntry::Priority priority) override; void setFilesPriority(const QModelIndexList& indexes, TorrentFilesModelEntry::Priority priority) override; void renameFile(const QModelIndex& index, const QString& newName) override; From a0e6d245a7a87695c8d795923d3c30b1a387bdca Mon Sep 17 00:00:00 2001 From: Alexey Rochev Date: Sat, 4 Apr 2026 01:37:46 +0300 Subject: [PATCH 11/12] Simplify getting file ids from model indexes --- src/ui/itemmodels/torrentfilesmodelentry.cpp | 13 ++----------- src/ui/itemmodels/torrentfilesmodelentry.h | 5 ++++- .../screens/torrentproperties/torrentfilesmodel.cpp | 8 +------- 3 files changed, 7 insertions(+), 19 deletions(-) diff --git a/src/ui/itemmodels/torrentfilesmodelentry.cpp b/src/ui/itemmodels/torrentfilesmodelentry.cpp index dcc19415..086c5dc7 100644 --- a/src/ui/itemmodels/torrentfilesmodelentry.cpp +++ b/src/ui/itemmodels/torrentfilesmodelentry.cpp @@ -141,19 +141,10 @@ namespace tremotesf { void TorrentFilesModelDirectory::clearChildren() { mChildren.clear(); } - std::vector TorrentFilesModelDirectory::childrenIds() const { - std::vector ids{}; - ids.reserve(mChildren.size()); + void TorrentFilesModelDirectory::getFileIds(std::vector& ids) const { for (const auto& child : mChildren) { - if (child->isDirectory()) { - const auto childrenIds = static_cast(child.get())->childrenIds(); - ids.reserve(ids.size() + childrenIds.size()); - ids.insert(ids.end(), childrenIds.begin(), childrenIds.end()); - } else { - ids.push_back(static_cast(child.get())->id()); - } + child->getFileIds(ids); } - return ids; } QIcon tremotesf::TorrentFilesModelDirectory::icon() const { return desktoputils::standardDirIcon(); } diff --git a/src/ui/itemmodels/torrentfilesmodelentry.h b/src/ui/itemmodels/torrentfilesmodelentry.h index b72acdb5..f69f5672 100644 --- a/src/ui/itemmodels/torrentfilesmodelentry.h +++ b/src/ui/itemmodels/torrentfilesmodelentry.h @@ -71,6 +71,7 @@ namespace tremotesf { virtual bool isDirectory() const = 0; virtual QIcon icon() const = 0; + virtual void getFileIds(std::vector& ids) const = 0; protected: int mRow{}; @@ -104,7 +105,7 @@ namespace tremotesf { TorrentFilesModelDirectory* addDirectory(const QString& name); void clearChildren(); - std::vector childrenIds() const; + void getFileIds(std::vector& ids) const override; QIcon icon() const override; @@ -135,6 +136,8 @@ namespace tremotesf { inline int id() const { return mId; } + inline void getFileIds(std::vector& ids) const override { ids.push_back(mId); } + private: mutable QIcon mIcon{}; mutable bool mInitializedIcon{}; diff --git a/src/ui/screens/torrentproperties/torrentfilesmodel.cpp b/src/ui/screens/torrentproperties/torrentfilesmodel.cpp index 17ed6011..d72de690 100644 --- a/src/ui/screens/torrentproperties/torrentfilesmodel.cpp +++ b/src/ui/screens/torrentproperties/torrentfilesmodel.cpp @@ -24,13 +24,7 @@ namespace tremotesf { ids.reserve(static_cast(indexes.size())); for (const QModelIndex& index : indexes) { auto entry = static_cast(index.internalPointer()); - if (entry->isDirectory()) { - const auto childrenIds = static_cast(entry)->childrenIds(); - ids.reserve(ids.size() + childrenIds.size()); - ids.insert(ids.end(), childrenIds.begin(), childrenIds.end()); - } else { - ids.push_back(static_cast(entry)->id()); - } + entry->getFileIds(ids); } std::ranges::sort(ids); const auto toErase = std::ranges::unique(ids); From 72516cfd56ad5dfd93d881610310063c2052c0bd Mon Sep 17 00:00:00 2001 From: Alexey Rochev Date: Thu, 9 Apr 2026 02:04:18 +0300 Subject: [PATCH 12/12] Store file/directory data in TorrentFilesModelEntry in std::variant instead of using polymorphism, and remove synthetic root directory in BaseTorrentFilesModel --- src/ui/itemmodels/basetorrentfilesmodel.cpp | 65 ++++---- src/ui/itemmodels/basetorrentfilesmodel.h | 6 +- src/ui/itemmodels/torrentfilesmodel_test.cpp | 7 +- src/ui/itemmodels/torrentfilesmodelentry.cpp | 141 ++++++----------- src/ui/itemmodels/torrentfilesmodelentry.h | 143 ++++++++---------- src/ui/itemmodels/torrentfilestreebuilder.h | 122 +++++++++++---- .../addtorrent/localtorrentfilesmodel.cpp | 40 ++--- .../torrentproperties/torrentfilesmodel.cpp | 24 +-- 8 files changed, 279 insertions(+), 269 deletions(-) diff --git a/src/ui/itemmodels/basetorrentfilesmodel.cpp b/src/ui/itemmodels/basetorrentfilesmodel.cpp index f10ccf60..96cd9f13 100644 --- a/src/ui/itemmodels/basetorrentfilesmodel.cpp +++ b/src/ui/itemmodels/basetorrentfilesmodel.cpp @@ -24,7 +24,7 @@ namespace tremotesf { if (!index.isValid()) { return {}; } - const TorrentFilesModelEntry* entry = static_cast(index.internalPointer()); + const auto* const entry = static_cast(index.internalPointer()); const Column column = mColumns.at(static_cast(index.column())); switch (role) { case Qt::CheckStateRole: @@ -130,39 +130,39 @@ namespace tremotesf { } QModelIndex BaseTorrentFilesModel::index(int row, int column, const QModelIndex& parent) const { - const TorrentFilesModelDirectory* parentDirectory{}; - if (parent.isValid()) { - parentDirectory = static_cast(parent.internalPointer()); - } else if (mRootDirectory) { - parentDirectory = mRootDirectory.get(); - } else { + if (!parent.isValid()) { + if (row == 0 && mRootEntry) { + return createIndex(0, column, mRootEntry.get()); + } + return {}; + } + const auto* const parentEntry = static_cast(parent.internalPointer()); + if (!parentEntry->isDirectory()) { return {}; } - return createIndex(row, column, parentDirectory->children().at(static_cast(row)).get()); + return createIndex(row, column, &parentEntry->children().at(static_cast(row))); } QModelIndex BaseTorrentFilesModel::parent(const QModelIndex& child) const { if (!child.isValid()) { return {}; } - const auto* const parentDirectory = + const auto* const parentDirectoryEntry = static_cast(child.internalPointer())->parentDirectory(); - if (parentDirectory == mRootDirectory.get()) { + if (!parentDirectoryEntry) { return {}; } - return createIndex(parentDirectory->row(), 0, parentDirectory); + return createIndex(parentDirectoryEntry->row(), 0, parentDirectoryEntry); } int BaseTorrentFilesModel::rowCount(const QModelIndex& parent) const { if (parent.isValid()) { - const TorrentFilesModelEntry* entry = static_cast(parent.internalPointer()); + const auto* const entry = static_cast(parent.internalPointer()); if (entry->isDirectory()) { - return static_cast(static_cast(entry)->children().size()); + return static_cast(entry->children().size()); } - return 0; - } - if (mRootDirectory) { - return static_cast(mRootDirectory->children().size()); + } else if (mRootEntry) { + return 1; } return 0; } @@ -213,7 +213,7 @@ namespace tremotesf { }; void recalculateDirectoryAndItsParents( - TorrentFilesModelDirectory* directory, QModelIndex index, DataChangedDispatcher& dispatcher + TorrentFilesModelEntry* directory, QModelIndex index, DataChangedDispatcher& dispatcher ) { while (index.isValid()) { if (!directory->recalculateFromChildren()) { @@ -228,21 +228,20 @@ namespace tremotesf { template UpdateState> requires std::same_as, bool> void updateDirectoryChildrenRecursively( - TorrentFilesModelDirectory* directory, + TorrentFilesModelEntry* directory, const QModelIndex& directoryIndex, UpdateState updateState, BaseTorrentFilesModel& model, DataChangedDispatcher& dispatcher ) { - for (const auto& entry : directory->children()) { - if (updateState(entry.get())) { - dispatcher.add(directoryIndex, entry->row()); + for (auto& entry : directory->children()) { + if (updateState(&entry)) { + dispatcher.add(directoryIndex, entry.row()); } - if (entry->isDirectory()) { - auto* const directory = static_cast(entry.get()); + if (entry.isDirectory()) { updateDirectoryChildrenRecursively( - directory, - model.index(directory->row(), 0, directoryIndex), + &entry, + model.index(entry.row(), 0, directoryIndex), updateState, model, dispatcher @@ -259,20 +258,14 @@ namespace tremotesf { if (!std::ranges::all_of(indexes, &QModelIndex::isValid)) return; DataChangedDispatcher dispatcher{}; - QSet> parentDirectoriesToRecalculate{}; + QSet> parentDirectoriesToRecalculate{}; for (const auto& index : indexes) { auto* const entry = static_cast(index.internalPointer()); if (updateState(entry)) { dispatcher.add(index.parent(), index.row()); if (entry->isDirectory()) { - updateDirectoryChildrenRecursively( - static_cast(entry), - index, - updateState, - model, - dispatcher - ); + updateDirectoryChildrenRecursively(entry, index, updateState, model, dispatcher); } parentDirectoriesToRecalculate.insert({entry->parentDirectory(), index.parent()}); } @@ -304,7 +297,7 @@ namespace tremotesf { } void BaseTorrentFilesModel::updateFiles( - std::span changedFiles, std::function&& updateFile + std::span changedFiles, std::function&& updateFile ) { if (changedFiles.empty()) return; @@ -316,7 +309,7 @@ namespace tremotesf { updateFile(sIndex, file); auto* const parent = file->parentDirectory(); const auto parentIndex = [&] { - if (parent == mRootDirectory.get()) { + if (!parent) { return QModelIndex{}; } return createIndex(parent->row(), 0, parent); diff --git a/src/ui/itemmodels/basetorrentfilesmodel.h b/src/ui/itemmodels/basetorrentfilesmodel.h index 3a54b908..11e6810e 100644 --- a/src/ui/itemmodels/basetorrentfilesmodel.h +++ b/src/ui/itemmodels/basetorrentfilesmodel.h @@ -40,11 +40,11 @@ namespace tremotesf { protected: void updateFiles( - std::span changedFiles, std::function&& updateFile + std::span changedFiles, std::function&& updateFile ); - std::unique_ptr mRootDirectory{}; - std::vector mFiles{}; + std::unique_ptr mRootEntry{}; + std::vector mFiles{}; private: const std::vector mColumns; diff --git a/src/ui/itemmodels/torrentfilesmodel_test.cpp b/src/ui/itemmodels/torrentfilesmodel_test.cpp index c213bdc2..8e0f40fe 100644 --- a/src/ui/itemmodels/torrentfilesmodel_test.cpp +++ b/src/ui/itemmodels/torrentfilesmodel_test.cpp @@ -112,11 +112,11 @@ namespace tremotesf { .wanted = true } }; - mRootDirectory = std::make_unique(); - TorrentFilesTreeBuilder builder(mRootDirectory.get(), files.size()); + TorrentFilesTreeBuilder builder(files.size()); for (const File& file : files) { builder.addFile( file.path.tokenize(u'/', Qt::SkipEmptyParts), + true, file.size, file.completedSize, file.wanted, @@ -124,6 +124,7 @@ namespace tremotesf { ); } builder.calculateDirectoriesRecursively(); + mRootEntry = std::move(builder.rootEntry); mFiles = std::move(builder.files); } @@ -506,7 +507,7 @@ namespace tremotesf { QSignalSpy dataChanged(&model, &QAbstractItemModel::dataChanged); - model.updateFiles(std::array{0, 1}, [](size_t index, TorrentFilesModelFile* file) { + model.updateFiles(std::array{0, 1}, [](size_t index, TorrentFilesModelEntry* file) { switch (index) { case 0: file->update(true, TorrentFilesModelEntry::Priority::High, 0); diff --git a/src/ui/itemmodels/torrentfilesmodelentry.cpp b/src/ui/itemmodels/torrentfilesmodelentry.cpp index 086c5dc7..dc3ed7da 100644 --- a/src/ui/itemmodels/torrentfilesmodelentry.cpp +++ b/src/ui/itemmodels/torrentfilesmodelentry.cpp @@ -23,7 +23,7 @@ namespace tremotesf { } } - TorrentFile::Priority TorrentFilesModelEntry::toFilePriority(TorrentFilesModelEntry::Priority priority) { + TorrentFile::Priority TorrentFilesModelEntry::toFilePriority(Priority priority) { switch (priority) { case Priority::Low: return TorrentFile::Priority::Low; @@ -36,23 +36,6 @@ namespace tremotesf { } } - TorrentFilesModelEntry::TorrentFilesModelEntry( - int row, - TorrentFilesModelDirectory* parentDirectory, - QString name, - long long size, - long long completedSize, - bool wanted, - Priority priority - ) - : mRow(row), - mParentDirectory(parentDirectory), - mName(std::move(name)), - mSize(size), - mCompletedSize(completedSize), - mWantedState(wanted ? WantedState::Wanted : WantedState::Unwanted), - mPriority(priority) {} - QString TorrentFilesModelEntry::path() const { QString path(mName); const auto* parent = mParentDirectory; @@ -79,7 +62,7 @@ namespace tremotesf { } QString TorrentFilesModelEntry::priorityString() const { - switch (priority()) { + switch (mPriority) { case Priority::Low: //: Torrent's file loading priority return qApp->translate("tremotesf", "Low"); @@ -116,73 +99,6 @@ namespace tremotesf { return changed; } - TorrentFilesModelDirectory::TorrentFilesModelDirectory( - int row, TorrentFilesModelDirectory* parentDirectory, QString name - ) - : TorrentFilesModelEntry(row, parentDirectory, std::move(name), 0, 0, true, Priority::Normal) {} - - TorrentFilesModelFile* TorrentFilesModelDirectory::addFile( - int id, const QString& name, long long size, long long completedSize, bool wanted, Priority priority - ) { - const int row = static_cast(mChildren.size()); - auto file = std::make_unique(row, this, id, name, size, completedSize, wanted, priority); - auto* filePtr = file.get(); - addChild(std::move(file)); - return filePtr; - } - - TorrentFilesModelDirectory* TorrentFilesModelDirectory::addDirectory(const QString& name) { - const int row = static_cast(mChildren.size()); - auto directory = std::make_unique(row, this, name); - auto* directoryPtr = directory.get(); - addChild(std::move(directory)); - return directoryPtr; - } - - void TorrentFilesModelDirectory::clearChildren() { mChildren.clear(); } - - void TorrentFilesModelDirectory::getFileIds(std::vector& ids) const { - for (const auto& child : mChildren) { - child->getFileIds(ids); - } - } - - QIcon tremotesf::TorrentFilesModelDirectory::icon() const { return desktoputils::standardDirIcon(); } - - bool TorrentFilesModelDirectory::recalculateFromChildren() { - if (mChildren.empty()) return false; - long long completedSize{}; - WantedState wantedState = mChildren.front()->wantedState(); - Priority priority = mChildren.front()->priority(); - for (const auto& child : mChildren) { - completedSize += child->completedSize(); - if (wantedState != TorrentFilesModelEntry::WantedState::Mixed && child->wantedState() != wantedState) { - wantedState = TorrentFilesModelEntry::WantedState::Mixed; - } - if (priority != TorrentFilesModelEntry::Priority::Mixed && child->priority() != priority) { - priority = TorrentFilesModelEntry::Priority::Mixed; - } - } - return update(wantedState, priority, completedSize); - } - - void TorrentFilesModelDirectory::addChild(std::unique_ptr&& child) { - mChildren.push_back(std::move(child)); - } - - TorrentFilesModelFile::TorrentFilesModelFile( - int row, - TorrentFilesModelDirectory* parentDirectory, - int id, - QString name, - long long size, - long long completedSize, - bool wanted, - Priority priority - ) - : TorrentFilesModelEntry(row, parentDirectory, std::move(name), size, completedSize, wanted, priority), - mId(id) {} - namespace { QIcon determineFileIcon(const QString& fileName) { static const QMimeDatabase mimeDb{}; @@ -200,11 +116,54 @@ namespace tremotesf { } } - QIcon TorrentFilesModelFile::icon() const { - if (!mInitializedIcon) { - mIcon = determineFileIcon(name()); - mInitializedIcon = true; + QIcon TorrentFilesModelEntry::icon() const { + return std::visit( + [&](auto&& data) { + if constexpr (std::same_as>) { + if (!data.initializedIcon) { + data.icon = determineFileIcon(name()); + data.initializedIcon = true; + } + return data.icon; + } else { + return desktoputils::standardDirIcon(); + } + }, + mFileOrDirectoryData + ); + } + + void TorrentFilesModelEntry::getFileIds(std::vector& ids) const { + std::visit( + [&](auto&& data) { + if constexpr (std::same_as>) { + ids.push_back(data.id); + } else { + for (const auto& child : data.children) { + child.getFileIds(ids); + } + } + }, + mFileOrDirectoryData + ); + } + + bool TorrentFilesModelEntry::recalculateFromChildren() { + const auto& children = std::get(mFileOrDirectoryData).children; + if (children.empty()) return false; + const auto& firstChild = children.front(); + long long completedSize = firstChild.completedSize(); + WantedState wantedState = firstChild.wantedState(); + Priority priority = firstChild.priority(); + for (const auto& child : children | std::views::drop(1)) { + completedSize += child.completedSize(); + if (wantedState != TorrentFilesModelEntry::WantedState::Mixed && child.wantedState() != wantedState) { + wantedState = TorrentFilesModelEntry::WantedState::Mixed; + } + if (priority != TorrentFilesModelEntry::Priority::Mixed && child.priority() != priority) { + priority = TorrentFilesModelEntry::Priority::Mixed; + } } - return mIcon; + return update(wantedState, priority, completedSize); } } diff --git a/src/ui/itemmodels/torrentfilesmodelentry.h b/src/ui/itemmodels/torrentfilesmodelentry.h index f69f5672..a44a2ef5 100644 --- a/src/ui/itemmodels/torrentfilesmodelentry.h +++ b/src/ui/itemmodels/torrentfilesmodelentry.h @@ -5,7 +5,7 @@ #ifndef TREMOTESF_TORRENTFILESMODELENTRY_H #define TREMOTESF_TORRENTFILESMODELENTRY_H -#include +#include #include #include @@ -14,35 +14,23 @@ #include "rpc/torrentfile.h" namespace tremotesf { - class TorrentFilesModelDirectory; - class TorrentFilesModelEntry { Q_GADGET + public: - enum class WantedState { Wanted, Unwanted, Mixed }; + enum class WantedState : char { Wanted, Unwanted, Mixed }; Q_ENUM(WantedState) - enum class Priority { Low, Normal, High, Mixed }; + enum class Priority : char { Low, Normal, High, Mixed }; Q_ENUM(Priority) static Priority fromFilePriority(TorrentFile::Priority priority); static TorrentFile::Priority toFilePriority(Priority priority); - TorrentFilesModelEntry() = default; - explicit TorrentFilesModelEntry( - int row, - TorrentFilesModelDirectory* parentDirectory, - QString name, - long long size, - long long completedSize, - bool wanted, - TorrentFilesModelEntry::Priority priority - ); - virtual ~TorrentFilesModelEntry() = default; - Q_DISABLE_COPY_MOVE(TorrentFilesModelEntry) - inline int row() const { return mRow; } - inline TorrentFilesModelDirectory* parentDirectory() const { return mParentDirectory; } + + inline TorrentFilesModelEntry* parentDirectory() const { return mParentDirectory; } + inline void setParentDirectory(TorrentFilesModelEntry* directory) { mParentDirectory = directory; } inline QString name() const { return mName; } inline void setName(const QString& name) { mName = name; } @@ -69,79 +57,80 @@ namespace tremotesf { bool update(bool wanted, Priority priority, long long completedSize); bool update(WantedState wantedState, Priority priority, long long completedSize); - virtual bool isDirectory() const = 0; - virtual QIcon icon() const = 0; - virtual void getFileIds(std::vector& ids) const = 0; + QIcon icon() const; + void getFileIds(std::vector& ids) const; - protected: - int mRow{}; - TorrentFilesModelDirectory* mParentDirectory{}; - QString mName; - long long mSize; - long long mCompletedSize; - WantedState mWantedState; - Priority mPriority; - }; - - class TorrentFilesModelFile; - - class TorrentFilesModelDirectory final : public TorrentFilesModelEntry { - public: - TorrentFilesModelDirectory() = default; - explicit TorrentFilesModelDirectory(int row, TorrentFilesModelDirectory* parentDirectory, QString name); + inline bool isDirectory() const { return std::holds_alternative(mFileOrDirectoryData); } - inline bool isDirectory() const override { return true; } + inline std::vector& children() { + return std::get(mFileOrDirectoryData).children; + } + inline const std::vector& children() const { + return std::get(mFileOrDirectoryData).children; + } - inline const std::vector>& children() const { return mChildren; } + bool recalculateFromChildren(); - TorrentFilesModelFile* addFile( - int id, - const QString& name, - long long size, - long long completedSize, - bool wanted, - TorrentFilesModelEntry::Priority priority - ); - TorrentFilesModelDirectory* addDirectory(const QString& name); + inline int fileId() const { return std::get(mFileOrDirectoryData).id; } + + inline static TorrentFilesModelEntry createFile( + int id, int row, QString name, long long size, long long completedSize, bool wanted, Priority priority + ) { + return TorrentFilesModelEntry( + row, + std::move(name), + size, + completedSize, + wanted, + priority, + FileData{.id = id} + ); + } - void clearChildren(); - void getFileIds(std::vector& ids) const override; + inline static TorrentFilesModelEntry createDirectory(int row, QString name) { + return TorrentFilesModelEntry(row, std::move(name), DirectoryData{}); + } - QIcon icon() const override; + private: + TorrentFilesModelEntry* mParentDirectory{}; + QString mName; + long long mSize{}; + long long mCompletedSize{}; + int mRow; + WantedState mWantedState{}; + Priority mPriority{}; - bool recalculateFromChildren(); + struct FileData { + mutable QIcon icon{}; + mutable bool initializedIcon{}; + int id; + }; - private: - void addChild(std::unique_ptr&& child); + struct DirectoryData { + std::vector children; + }; - std::vector> mChildren; - }; + std::variant mFileOrDirectoryData; - class TorrentFilesModelFile final : public TorrentFilesModelEntry { - public: - explicit TorrentFilesModelFile( + inline TorrentFilesModelEntry( int row, - TorrentFilesModelDirectory* parentDirectory, - int id, QString name, long long size, long long completedSize, bool wanted, - TorrentFilesModelEntry::Priority priority - ); - - inline bool isDirectory() const override { return false; } - - QIcon icon() const override; - - inline int id() const { return mId; } - - inline void getFileIds(std::vector& ids) const override { ids.push_back(mId); } - - private: - mutable QIcon mIcon{}; - mutable bool mInitializedIcon{}; - int mId; + Priority priority, + FileData data + ) + : mName(std::move(name)), + mSize(size), + mCompletedSize(completedSize), + mRow(row), + mWantedState(wanted ? WantedState::Wanted : WantedState::Unwanted), + mPriority(priority), + mFileOrDirectoryData(std::move(data)) {} + + inline TorrentFilesModelEntry(int row, QString name, DirectoryData data) + : mName(std::move(name)), mRow(row), mFileOrDirectoryData(std::move(data)) {} }; } diff --git a/src/ui/itemmodels/torrentfilestreebuilder.h b/src/ui/itemmodels/torrentfilestreebuilder.h index 565b65eb..a883dccb 100644 --- a/src/ui/itemmodels/torrentfilestreebuilder.h +++ b/src/ui/itemmodels/torrentfilestreebuilder.h @@ -5,6 +5,7 @@ #ifndef TREMOTESF_TORRENTFILESTREEBUILDER_H #define TREMOTESF_TORRENTFILESTREEBUILDER_H +#include #include #include #include @@ -22,27 +23,70 @@ namespace tremotesf { class TorrentFilesTreeBuilder final { public: - explicit TorrentFilesTreeBuilder(TorrentFilesModelDirectory* rootDirectory, size_t reserveFilesCount) - : rootDirectory(rootDirectory) { - files.reserve(reserveFilesCount); - } + explicit TorrentFilesTreeBuilder(size_t reserveFilesCount) { files.reserve(reserveFilesCount); } ~TorrentFilesTreeBuilder() = default; Q_DISABLE_COPY_MOVE(TorrentFilesTreeBuilder) + void initializeRootDirectory(QString name) { + rootEntry = + std::make_unique(TorrentFilesModelEntry::createDirectory(0, std::move(name))); + } + template requires(std::ranges::input_range && impl::QStringOrView>) void addFile( PathParts&& pathParts, + bool pathIncludesFirstPart, long long size, long long completedSize, bool wanted, TorrentFilesModelEntry::Priority priority ) { - TorrentFilesModelDirectory* currentDirectory = rootDirectory; + auto iter = pathParts.begin(); + if (iter == pathParts.end()) return; + + if (pathIncludesFirstPart) { + if (!rootEntry) { + auto firstPart = *iter; + ++iter; + if (iter == pathParts.end()) { + // This is a file not in any directory. We don't expect any addFile() calls + rootEntry = std::make_unique(TorrentFilesModelEntry::createFile( + 0, + 0, + impl::toString(firstPart), + size, + completedSize, + wanted, + priority + )); + files.push_back(rootEntry.get()); + return; + } + initializeRootDirectory(impl::toString(firstPart)); + } else { + if (!rootEntry->isDirectory()) { + // Root entry must be a directory + return; + } + ++iter; + if (iter == pathParts.end()) { + // This is a file not in any directory, but we already have root entry? This shouldn't happen + return; + } + } + } else if (!rootEntry || !rootEntry->isDirectory()) { + // If path doesn't include first part then root entry must be initialized using initializeRootDirectory() + return; + } + + // This a file in a directory(ies) + + TorrentFilesModelEntry* currentDirectory = rootEntry.get(); + DirectoryCacheEntry* currentDirectoryCacheEntry = &mRootDirectoryCacheEntry; - auto iter = pathParts.begin(); while (true) { auto part = *iter; ++iter; @@ -50,51 +94,73 @@ namespace tremotesf { // This was not the last part, therefore a directory name const auto found = currentDirectoryCacheEntry->childDirectoriesCache.find(part); if (found != currentDirectoryCacheEntry->childDirectoriesCache.constEnd()) { - currentDirectory = found.value().directory; + currentDirectory = ¤tDirectory->children()[found.value().indexInParent]; currentDirectoryCacheEntry = &found.value(); } else { - const auto& partString = impl::toString(part); - currentDirectory = currentDirectory->addDirectory(partString); + const auto partString = impl::toString(part); + auto& children = currentDirectory->children(); + children.push_back( + TorrentFilesModelEntry::createDirectory(static_cast(children.size()), partString) + ); + const auto indexInParent = children.size() - 1; + currentDirectory = &children.back(); const auto inserted = currentDirectoryCacheEntry->childDirectoriesCache.emplace( partString, - DirectoryCacheEntry{.directory = currentDirectory} + DirectoryCacheEntry{.indexInParent = indexInParent} ); currentDirectoryCacheEntry = &inserted.value(); } } else { // This was the last part, therefore a file name - auto* const file = - currentDirectory->addFile(mFileId, impl::toString(part), size, completedSize, wanted, priority); - files.push_back(file); + auto& children = currentDirectory->children(); + children.push_back( + TorrentFilesModelEntry::createFile( + mFileId, + static_cast(children.size()), + impl::toString(part), + size, + completedSize, + wanted, + priority + ) + ); ++mFileId; break; } } } - void calculateDirectoriesRecursively() { calculateFromChildrenRecursively(rootDirectory); } + void calculateDirectoriesRecursively() { + if (!rootEntry) return; + if (!rootEntry->isDirectory()) return; + calculateFromChildrenRecursively(rootEntry.get()); + std::ranges::sort(files, std::ranges::less{}, &TorrentFilesModelEntry::fileId); + } - TorrentFilesModelDirectory* rootDirectory; - std::vector files{}; + std::unique_ptr rootEntry{}; + std::vector files{}; private: - void calculateFromChildrenRecursively(TorrentFilesModelDirectory* directory) { - const auto& children = directory->children(); + void calculateFromChildrenRecursively(TorrentFilesModelEntry* directory) { + auto& children = directory->children(); if (children.empty()) return; long long size{}; long long completedSize{}; - TorrentFilesModelEntry::WantedState wantedState = children.front()->wantedState(); - TorrentFilesModelEntry::Priority priority = children.front()->priority(); - for (const auto& child : children) { - if (child->isDirectory()) { - calculateFromChildrenRecursively(static_cast(child.get())); + TorrentFilesModelEntry::WantedState wantedState = children.front().wantedState(); + TorrentFilesModelEntry::Priority priority = children.front().priority(); + for (auto& child : children) { + child.setParentDirectory(directory); + if (child.isDirectory()) { + calculateFromChildrenRecursively(&child); + } else { + files.push_back(&child); } - size += child->size(); - completedSize += child->completedSize(); - if (wantedState != TorrentFilesModelEntry::WantedState::Mixed && child->wantedState() != wantedState) { + size += child.size(); + completedSize += child.completedSize(); + if (wantedState != TorrentFilesModelEntry::WantedState::Mixed && child.wantedState() != wantedState) { wantedState = TorrentFilesModelEntry::WantedState::Mixed; } - if (priority != TorrentFilesModelEntry::Priority::Mixed && child->priority() != priority) { + if (priority != TorrentFilesModelEntry::Priority::Mixed && child.priority() != priority) { priority = TorrentFilesModelEntry::Priority::Mixed; } } @@ -103,7 +169,7 @@ namespace tremotesf { } struct DirectoryCacheEntry { - TorrentFilesModelDirectory* directory; + size_t indexInParent; QHash childDirectoriesCache{}; }; DirectoryCacheEntry mRootDirectoryCacheEntry{}; diff --git a/src/ui/screens/addtorrent/localtorrentfilesmodel.cpp b/src/ui/screens/addtorrent/localtorrentfilesmodel.cpp index c23db490..137292e9 100644 --- a/src/ui/screens/addtorrent/localtorrentfilesmodel.cpp +++ b/src/ui/screens/addtorrent/localtorrentfilesmodel.cpp @@ -12,31 +12,33 @@ namespace tremotesf { namespace { struct CreateTreeResult { - std::unique_ptr rootDirectory; - std::vector files; + std::unique_ptr rootEntry; + std::vector files; }; CreateTreeResult createTree(TorrentMetainfoFile torrentFile) { - auto rootDirectory = std::make_unique(); if (torrentFile.isSingleFile() == 1) { - auto* const file = rootDirectory->addFile( + auto rootEntry = std::make_unique(TorrentFilesModelEntry::createFile( 0, - torrentFile.rootFileName, + 0, + std::move(torrentFile.rootFileName), torrentFile.singleFileSize(), 0, true, TorrentFilesModelEntry::Priority::Normal - ); - return {.rootDirectory = std::move(rootDirectory), .files = {file}}; + )); + auto rootEntryPtr = rootEntry.get(); + return {.rootEntry = std::move(rootEntry), .files = {rootEntryPtr}}; } - const auto files = torrentFile.files(); - TorrentFilesTreeBuilder builder(rootDirectory->addDirectory(torrentFile.rootFileName), files.size()); + auto files = torrentFile.files(); + TorrentFilesTreeBuilder builder(files.size()); + builder.initializeRootDirectory(std::move(torrentFile.rootFileName)); for (TorrentMetainfoFile::File file : files) { - builder.addFile(file.path(), file.size, 0, true, TorrentFilesModelEntry::Priority::Normal); + builder.addFile(file.path(), false, file.size, 0, true, TorrentFilesModelEntry::Priority::Normal); } builder.calculateDirectoriesRecursively(); - return {.rootDirectory = std::move(rootDirectory), .files = std::move(builder.files)}; + return {.rootEntry = std::move(builder.rootEntry), .files = std::move(builder.files)}; } } @@ -46,8 +48,8 @@ namespace tremotesf { Coroutine<> LocalTorrentFilesModel::load(TorrentMetainfoFile torrentFile) { beginResetModel(); try { - auto [rootDirectory, files] = co_await runOnThreadPool(&createTree, std::move(torrentFile)); - mRootDirectory = std::move(rootDirectory); + auto [rootEntry, files] = co_await runOnThreadPool(&createTree, std::move(torrentFile)); + mRootEntry = std::move(rootEntry); mFiles = std::move(files); mLoaded = true; endResetModel(); @@ -61,9 +63,9 @@ namespace tremotesf { std::vector LocalTorrentFilesModel::unwantedFiles() const { std::vector files; - for (const TorrentFilesModelFile* file : mFiles) { + for (const TorrentFilesModelEntry* file : mFiles) { if (file->wantedState() == TorrentFilesModelEntry::WantedState::Unwanted) { - files.push_back(file->id()); + files.push_back(file->fileId()); } } return files; @@ -71,9 +73,9 @@ namespace tremotesf { std::vector LocalTorrentFilesModel::highPriorityFiles() const { std::vector files; - for (const TorrentFilesModelFile* file : mFiles) { + for (const TorrentFilesModelEntry* file : mFiles) { if (file->priority() == TorrentFilesModelEntry::Priority::High) { - files.push_back(file->id()); + files.push_back(file->fileId()); } } return files; @@ -81,9 +83,9 @@ namespace tremotesf { std::vector LocalTorrentFilesModel::lowPriorityFiles() const { std::vector files; - for (const TorrentFilesModelFile* file : mFiles) { + for (const TorrentFilesModelEntry* file : mFiles) { if (file->priority() == TorrentFilesModelEntry::Priority::Low) { - files.push_back(file->id()); + files.push_back(file->fileId()); } } return files; diff --git a/src/ui/screens/torrentproperties/torrentfilesmodel.cpp b/src/ui/screens/torrentproperties/torrentfilesmodel.cpp index d72de690..51554a66 100644 --- a/src/ui/screens/torrentproperties/torrentfilesmodel.cpp +++ b/src/ui/screens/torrentproperties/torrentfilesmodel.cpp @@ -32,13 +32,13 @@ namespace tremotesf { return ids; } - std::pair, std::vector> + std::pair, std::vector> doCreateTree(const std::vector& files) { - auto rootDirectory = std::make_unique(); - TorrentFilesTreeBuilder builder(rootDirectory.get(), files.size()); + TorrentFilesTreeBuilder builder(files.size()); for (const TorrentFile& file : files) { builder.addFile( file.pathParts(), + true, file.size, file.completedSize, file.wanted, @@ -46,7 +46,7 @@ namespace tremotesf { ); } builder.calculateDirectoriesRecursively(); - return {std::move(rootDirectory), std::move(builder.files)}; + return {std::move(builder.rootEntry), std::move(builder.files)}; } } @@ -106,17 +106,17 @@ namespace tremotesf { } void TorrentFilesModel::fileRenamed(const QString& path, const QString& newName) { - if (!mLoaded || !mRootDirectory) { + if (!mLoaded || !mRootEntry) { return; } - TorrentFilesModelEntry* entry = mRootDirectory.get(); + TorrentFilesModelEntry* entry = mRootEntry.get(); const auto parts = path.split('/', Qt::SkipEmptyParts); for (const QString& part : parts) { if (!entry->isDirectory()) return; - const auto& children = static_cast(entry)->children(); + auto& children = static_cast(entry)->children(); const auto found = std::ranges::find(children, part, &TorrentFilesModelEntry::name); if (found == children.end()) return; - entry = found->get(); + entry = &*found; } BaseTorrentFilesModel::fileRenamed(entry, newName); } @@ -157,12 +157,12 @@ namespace tremotesf { mCreatingTree = true; beginResetModel(); - auto [rootDirectory, files] = co_await runOnThreadPool( + auto [rootEntry, files] = co_await runOnThreadPool( [](const std::vector& files) { return doCreateTree(files); }, mTorrent->files() ); - mRootDirectory = std::move(rootDirectory); + mRootEntry = std::move(rootEntry); endResetModel(); mFiles = std::move(files); @@ -174,7 +174,7 @@ namespace tremotesf { void TorrentFilesModel::resetTree() { if (mLoaded) { beginResetModel(); - mRootDirectory.reset(); + mRootEntry.reset(); endResetModel(); mFiles.clear(); setLoaded(false); @@ -183,7 +183,7 @@ namespace tremotesf { void TorrentFilesModel::updateTree(std::span changed) { const auto& jsons = mTorrent->files(); - updateFiles(changed, [&](size_t i, TorrentFilesModelFile* file) { + updateFiles(changed, [&](size_t i, TorrentFilesModelEntry* file) { const auto& json = jsons.at(i); file->update(json.wanted, TorrentFilesModelEntry::fromFilePriority(json.priority), json.completedSize); });