diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -54,6 +54,8 @@ model/autotranslatelanguagesmodel.cpp model/commandsmodel.cpp + + model/accountschannelsmodel.cpp ) set(Ruqola_plugins_srcs diff --git a/src/core/autotests/CMakeLists.txt b/src/core/autotests/CMakeLists.txt --- a/src/core/autotests/CMakeLists.txt +++ b/src/core/autotests/CMakeLists.txt @@ -8,6 +8,7 @@ target_link_libraries( ${_name} Qt5::Test libruqolacore) endmacro() +add_ruqola_test(accountschannelsmodeltest.cpp) add_ruqola_test(rocketchatmessagetest.cpp) add_ruqola_test(roommodeltest.cpp) add_ruqola_test(messagemodeltest.cpp) diff --git a/src/core/autotests/accountschannelsmodeltest.h b/src/core/autotests/accountschannelsmodeltest.h new file mode 100644 --- /dev/null +++ b/src/core/autotests/accountschannelsmodeltest.h @@ -0,0 +1,37 @@ +/* + Copyright (c) 2020 Olivier de Gaalon + + This library is free software; you can redistribute it and/or modify + it under the terms of the GNU Library General Public License as published + by the Free Software Foundation; either version 2 of the License or + ( at your option ) version 3 or, at the discretion of KDE e.V. + ( which shall act as a proxy as in section 14 of the GPLv3 ), any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef ACCOUNTSCHANNELSMODELTEST_H +#define ACCOUNTSCHANNELSMODELTEST_H + +#include + +class AccountsChannelsModelTest : public QObject +{ + Q_OBJECT +public: + explicit AccountsChannelsModelTest(QObject *parent = nullptr); + +private Q_SLOTS: + void initTestCase(); + void accountsAndChannels(); +}; + +#endif // ACCOUNTSCHANNELSMODELTEST_H diff --git a/src/core/autotests/accountschannelsmodeltest.cpp b/src/core/autotests/accountschannelsmodeltest.cpp new file mode 100644 --- /dev/null +++ b/src/core/autotests/accountschannelsmodeltest.cpp @@ -0,0 +1,88 @@ +/* + Copyright (c) 2020 Olivier de Gaalon + + This library is free software; you can redistribute it and/or modify + it under the terms of the GNU Library General Public License as published + by the Free Software Foundation; either version 2 of the License or + ( at your option ) version 3 or, at the discretion of KDE e.V. + ( which shall act as a proxy as in section 14 of the GPLv3 ), any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "accountschannelsmodeltest.h" + +#include "accountmanager.h" +#include "model/accountschannelsmodel.h" +#include "model/roomfilterproxymodel.h" +#include "rocketchataccount.h" +#include "ruqola.h" + +#include +#include +#include + +QTEST_MAIN(AccountsChannelsModelTest) + +AccountsChannelsModelTest::AccountsChannelsModelTest(QObject *parent) + : QObject(parent) +{ +} + +void AccountsChannelsModelTest::initTestCase() +{ + QStandardPaths::setTestModeEnabled(true); +} + +void AccountsChannelsModelTest::accountsAndChannels() +{ + AccountsChannelsModel model; + QAbstractItemModelTester tester(&model, QAbstractItemModelTester::FailureReportingMode::QtTest); + + QCOMPARE(model.rowCount(), 1); // Ruqola creates one account by default + QCOMPARE(model.data(model.index(1, 0)).toString(), QStringLiteral("")); + QCOMPARE(model.rowCount(model.index(1, 0)), 0); + + const auto newAcctName = QStringLiteral("Test Account"); + const auto acct = new RocketChatAccount; + Ruqola::self()->accountManager()->addAccount(acct); + const auto newAcctIndex = model.index(1, 0); + QCOMPARE(model.rowCount(), 2); + QVERIFY(!model.hasChildren(newAcctIndex)); + QCOMPARE(model.data(newAcctIndex).toString(), QString()); + acct->setAccountName(newAcctName); + QCOMPARE(model.data(newAcctIndex).toString(), newAcctName); + + Ruqola::self()->setCurrentAccount(newAcctName); + + const auto newRoomId = QStringLiteral("RoomId"); + const auto newRoomName = QStringLiteral("Room Name"); + acct->roomModel()->addRoom(newRoomId, newRoomName); + QCOMPARE(model.rowCount(newAcctIndex), 0); // Room not yet open + + // FIXME: RoomModel should probably emit dataChanged to allow the sort/filter to update + acct->roomModel()->findRoom(newRoomId)->setOpen(true); + QEXPECT_FAIL("", "RoomModel missing dataChanged", Continue); + QCOMPARE(model.rowCount(newAcctIndex), 1); + // ... workaround for the above + acct->roomFilterProxyModel()->invalidate(); + // ... and try again + QCOMPARE(model.rowCount(newAcctIndex), 1); + + const auto newRoomIndex = model.index(0, 0, newAcctIndex); + QVERIFY(!model.hasChildren(newRoomIndex)); + QCOMPARE(model.data(newRoomIndex).toString(), newRoomName); + + // TODO: RoomsModel currently has no API for removing rooms + + Ruqola::self()->accountManager()->removeAccount(newAcctName); + QCOMPARE(model.rowCount(), 1); // Only the default account remains +} diff --git a/src/core/model/accountschannelsmodel.h b/src/core/model/accountschannelsmodel.h new file mode 100644 --- /dev/null +++ b/src/core/model/accountschannelsmodel.h @@ -0,0 +1,57 @@ +/* + Copyright (c) 2020 Olivier de Gaalon + + This library is free software; you can redistribute it and/or modify + it under the terms of the GNU Library General Public License as published + by the Free Software Foundation; either version 2 of the License or + ( at your option ) version 3 or, at the discretion of KDE e.V. + ( which shall act as a proxy as in section 14 of the GPLv3 ), any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef ACCOUNTSCHANNELSMODEL_H +#define ACCOUNTSCHANNELSMODEL_H + +#include +#include + +#include "libruqolacore_export.h" + +class RocketChatAccount; +class RoomFilterProxyModel; + +class LIBRUQOLACORE_EXPORT AccountsChannelsModel : public QAbstractItemModel +{ +public: + explicit AccountsChannelsModel(QObject *parent = nullptr); + + QModelIndex index(int row, int column, const QModelIndex &parent = {}) const override; + QModelIndex parent(const QModelIndex &child) const override; + int rowCount(const QModelIndex &parent = {}) const override; + int columnCount(const QModelIndex &parent = {}) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + +private: + QModelIndex modelRoot(QAbstractItemModel *model) const; + QAbstractItemModel *rootModel(const QModelIndex &root) const; + void mapModelToIndex(QAbstractItemModel *model, const std::function &root); + void unproxyModel(QAbstractItemModel *model); + + struct ProxyIndex + { + QAbstractItemModel *model; + std::function root; + }; + QVector mProxied; +}; + +#endif // ACCOUNTSCHANNELSMODEL_H diff --git a/src/core/model/accountschannelsmodel.cpp b/src/core/model/accountschannelsmodel.cpp new file mode 100644 --- /dev/null +++ b/src/core/model/accountschannelsmodel.cpp @@ -0,0 +1,181 @@ +/* + Copyright (c) 2020 Olivier de Gaalon + + This library is free software; you can redistribute it and/or modify + it under the terms of the GNU Library General Public License as published + by the Free Software Foundation; either version 2 of the License or + ( at your option ) version 3 or, at the discretion of KDE e.V. + ( which shall act as a proxy as in section 14 of the GPLv3 ), any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "accountschannelsmodel.h" + +#include "accountmanager.h" +#include "ruqola.h" +#include "rocketchataccount.h" +#include "roomfilterproxymodel.h" +#include "rocketchataccountmodel.h" +#include "rocketchataccountfilterproxymodel.h" + +AccountsChannelsModel::AccountsChannelsModel(QObject *parent) + : QAbstractItemModel(parent) +{ + const auto src = Ruqola::self()->accountManager()->rocketChatAccountModel(); + const auto acctsProxy = Ruqola::self()->accountManager()->rocketChatAccountProxyModel(); + + auto roomsModel = [src, acctsProxy](int i) { + const auto acctIndex = acctsProxy->mapToSource(acctsProxy->index(i, 0)).row(); + return src->account(acctIndex)->roomFilterProxyModel(); + }; + + auto mapRoomsModel = [roomsModel, acctsProxy, this](int roomsModelIndex) { + auto rooms = roomsModel(roomsModelIndex); + mapModelToIndex(rooms, [roomsModel, acctsProxy, rooms, this] { + for (int i = 0, count = acctsProxy->rowCount(); i < count; ++i) + if (roomsModel(i) == rooms) + return index(i, 0); + return QModelIndex(); + }); + }; + + connect(acctsProxy, &QAbstractItemModel::rowsInserted, this, + [mapRoomsModel](const QModelIndex &, int first, int last) { + for (int i = first; i <= last; ++i) + mapRoomsModel(i); + }); + + connect(acctsProxy, &QAbstractItemModel::rowsAboutToBeRemoved, this, + [roomsModel, this](const QModelIndex &, int first, int last) { + for (int i = first; i <= last; ++i) + unproxyModel(roomsModel(i)); + }); + + connect(acctsProxy, &QAbstractItemModel::modelReset, this, + [mapRoomsModel, acctsProxy, this]() { + while (!mProxied.isEmpty()) + unproxyModel(mProxied.begin()->model); + for (int i = 0, count = acctsProxy->rowCount(); i < count; ++i) + mapRoomsModel(i); + }); + + + mapModelToIndex(acctsProxy, []{ return QModelIndex(); }); + for (int i = 0, count = acctsProxy->rowCount(); i < count; ++i) + mapRoomsModel(i); +} + +QModelIndex AccountsChannelsModel::index(int row, int column, const QModelIndex &parent) const +{ + if (auto model = rootModel(parent)) + return createIndex(row, column, model); + return {}; +} + +QModelIndex AccountsChannelsModel::parent(const QModelIndex &child) const +{ + if (!child.isValid()) + return {}; + + if (auto model = static_cast(child.internalPointer())) + return modelRoot(model); + + return {}; +} + +int AccountsChannelsModel::rowCount(const QModelIndex &parent) const +{ + if (auto model = rootModel(parent)) + return model->rowCount(); + return 0; +} + +int AccountsChannelsModel::columnCount(const QModelIndex &) const +{ + return 1; +} + +QVariant AccountsChannelsModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return {}; + + const auto model = static_cast(index.internalPointer()); + if (!model) + return {}; + + return model->index(index.row(), index.column()).data(role); +} + +QModelIndex AccountsChannelsModel::modelRoot(QAbstractItemModel *model) const +{ + const auto find = [model](const ProxyIndex &i){ return i.model == model; }; + const auto it = std::find_if(mProxied.begin(), mProxied.end(), find); + return (it == mProxied.end()) ? QModelIndex() : it->root(); +} + +QAbstractItemModel *AccountsChannelsModel::rootModel(const QModelIndex &root) const +{ + const auto find = [&root](const ProxyIndex &i){ return i.root() == root; }; + const auto it = std::find_if(mProxied.begin(), mProxied.end(), find); + return (it == mProxied.end()) ? nullptr : it->model; +} + +void AccountsChannelsModel::mapModelToIndex(QAbstractItemModel *model, const std::function &root) +{ + connect(model, &QAbstractItemModel::rowsAboutToBeInserted, this, + [this, model](const QModelIndex &parent, int first, int last) { + Q_ASSERT(!parent.isValid()); + beginInsertRows(modelRoot(model), first, last); + }); + connect(model, &QAbstractItemModel::rowsInserted, this, &AccountsChannelsModel::endInsertRows); + + connect(model, &QAbstractItemModel::rowsAboutToBeRemoved, this, + [this, model](const QModelIndex &parent, int first, int last) { + Q_ASSERT(!parent.isValid()); + beginRemoveRows(modelRoot(model), first, last); + }); + connect(model, &QAbstractItemModel::rowsRemoved, this, &AccountsChannelsModel::endRemoveRows); + + connect(model, &QAbstractItemModel::rowsAboutToBeMoved, this, + [this, model](const QModelIndex &src, int sf, int sl, const QModelIndex &dst, int df) { + Q_ASSERT(!src.isValid() && !dst.isValid()); + const auto idx = modelRoot(model); + beginMoveRows(idx, sf, sl, idx, df); + }); + connect(model, &QAbstractItemModel::rowsMoved, this, &AccountsChannelsModel::endMoveRows); + + connect(model, &QAbstractItemModel::modelAboutToBeReset, this, &AccountsChannelsModel::beginResetModel); + connect(model, &QAbstractItemModel::modelReset, this, &AccountsChannelsModel::endResetModel); + + connect(model, &QAbstractItemModel::layoutAboutToBeChanged, this, &AccountsChannelsModel::layoutAboutToBeChanged); + connect(model, &QAbstractItemModel::layoutChanged, this, &AccountsChannelsModel::layoutChanged); + + connect(model, &QAbstractItemModel::dataChanged, this, + [this, model](const QModelIndex &tl, const QModelIndex &br) { + const auto parent = modelRoot(model); + emit dataChanged(index(tl.row(), tl.column(), parent), index(br.row(), br.column(), parent)); + }); + + mProxied.append({model, root}); +} + +void AccountsChannelsModel::unproxyModel(QAbstractItemModel *model) +{ + const auto find = [model](const ProxyIndex &i){ return i.model == model; }; + const auto it = std::find_if(mProxied.begin(), mProxied.end(), find); + if (it != mProxied.end()) + { + model->disconnect(this); + mProxied.erase(it); + } +} diff --git a/src/core/model/rocketchataccountmodel.cpp b/src/core/model/rocketchataccountmodel.cpp --- a/src/core/model/rocketchataccountmodel.cpp +++ b/src/core/model/rocketchataccountmodel.cpp @@ -113,6 +113,7 @@ const int idx = index.row(); RocketChatAccount *account = mRocketChatAccount.at(idx); switch (role) { + case Qt::DisplayRole: case Name: return account->accountName(); case SiteUrl: