Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -493,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()
2 changes: 1 addition & 1 deletion src/rpc/torrent.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -802,7 +802,7 @@ namespace tremotesf {
mFiles.reserve(static_cast<size_t>(count));
changed.reserve(static_cast<size_t>(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<int>(i));
}
} else {
Expand Down
9 changes: 2 additions & 7 deletions src/rpc/torrentfile.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,8 @@ 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<size_t>(p.size()));
for (QString& part : p) {
path.push_back(std::move(part));
}
TorrentFile::TorrentFile(const QJsonObject& fileMap, const QJsonObject& fileStatsMap)
: path(fileMap.value("name"_L1).toString()), size(fileMap.value("length"_L1).toInteger()) {
update(fileStatsMap);
}

Expand Down
9 changes: 4 additions & 5 deletions src/rpc/torrentfile.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
#ifndef TREMOTESF_RPC_TORRENTFILE_H
#define TREMOTESF_RPC_TORRENTFILE_H

#include <vector>
#include <QObject>
#include <QString>

Expand All @@ -18,16 +17,16 @@ 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{};

std::vector<QString> path{};
QString path{};
qint64 size{};
qint64 completedSize{};
Priority priority{};
bool wanted{};

auto pathParts() const { return path.tokenize(u'/', Qt::SkipEmptyParts); }
};
}

Expand Down
221 changes: 155 additions & 66 deletions src/ui/itemmodels/basetorrentfilesmodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@

#include "basetorrentfilesmodel.h"

#include <ranges>

#include <QCoreApplication>
#include <QHash>
#include <QIcon>
#include <QSet>

#include "desktoputils.h"
#include "itemlistupdater.h"
#include "formatutils.h"

namespace tremotesf {
Expand All @@ -21,7 +24,7 @@ namespace tremotesf {
if (!index.isValid()) {
return {};
}
const TorrentFilesModelEntry* entry = static_cast<TorrentFilesModelEntry*>(index.internalPointer());
const auto* const entry = static_cast<TorrentFilesModelEntry*>(index.internalPointer());
const Column column = mColumns.at(static_cast<size_t>(index.column()));
switch (role) {
case Qt::CheckStateRole:
Expand Down Expand Up @@ -120,115 +123,201 @@ namespace tremotesf {
return false;
}
if (static_cast<Column>(index.column()) == Column::Name && role == Qt::CheckStateRole) {
setFileWanted(index, (value.toInt() == Qt::Checked));
setFilesWanted({index}, (value.toInt() == Qt::Checked));
return true;
}
return false;
}

QModelIndex BaseTorrentFilesModel::index(int row, int column, const QModelIndex& parent) const {
const TorrentFilesModelDirectory* parentDirectory{};
if (parent.isValid()) {
parentDirectory = static_cast<TorrentFilesModelDirectory*>(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<TorrentFilesModelEntry*>(parent.internalPointer());
if (!parentEntry->isDirectory()) {
return {};
}
return createIndex(row, column, parentDirectory->children().at(static_cast<size_t>(row)).get());
return createIndex(row, column, &parentEntry->children().at(static_cast<size_t>(row)));
}

QModelIndex BaseTorrentFilesModel::parent(const QModelIndex& child) const {
if (!child.isValid()) {
return {};
}
const auto* const parentDirectory =
const auto* const parentDirectoryEntry =
static_cast<TorrentFilesModelEntry*>(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<TorrentFilesModelDirectory*>(parent.internalPointer());
const auto* const entry = static_cast<TorrentFilesModelEntry*>(parent.internalPointer());
if (entry->isDirectory()) {
return static_cast<int>(static_cast<const TorrentFilesModelDirectory*>(entry)->children().size());
return static_cast<int>(entry->children().size());
}
return 0;
}
if (mRootDirectory) {
return static_cast<int>(mRootDirectory->children().size());
} else if (mRootEntry) {
return 1;
}
return 0;
}

void BaseTorrentFilesModel::setFileWanted(const QModelIndex& index, bool wanted) {
if (index.isValid()) {
static_cast<TorrentFilesModelEntry*>(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<QModelIndex, std::vector<int>> mPendingSignals{};
};

void recalculateDirectoryAndItsParents(
TorrentFilesModelEntry* 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<TorrentFilesModelEntry*>(index.internalPointer())->setWanted(wanted);
template<std::invocable<TorrentFilesModelEntry*> UpdateState>
requires std::same_as<std::invoke_result_t<UpdateState, TorrentFilesModelEntry*>, bool>
void updateDirectoryChildrenRecursively(
TorrentFilesModelEntry* directory,
const QModelIndex& directoryIndex,
UpdateState updateState,
BaseTorrentFilesModel& model,
DataChangedDispatcher& dispatcher
) {
for (auto& entry : directory->children()) {
if (updateState(&entry)) {
dispatcher.add(directoryIndex, entry.row());
}
if (entry.isDirectory()) {
updateDirectoryChildrenRecursively(
&entry,
model.index(entry.row(), 0, directoryIndex),
updateState,
model,
dispatcher
);
}
}
}
updateDirectoryChildren();
}

void BaseTorrentFilesModel::setFilePriority(const QModelIndex& index, TorrentFilesModelEntry::Priority priority) {
if (index.isValid()) {
static_cast<TorrentFilesModelEntry*>(index.internalPointer())->setPriority(priority);
updateDirectoryChildren();
template<std::invocable<TorrentFilesModelEntry*> UpdateState>
requires std::same_as<std::invoke_result_t<UpdateState, TorrentFilesModelEntry*>, 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<std::pair<TorrentFilesModelEntry*, QModelIndex>> parentDirectoriesToRecalculate{};

for (const auto& index : indexes) {
auto* const entry = static_cast<TorrentFilesModelEntry*>(index.internalPointer());
if (updateState(entry)) {
dispatcher.add(index.parent(), index.row());
if (entry->isDirectory()) {
updateDirectoryChildrenRecursively(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<TorrentFilesModelEntry*>(index.internalPointer())->setPriority(priority);
}
}
updateDirectoryChildren();
setWantedOrPriority(
indexes,
[&](TorrentFilesModelEntry* entry) { return entry->setPriority(priority); },
*this
);
}

void BaseTorrentFilesModel::fileRenamed(TorrentFilesModelEntry* entry, const QString& newName) {
entry->setName(newName);
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<const TorrentFilesModelDirectory*>(parent.internalPointer());
} else if (mRootDirectory) {
directory = mRootDirectory.get();
} else {
return;
}

auto changedBatchProcessor = ItemBatchProcessor([&](size_t first, size_t last) {
emit dataChanged(
index(static_cast<int>(first), 0, parent),
index(static_cast<int>(last) - 1, columnCount() - 1, parent)
);
});

for (auto& child : directory->children()) {
if (child->isChanged()) {
changedBatchProcessor.nextIndex(static_cast<size_t>(child->row()));
if (child->isDirectory()) {
updateDirectoryChildren(index(child->row(), 0, parent));
} else {
static_cast<TorrentFilesModelFile*>(child.get())->setChanged(false);
void BaseTorrentFilesModel::updateFiles(
std::span<const int> changedFiles, std::function<void(size_t, TorrentFilesModelEntry*)>&& updateFile
) {
if (changedFiles.empty()) return;

DataChangedDispatcher dispatcher{};

for (int index : changedFiles) {
const auto sIndex = static_cast<size_t>(index);
auto* const file = mFiles.at(sIndex);
updateFile(sIndex, file);
auto* const parent = file->parentDirectory();
const auto parentIndex = [&] {
if (!parent) {
return QModelIndex{};
}
}
return createIndex(parent->row(), 0, parent);
}();
dispatcher.add(parentIndex, file->row());
recalculateDirectoryAndItsParents(parent, parentIndex, dispatcher);
}
changedBatchProcessor.commitIfNeeded();

dispatcher.dispatchSignals(*this);
}
}
10 changes: 6 additions & 4 deletions src/ui/itemmodels/basetorrentfilesmodel.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#ifndef TREMOTESF_BASETORRENTFILESMODEL_H
#define TREMOTESF_BASETORRENTFILESMODEL_H

#include <functional>
#include <memory>
#include <vector>
#include <QAbstractItemModel>
Expand All @@ -31,18 +32,19 @@ 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<const int> changedFiles, std::function<void(size_t, TorrentFilesModelEntry*)>&& updateFile
);

std::shared_ptr<TorrentFilesModelDirectory> mRootDirectory;
std::unique_ptr<TorrentFilesModelEntry> mRootEntry{};
std::vector<TorrentFilesModelEntry*> mFiles{};

private:
const std::vector<Column> mColumns;
Expand Down
Loading
Loading