From 6cfab9e3ea3dad4c5ef54934b15c6302da4ca201 Mon Sep 17 00:00:00 2001 From: James Graham Date: Tue, 2 Apr 2024 14:44:20 +0000 Subject: [PATCH] Tree Model 2 Electric Boogaloo This draws heavily on what @carlschwan did in network/neochat!1579 but I found it easier to start again and grab the bits as I needed them plus some other copying from what I did in the Space tree model. From my current limited testing this seems to work nicely try and break it. --- src/CMakeLists.txt | 2 + src/enums/neochatroomtype.h | 1 + src/models/roomtreeitem.cpp | 100 ++++++++++++++ src/models/roomtreeitem.h | 78 +++++++++++ src/models/roomtreemodel.cpp | 179 +++++++++++++++++-------- src/models/roomtreemodel.h | 7 +- src/models/sortfilterroomtreemodel.cpp | 1 - 7 files changed, 308 insertions(+), 60 deletions(-) create mode 100644 src/models/roomtreeitem.cpp create mode 100644 src/models/roomtreeitem.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index eeea72eb9..de856b40b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -172,6 +172,8 @@ add_library(neochat STATIC models/statekeysmodel.h sharehandler.cpp sharehandler.h + models/roomtreeitem.cpp + models/roomtreeitem.h ) set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES diff --git a/src/enums/neochatroomtype.h b/src/enums/neochatroomtype.h index 0dc9aa31d..368532168 100644 --- a/src/enums/neochatroomtype.h +++ b/src/enums/neochatroomtype.h @@ -29,6 +29,7 @@ public: Deprioritized, /**< The room is set as low priority. */ Space, /**< The room is a space. */ AddDirect, /**< So we can show the add friend delegate. */ + TypesCount, /**< Number of different types (this should always be last). */ }; Q_ENUM(Types); diff --git a/src/models/roomtreeitem.cpp b/src/models/roomtreeitem.cpp new file mode 100644 index 000000000..0a48c667b --- /dev/null +++ b/src/models/roomtreeitem.cpp @@ -0,0 +1,100 @@ +// SPDX-FileCopyrightText: 2024 Carl Schwan +// SPDX-FileCopyrightText: 2024 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +#include "roomtreeitem.h" + +RoomTreeItem::RoomTreeItem(TreeData data, RoomTreeItem *parent) + : m_parentItem(parent) + , m_data(data) +{ +} + +bool RoomTreeItem::operator==(const RoomTreeItem &other) const +{ + if (std::holds_alternative(m_data) && std::holds_alternative(other.data())) { + return std::get(m_data) == std::get(m_data); + } + if (std::holds_alternative(m_data) && std::holds_alternative(other.data())) { + return std::get(m_data)->id() == std::get(m_data)->id(); + } + return false; +} + +RoomTreeItem *RoomTreeItem::child(int row) +{ + return row >= 0 && row < childCount() ? m_children.at(row).get() : nullptr; +} + +int RoomTreeItem::childCount() const +{ + return int(m_children.size()); +} + +bool RoomTreeItem::insertChild(std::unique_ptr newChild) +{ + if (newChild == nullptr) { + return false; + } + + for (auto it = m_children.begin(), end = m_children.end(); it != end; ++it) { + if (*it == newChild) { + *it = std::move(newChild); + return true; + } + } + + m_children.push_back(std::move(newChild)); + return true; +} + +bool RoomTreeItem::removeChild(int row) +{ + if (row < 0 || row >= childCount()) { + return false; + } + m_children.erase(m_children.begin() + row); + return true; +} + +int RoomTreeItem::row() const +{ + if (m_parentItem == nullptr) { + return 0; + } + + const auto it = std::find_if(m_parentItem->m_children.cbegin(), m_parentItem->m_children.cend(), [this](const std::unique_ptr &treeItem) { + return treeItem.get() == this; + }); + + if (it != m_parentItem->m_children.cend()) { + return std::distance(m_parentItem->m_children.cbegin(), it); + } + Q_ASSERT(false); // should not happen + return -1; +} + +RoomTreeItem *RoomTreeItem::parentItem() const +{ + return m_parentItem; +} + +RoomTreeItem::TreeData RoomTreeItem::data() const +{ + return m_data; +} + +std::optional RoomTreeItem::rowForRoom(Quotient::Room *room) const +{ + Q_ASSERT_X(std::holds_alternative(m_data), __FUNCTION__, "rowForRoom only works items for rooms not categories"); + + int i = 0; + for (const auto &child : m_children) { + if (std::get(child->data()) == room) { + return i; + } + i++; + } + + return std::nullopt; +} diff --git a/src/models/roomtreeitem.h b/src/models/roomtreeitem.h new file mode 100644 index 000000000..6eaa8aebf --- /dev/null +++ b/src/models/roomtreeitem.h @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: 2024 Carl Schwan +// SPDX-FileCopyrightText: 2024 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +#include "enums/neochatroomtype.h" + +class NeoChatRoom; + +/** + * @class RoomTreeItem + * + * This class defines an item in the space tree hierarchy model. + * + * @note This is separate from Quotient::Room and NeoChatRoom because we don't have + * full room information for any room/space the user hasn't joined and we + * don't want to create one for ever possible child in a space as that would + * be expensive. + * + * @sa Quotient::Room, NeoChatRoom + */ +class RoomTreeItem +{ +public: + using TreeData = std::variant; + + explicit RoomTreeItem(TreeData data, RoomTreeItem *parent = nullptr); + + bool operator==(const RoomTreeItem &other) const; + + /** + * @brief Return the child at the given row number. + * + * Nullptr is returned if there is no child at the given row number. + */ + RoomTreeItem *child(int row); + + /** + * @brief The number of children this item has. + */ + int childCount() const; + + /** + * @brief Insert the given child. + */ + bool insertChild(std::unique_ptr newChild); + + /** + * @brief Remove the child at the given row number. + * + * @return True if a child was removed, false if the given row isn't valid. + */ + bool removeChild(int row); + + /** + * @brief Return this item's parent. + */ + RoomTreeItem *parentItem() const; + + /** + * @brief Return the row number for this child relative to the parent. + * + * @return The row value if the child has a parent, 0 otherwise. + */ + int row() const; + + /** + * @brief Return this item's data. + */ + TreeData data() const; + + std::optional rowForRoom(Quotient::Room *room) const; + +private: + std::vector> m_children; + RoomTreeItem *m_parentItem; + + TreeData m_data; +}; diff --git a/src/models/roomtreemodel.cpp b/src/models/roomtreemodel.cpp index f88e244d5..3693414bf 100644 --- a/src/models/roomtreemodel.cpp +++ b/src/models/roomtreemodel.cpp @@ -15,21 +15,47 @@ using namespace Quotient; RoomTreeModel::RoomTreeModel(QObject *parent) : QAbstractItemModel(parent) + , m_rootItem(new RoomTreeItem(nullptr)) { - initializeCategories(); } -void RoomTreeModel::initializeCategories() +RoomTreeItem *RoomTreeModel::getItem(const QModelIndex &index) const { - for (const auto &key : m_rooms.keys()) { - for (const auto &room : m_rooms[key]) { - room->disconnect(this); + if (index.isValid()) { + RoomTreeItem *item = static_cast(index.internalPointer()); + if (item) { + return item; } } - m_rooms.clear(); - for (int i = 0; i < 8; i++) { - m_rooms[NeoChatRoomType::Types(i)] = {}; + return m_rootItem.get(); +} + +void RoomTreeModel::resetModel() +{ + if (m_connection == nullptr) { + beginResetModel(); + m_rootItem.reset(); + endResetModel(); + return; } + + beginResetModel(); + m_rootItem.reset(new RoomTreeItem(nullptr)); + + for (int i = 0; i < NeoChatRoomType::TypesCount; i++) { + m_rootItem->insertChild(std::make_unique(NeoChatRoomType::Types(i), m_rootItem.get())); + } + + for (const auto &r : m_connection->allRooms()) { + const auto room = dynamic_cast(r); + const auto type = NeoChatRoomType::typeForRoom(room); + const auto categoryItem = m_rootItem->child(type); + if (categoryItem->insertChild(std::make_unique(room, categoryItem))) { + connectRoomSignals(room); + } + } + + endResetModel(); } void RoomTreeModel::setConnection(NeoChatConnection *connection) @@ -41,16 +67,13 @@ void RoomTreeModel::setConnection(NeoChatConnection *connection) disconnect(m_connection.get(), nullptr, this, nullptr); } m_connection = connection; - beginResetModel(); - initializeCategories(); - endResetModel(); + + resetModel(); + connect(connection, &Connection::newRoom, this, &RoomTreeModel::newRoom); connect(connection, &Connection::leftRoom, this, &RoomTreeModel::leftRoom); connect(connection, &Connection::aboutToDeleteRoom, this, &RoomTreeModel::leftRoom); - for (const auto &room : m_connection->allRooms()) { - newRoom(dynamic_cast(room)); - } Q_EMIT connectionChanged(); } @@ -68,23 +91,28 @@ void RoomTreeModel::newRoom(Room *r) return; } - beginInsertRows(index(type, 0), m_rooms[type].size(), m_rooms[type].size()); - m_rooms[type].append(room); + const auto parentItem = m_rootItem->child(type); + beginInsertRows(index(parentItem->row(), 0), parentItem->childCount(), parentItem->childCount()); + parentItem->insertChild(std::make_unique(room, parentItem)); connectRoomSignals(room); endInsertRows(); + qWarning() << "adding room" << type << "new count" << parentItem->childCount(); } void RoomTreeModel::leftRoom(Room *r) { const auto room = dynamic_cast(r); - const auto type = NeoChatRoomType::typeForRoom(room); - auto row = m_rooms[type].indexOf(room); - if (row == -1) { + auto index = indexForRoom(room); + if (!index.isValid()) { return; } - beginRemoveRows(index(type, 0), row, row); - m_rooms[type][row]->disconnect(this); - m_rooms[type].removeAt(row); + + const auto parentItem = getItem(index.parent()); + Q_ASSERT(parentItem); + + beginRemoveRows(index.parent(), index.row(), index.row()); + parentItem->removeChild(index.row()); + room->disconnect(this); endRemoveRows(); } @@ -94,30 +122,41 @@ void RoomTreeModel::moveRoom(Quotient::Room *room) // NeoChatRoomType::typeForRoom doesn't match it's current location. So find the room. NeoChatRoomType::Types oldType; int oldRow = -1; - for (const auto &key : m_rooms.keys()) { - if (m_rooms[key].contains(room)) { - oldType = key; - oldRow = m_rooms[key].indexOf(room); + for (int i = 0; i < NeoChatRoomType::TypesCount; i++) { + const auto categoryItem = m_rootItem->child(i); + const auto row = categoryItem->rowForRoom(room); + if (row) { + oldType = static_cast(i); + oldRow = *row; } } if (oldRow == -1) { return; } - const auto newType = NeoChatRoomType::typeForRoom(dynamic_cast(room)); + auto neochatRoom = dynamic_cast(room); + const auto newType = NeoChatRoomType::typeForRoom(neochatRoom); if (newType == oldType) { return; } const auto oldParent = index(oldType, 0, {}); + auto oldParentItem = getItem(oldParent); + Q_ASSERT(oldParentItem); + const auto newParent = index(newType, 0, {}); + auto newParentItem = getItem(newParent); + Q_ASSERT(newParentItem); + // HACK: We're doing this as a remove then insert because moving doesn't work // properly with DelegateChooser for whatever reason. + Q_ASSERT(checkIndex(index(oldRow, 0, oldParent), QAbstractItemModel::CheckIndexOption::IndexIsValid)); beginRemoveRows(oldParent, oldRow, oldRow); - m_rooms[oldType].removeAt(oldRow); + const bool success = oldParentItem->removeChild(oldRow); + Q_ASSERT(success); endRemoveRows(); - beginInsertRows(newParent, m_rooms[newType].size(), m_rooms[newType].size()); - m_rooms[newType].append(dynamic_cast(room)); + beginInsertRows(newParent, newParentItem->childCount(), newParentItem->childCount()); + newParentItem->insertChild(std::make_unique(neochatRoom, newParentItem)); endInsertRows(); } @@ -151,15 +190,12 @@ void RoomTreeModel::connectRoomSignals(NeoChatRoom *room) void RoomTreeModel::refreshRoomRoles(NeoChatRoom *room, const QList &roles) { - const auto roomType = NeoChatRoomType::typeForRoom(room); - const auto it = std::find(m_rooms[roomType].begin(), m_rooms[roomType].end(), room); - if (it == m_rooms[roomType].end()) { + const auto index = indexForRoom(room); + if (!index.isValid()) { qCritical() << "Room" << room->id() << "not found in the room list"; return; } - const auto parentIndex = index(roomType, 0, {}); - const auto idx = index(it - m_rooms[roomType].begin(), 0, parentIndex); - Q_EMIT dataChanged(idx, idx, roles); + Q_EMIT dataChanged(index, index, roles); } NeoChatConnection *RoomTreeModel::connection() const @@ -175,32 +211,55 @@ int RoomTreeModel::columnCount(const QModelIndex &parent) const int RoomTreeModel::rowCount(const QModelIndex &parent) const { + RoomTreeItem *parentItem; + if (parent.column() > 0) { + return 0; + } + if (!parent.isValid()) { - return m_rooms.keys().size(); + parentItem = m_rootItem.get(); + } else { + parentItem = static_cast(parent.internalPointer()); } - if (!parent.parent().isValid()) { - return m_rooms.values()[parent.row()].size(); - } - return 0; + + return parentItem->childCount(); } QModelIndex RoomTreeModel::parent(const QModelIndex &index) const { - if (!index.internalPointer()) { - return {}; + if (!index.isValid()) { + return QModelIndex(); } - return this->index(NeoChatRoomType::typeForRoom(static_cast(index.internalPointer())), 0, QModelIndex()); + + RoomTreeItem *childItem = static_cast(index.internalPointer()); + if (!childItem) { + return QModelIndex(); + } + RoomTreeItem *parentItem = childItem->parentItem(); + + if (parentItem == m_rootItem.get()) { + return QModelIndex(); + } + + return createIndex(parentItem->row(), 0, parentItem); } QModelIndex RoomTreeModel::index(int row, int column, const QModelIndex &parent) const { - if (!parent.isValid()) { - return createIndex(row, column, nullptr); + if (!hasIndex(row, column, parent)) { + return QModelIndex(); } - if (row >= rowCount(parent)) { - return {}; + + RoomTreeItem *parentItem = getItem(parent); + if (!parentItem) { + return QModelIndex(); } - return createIndex(row, column, m_rooms[NeoChatRoomType::Types(parent.row())][row]); + + RoomTreeItem *childItem = parentItem->child(row); + if (childItem) { + return createIndex(row, column, childItem); + } + return QModelIndex(); } QHash RoomTreeModel::roleNames() const @@ -235,7 +294,8 @@ QVariant RoomTreeModel::data(const QModelIndex &index, int role) const return QVariant(); } - if (!index.parent().isValid()) { + RoomTreeItem *child = getItem(index); + if (std::holds_alternative(child->data())) { if (role == DisplayNameRole) { return NeoChatRoomType::typeName(index.row()); } @@ -256,7 +316,8 @@ QVariant RoomTreeModel::data(const QModelIndex &index, int role) const } return {}; } - const auto room = m_rooms.values()[index.parent().row()][index.row()].get(); + + const auto room = std::get(child->data()); Q_ASSERT(room); if (role == DisplayNameRole) { @@ -338,16 +399,20 @@ QModelIndex RoomTreeModel::indexForRoom(NeoChatRoom *room) const // Try and find by checking type. const auto type = NeoChatRoomType::typeForRoom(room); - auto row = m_rooms[type].indexOf(room); - if (row >= 0) { - return index(row, 0, index(type, 0)); + const auto parentItem = m_rootItem->child(type); + const auto row = parentItem->rowForRoom(room); + if (row) { + return index(*row, 0, index(type, 0)); } // Double check that the room isn't in the wrong category. - for (const auto &key : m_rooms.keys()) { - if (m_rooms[key].contains(room)) { - return index(m_rooms[key].indexOf(room), 0, index(key, 0)); + for (int i = 0; i < NeoChatRoomType::TypesCount; i++) { + const auto parentItem = m_rootItem->child(i); + const auto row = parentItem->rowForRoom(room); + if (row) { + return index(*row, 0, index(i, 0)); } } + return {}; } diff --git a/src/models/roomtreemodel.h b/src/models/roomtreemodel.h index f4909f54f..7b2d32eda 100644 --- a/src/models/roomtreemodel.h +++ b/src/models/roomtreemodel.h @@ -7,6 +7,7 @@ #include #include "enums/neochatroomtype.h" +#include "roomtreeitem.h" namespace Quotient { @@ -83,9 +84,11 @@ Q_SIGNALS: private: QPointer m_connection; - QMap>> m_rooms; + std::unique_ptr m_rootItem; - void initializeCategories(); + RoomTreeItem *getItem(const QModelIndex &index) const; + + void resetModel(); void connectRoomSignals(NeoChatRoom *room); void newRoom(Quotient::Room *room); diff --git a/src/models/sortfilterroomtreemodel.cpp b/src/models/sortfilterroomtreemodel.cpp index 63eea77b1..b32fc58ca 100644 --- a/src/models/sortfilterroomtreemodel.cpp +++ b/src/models/sortfilterroomtreemodel.cpp @@ -24,7 +24,6 @@ SortFilterRoomTreeModel::SortFilterRoomTreeModel(RoomTreeModel *sourceModel, QOb setRecursiveFilteringEnabled(true); sort(0); - invalidateFilter(); connect(this, &SortFilterRoomTreeModel::filterTextChanged, this, &SortFilterRoomTreeModel::invalidateFilter); connect(this, &SortFilterRoomTreeModel::sourceModelChanged, this, [this]() { this->sourceModel()->disconnect(this);