// This file Copyright © Mnemosyne LLC. // It may be used under GPLv2 (SPDX: GPL-2.0-only), GPLv3 (SPDX: GPL-3.0-only), // or any future license endorsed by Mnemosyne LLC. // License text can be found in the licenses/ folder. #include #include #include #include #include #include #include #include // priorities #include #include #include "FileTreeItem.h" #include "FileTreeModel.h" namespace { class PathIteratorBase { protected: PathIteratorBase(QString const& path, int const slash_index) : path_{ path } , slash_index_{ slash_index } { token_.reserve(path.size() / 2); } QString const& path_; QString token_; int slash_index_; static QChar const SlashChar; }; QChar const PathIteratorBase::SlashChar = QLatin1Char('/'); class ForwardPathIterator : public PathIteratorBase { public: explicit ForwardPathIterator(QString const& path) : PathIteratorBase(path, path.size() - 1) { } [[nodiscard]] bool hasNext() const { return slash_index_ > -1; } QString const& next() { int const new_slash_index = path_.lastIndexOf(SlashChar, slash_index_); token_.truncate(0); token_.append(&path_.data()[new_slash_index + 1], slash_index_ - new_slash_index); slash_index_ = new_slash_index - 1; return token_; } }; class BackwardPathIterator : public PathIteratorBase { public: explicit BackwardPathIterator(QString const& path) : PathIteratorBase{ path, 0 } { } [[nodiscard]] bool hasNext() const { return slash_index_ < path_.size(); } QString const& next() { int new_slash_index = path_.indexOf(SlashChar, slash_index_); if (new_slash_index == -1) { new_slash_index = path_.size(); } token_.truncate(0); token_.append(&path_.data()[slash_index_], new_slash_index - slash_index_); slash_index_ = new_slash_index + 1; return token_; } }; } // namespace FileTreeModel::FileTreeModel(QObject* parent, bool is_editable) : QAbstractItemModel{ parent } , root_item_{ std::make_unique() } , is_editable_{ is_editable } { } FileTreeModel::~FileTreeModel() { clear(); } void FileTreeModel::setEditable(bool is_editable) { is_editable_ = is_editable; } FileTreeItem* FileTreeModel::itemFromIndex(QModelIndex const& index) const { if (!index.isValid()) { return nullptr; } assert(index.model() == this); return static_cast(index.internalPointer()); } QModelIndexList FileTreeModel::getOrphanIndices(QModelIndexList const& indices) const { QModelIndexList orphan_indices = indices; std::sort(orphan_indices.begin(), orphan_indices.end()); for (QMutableListIterator it(orphan_indices); it.hasNext();) { QModelIndex walk = it.next(); for (;;) { walk = parent(walk, walk.column()); if (!walk.isValid()) { break; } if (std::binary_search(orphan_indices.begin(), orphan_indices.end(), walk)) { it.remove(); break; } } } return orphan_indices; } QVariant FileTreeModel::data(QModelIndex const& index, int role) const { if (index.isValid()) { return itemFromIndex(index)->data(index.column(), role); } return {}; } Qt::ItemFlags FileTreeModel::flags(QModelIndex const& index) const { int i(Qt::ItemIsSelectable | Qt::ItemIsEnabled); if (is_editable_ && index.column() == COL_NAME) { i |= Qt::ItemIsEditable; } if (index.column() == COL_WANTED) { i |= Qt::ItemIsUserCheckable | Qt::ItemIsAutoTristate; } return { i }; } bool FileTreeModel::setData(QModelIndex const& index, QVariant const& value, int role) { if (role == Qt::EditRole) { FileTreeItem const* item = itemFromIndex(index); emit pathEdited(item->path(), value.toString()); } return false; // don't update the view until the session confirms the change } QVariant FileTreeModel::headerData(int column, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal && role == Qt::DisplayRole) { switch (column) { case COL_NAME: return tr("File"); case COL_SIZE: return tr("Size"); case COL_PROGRESS: return tr("Progress"); case COL_WANTED: return tr("Download"); case COL_PRIORITY: return tr("Priority"); default: break; } } return {}; } QModelIndex FileTreeModel::index(int row, int column, QModelIndex const& parent) const { if (hasIndex(row, column, parent)) { auto* const parent_item = parent.isValid() ? itemFromIndex(parent) : root_item_.get(); if (auto* const child_item = parent_item->child(row); child_item != nullptr) { return createIndex(row, column, child_item); } } return {}; } QModelIndex FileTreeModel::parent(QModelIndex const& child) const { return parent(child, 0); // QAbstractItemModel::parent() wants col 0 } QModelIndex FileTreeModel::parent(QModelIndex const& child, int column) const { QModelIndex parent; if (child.isValid()) { parent = indexOf(itemFromIndex(child)->parent(), column); } return parent; } int FileTreeModel::rowCount(QModelIndex const& parent) const { FileTreeItem const* parent_item = parent.isValid() ? itemFromIndex(parent) : root_item_.get(); return parent_item->childCount(); } int FileTreeModel::columnCount(QModelIndex const& parent) const { Q_UNUSED(parent) return NUM_COLUMNS; } QModelIndex FileTreeModel::indexOf(FileTreeItem* item, int column) const { if (item == nullptr || item == root_item_.get()) { return {}; } return createIndex(item->row(), column, item); } void FileTreeModel::clearSubtree(QModelIndex const& top) { size_t i = rowCount(top); while (i > 0) { clearSubtree(index(--i, 0, top)); } FileTreeItem* const item = itemFromIndex(top); if (item == nullptr) { return; } if (auto const idx = item->fileIndex(); idx != -1) { if (auto const iter = index_cache_.find(idx); iter != std::end(index_cache_)) { index_cache_.erase(iter); } } delete item; } void FileTreeModel::clear() { beginResetModel(); clearSubtree(QModelIndex{}); root_item_ = std::make_unique(); endResetModel(); assert(std::empty(index_cache_)); } FileTreeItem* FileTreeModel::findItemForFileIndex(int file_index) const { auto iter = index_cache_.find(file_index); return iter == std::end(index_cache_) ? nullptr : iter->second; } void FileTreeModel::addFile( int file_index, QString const& filename, bool wanted, int priority, uint64_t total_size, uint64_t have, bool update_fields) { auto* item = findItemForFileIndex(file_index); if (item != nullptr) // this file is already in the tree, we've added this { QModelIndex index_with_changed_parents; ForwardPathIterator filename_it(filename); while (filename_it.hasNext()) { auto const& token = filename_it.next(); auto const& [first_col, last_col] = item->update(token, wanted, priority, have, update_fields); if (first_col >= 0) { emit dataChanged(indexOf(item, first_col), indexOf(item, last_col)); if (!index_with_changed_parents.isValid() && first_col <= COL_PRIORITY && last_col >= COL_SIZE) { index_with_changed_parents = indexOf(item, 0); } } item = item->parent(); } assert(item == root_item_.get()); if (index_with_changed_parents.isValid()) { emitParentsChanged(index_with_changed_parents, COL_SIZE, COL_PRIORITY); } } else // we haven't build the FileTreeItems for these tokens yet { bool added = false; item = root_item_.get(); BackwardPathIterator filename_it(filename); while (filename_it.hasNext()) { QString const& token = filename_it.next(); FileTreeItem* child(item->child(token)); if (child == nullptr) { added = true; auto const parent_index = indexOf(item, 0); auto const n = item->childCount(); beginInsertRows(parent_index, n, n); if (!filename_it.hasNext()) { child = new FileTreeItem{ token, file_index, total_size }; } else { child = new FileTreeItem{ token }; } item->appendChild(child); endInsertRows(); } item = child; } if (item != root_item_.get()) { assert(item->fileIndex() == file_index); assert(item->totalSize() == total_size); index_cache_[file_index] = item; auto const [first_col, last_col] = item->update(item->name(), wanted, priority, have, added || update_fields); if (first_col >= 0) { emit dataChanged(indexOf(item, first_col), indexOf(item, last_col)); } } } } void FileTreeModel::emitParentsChanged( QModelIndex const& index, int first_column, int last_column, std::set* visited_parent_indices) { assert(first_column <= last_column); QModelIndex walk = index; for (;;) { walk = parent(walk, first_column); if (!walk.isValid()) { break; } if (visited_parent_indices != nullptr) { if (visited_parent_indices->count(walk) != 0U) { break; } visited_parent_indices->insert(walk); } emit dataChanged(walk, walk.sibling(walk.row(), last_column)); } } void FileTreeModel::emitSubtreeChanged(QModelIndex const& idx, int first_column, int last_column) { assert(first_column <= last_column); int const child_count = rowCount(idx); if (child_count == 0) { return; } // tell everyone that this item changed emit dataChanged(index(0, first_column, idx), index(child_count - 1, last_column, idx)); // walk the subitems for (int i = 0; i < child_count; ++i) { emitSubtreeChanged(index(i, 0, idx), first_column, last_column); } } void FileTreeModel::twiddleWanted(QModelIndexList const& indices) { auto wanted_indices = small::max_size_map{}; for (QModelIndex const& i : getOrphanIndices(indices)) { FileTreeItem const* const item = itemFromIndex(i); wanted_indices[item->isSubtreeWanted() != Qt::Checked] << i; } for (int i = 0; i <= 1; ++i) { if (wanted_indices.count(i) != 0) { setWanted(wanted_indices[i], i != 0); } } } void FileTreeModel::twiddlePriority(QModelIndexList const& indices) { auto priority_indices = small::max_size_map{}; for (QModelIndex const& i : getOrphanIndices(indices)) { FileTreeItem const* const item = itemFromIndex(i); int priority = item->priority(); // ... -> normal -> high -> low -> normal -> ...; mixed -> normal if (priority == FileTreeItem::Normal) { priority = TR_PRI_HIGH; } else if (priority == FileTreeItem::High) { priority = TR_PRI_LOW; } else { priority = TR_PRI_NORMAL; } priority_indices[priority] << i; } for (int i = TR_PRI_LOW; i <= TR_PRI_HIGH; ++i) { if (priority_indices.contains(i)) { setPriority(priority_indices[i], i); } } } void FileTreeModel::setWanted(QModelIndexList const& indices, bool wanted) { if (indices.isEmpty()) { return; } QModelIndexList const orphan_indices = getOrphanIndices(indices); auto file_ids = file_indices_t{}; for (QModelIndex const& i : orphan_indices) { FileTreeItem* const item = itemFromIndex(i); item->setSubtreeWanted(wanted, file_ids); emit dataChanged(i, i); emitSubtreeChanged(i, COL_WANTED, COL_WANTED); } // emit parent changes separately to avoid multiple updates for same items auto parent_indices = std::set{}; for (QModelIndex const& i : orphan_indices) { emitParentsChanged(i, COL_SIZE, COL_WANTED, &parent_indices); } if (!std::empty(file_ids)) { emit wantedChanged(file_ids, wanted); } } void FileTreeModel::setPriority(QModelIndexList const& indices, int priority) { if (indices.isEmpty()) { return; } QModelIndexList const orphan_indices = getOrphanIndices(indices); auto file_ids = file_indices_t{}; for (QModelIndex const& i : orphan_indices) { FileTreeItem* const item = itemFromIndex(i); item->setSubtreePriority(priority, file_ids); emit dataChanged(i, i); emitSubtreeChanged(i, COL_PRIORITY, COL_PRIORITY); } // emit parent changes separately to avoid multiple updates for same items auto parent_indices = std::set{}; for (QModelIndex const& i : orphan_indices) { emitParentsChanged(i, COL_PRIORITY, COL_PRIORITY, &parent_indices); } if (!std::empty(file_ids)) { emit priorityChanged(file_ids, priority); } } bool FileTreeModel::openFile(QModelIndex const& index) { if (!index.isValid()) { return false; } FileTreeItem const* const item = itemFromIndex(index); if (item->fileIndex() < 0 || !item->isComplete()) { return false; } emit openRequested(item->path()); return true; }