StatusQ: Add generic proxy model for roles renaming

This commit is contained in:
Michał Cieślak 2023-10-24 10:55:25 +02:00 committed by Michał
parent a3239d9e2b
commit 628d9cdd31
7 changed files with 546 additions and 5 deletions

View File

@ -0,0 +1,156 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import StatusQ 0.1
Item {
id: root
RolesRenamingModel {
id: renamedModel
sourceModel: sourceModel
mapping: [
RoleRename {
from: "tokenId"
to: "id"
},
RoleRename {
from: "title"
to: "name"
}
]
}
ListModel {
id: sourceModel
ListElement {
tokenId: "1"
title: "Token 1"
communityId: "1"
}
ListElement {
tokenId: "2"
title: "Token 2"
communityId: "1"
}
ListElement {
tokenId: "3"
title: "Token 3"
communityId: "2"
}
ListElement {
tokenId: "4"
title: "Token 4"
communityId: "3"
}
ListElement {
tokenId: "5"
title: "Token 5"
communityId: ""
}
ListElement {
tokenId: "6"
title: "Token 6"
communityId: "1"
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 40
RowLayout {
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
border.color: "gray"
ListView {
anchors.fill: parent
model: sourceModel
header: Label {
height: implicitHeight * 2
text: `Left model (${sourceModel.count})`
font.bold: true
verticalAlignment: Text.AlignVCenter
}
ScrollBar.vertical: ScrollBar {}
delegate: Label {
width: ListView.view.width
text: `token id: ${model.tokenId}, ${model.title}, community id: ${model.communityId || "-"}`
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
border.color: "gray"
ListView {
id: renamedListView
anchors.fill: parent
model: renamedModel
header: Label {
height: implicitHeight * 2
text: `Renamed model (${renamedListView.count})`
font.bold: true
verticalAlignment: Text.AlignVCenter
}
ScrollBar.vertical: ScrollBar {}
delegate: Label {
width: ListView.view.width
text: `id: ${model.id}, ${model.name}, community id: ${model.communityId || "-"}`
}
}
}
}
RowLayout {
Layout.fillWidth: true
Button {
text: "shuffle"
onClicked: {
const count = sourceModel.count
const iterations = count / 2
for (let i = 0; i < iterations; i++) {
sourceModel.move(Math.floor(Math.random() * (count - 1)),
Math.floor(Math.random() * (count - 1)),
Math.floor(Math.random() * 2) + 1)
}
}
}
}
}
}
// category: Models

View File

@ -91,6 +91,7 @@ add_library(StatusQ SHARED
include/StatusQ/QClipboardProxy.h include/StatusQ/QClipboardProxy.h
include/StatusQ/modelutilsinternal.h include/StatusQ/modelutilsinternal.h
include/StatusQ/permissionutilsinternal.h include/StatusQ/permissionutilsinternal.h
include/StatusQ/rolesrenamingmodel.h
include/StatusQ/rxvalidator.h include/StatusQ/rxvalidator.h
include/StatusQ/statussyntaxhighlighter.h include/StatusQ/statussyntaxhighlighter.h
include/StatusQ/statuswindow.h include/StatusQ/statuswindow.h
@ -99,6 +100,7 @@ add_library(StatusQ SHARED
src/modelutilsinternal.cpp src/modelutilsinternal.cpp
src/permissionutilsinternal.cpp src/permissionutilsinternal.cpp
src/plugin.cpp src/plugin.cpp
src/rolesrenamingmodel.cpp
src/rxvalidator.cpp src/rxvalidator.cpp
src/statussyntaxhighlighter.cpp src/statussyntaxhighlighter.cpp
src/statuswindow.cpp src/statuswindow.cpp
@ -144,7 +146,7 @@ target_link_libraries(StatusQ PRIVATE
qzxing qzxing
) )
target_include_directories(StatusQ PRIVATE include) target_include_directories(StatusQ PUBLIC include)
install(TARGETS StatusQ install(TARGETS StatusQ
RUNTIME DESTINATION StatusQ RUNTIME DESTINATION StatusQ

View File

@ -0,0 +1,44 @@
#pragma once
#include <QIdentityProxyModel>
#include <QQmlListProperty>
class RoleRename : public QObject {
Q_OBJECT
Q_PROPERTY(QString from READ from WRITE setFrom NOTIFY fromChanged)
Q_PROPERTY(QString to READ to WRITE setTo NOTIFY toChanged)
public:
explicit RoleRename(QObject* parent = nullptr);
void setFrom(const QString& from);
const QString& from() const;
void setTo(const QString& to);
const QString& to() const;
signals:
void fromChanged();
void toChanged();
private:
QString m_from;
QString m_to;
};
class RolesRenamingModel : public QIdentityProxyModel
{
Q_OBJECT
Q_PROPERTY(QQmlListProperty<RoleRename> mapping READ mapping CONSTANT)
public:
explicit RolesRenamingModel(QObject* parent = nullptr);
QQmlListProperty<RoleRename> mapping();
QHash<int, QByteArray> roleNames() const override;
private:
mutable bool m_rolesFetched = false;
QList<RoleRename*> m_mapping;
};

View File

@ -6,6 +6,7 @@
#include "StatusQ/QClipboardProxy.h" #include "StatusQ/QClipboardProxy.h"
#include "StatusQ/modelutilsinternal.h" #include "StatusQ/modelutilsinternal.h"
#include "StatusQ/permissionutilsinternal.h" #include "StatusQ/permissionutilsinternal.h"
#include "StatusQ/rolesrenamingmodel.h"
#include "StatusQ/rxvalidator.h" #include "StatusQ/rxvalidator.h"
#include "StatusQ/statussyntaxhighlighter.h" #include "StatusQ/statussyntaxhighlighter.h"
#include "StatusQ/statuswindow.h" #include "StatusQ/statuswindow.h"
@ -24,6 +25,9 @@ public:
qmlRegisterType<StatusSyntaxHighlighter>("StatusQ", 0, 1, "StatusSyntaxHighlighter"); qmlRegisterType<StatusSyntaxHighlighter>("StatusQ", 0, 1, "StatusSyntaxHighlighter");
qmlRegisterType<RXValidator>("StatusQ", 0, 1, "RXValidator"); qmlRegisterType<RXValidator>("StatusQ", 0, 1, "RXValidator");
qmlRegisterType<RolesRenamingModel>("StatusQ", 0, 1, "RolesRenamingModel");
qmlRegisterType<RoleRename>("StatusQ", 0, 1, "RoleRename");
qmlRegisterSingletonType<QClipboardProxy>("StatusQ", 0, 1, "QClipboardProxy", &QClipboardProxy::qmlInstance); qmlRegisterSingletonType<QClipboardProxy>("StatusQ", 0, 1, "QClipboardProxy", &QClipboardProxy::qmlInstance);
qmlRegisterSingletonType<ModelUtilsInternal>( qmlRegisterSingletonType<ModelUtilsInternal>(

View File

@ -0,0 +1,117 @@
#include "StatusQ/rolesrenamingmodel.h"
#include <QDebug>
RoleRename::RoleRename(QObject* parent)
: QObject{parent}
{
}
void RoleRename::setFrom(const QString& from)
{
if (m_from == from)
return;
if (!m_from.isEmpty()) {
qWarning() << "RoleRename: property \"from\" is inteded to be initialized once and not changed!";
return;
}
m_from = from;
emit fromChanged();
}
const QString& RoleRename::from() const
{
return m_from;
}
void RoleRename::setTo(const QString& to)
{
if (m_to == to)
return;
if (!m_to.isEmpty()) {
qWarning() << "RoleRename: property \"to\" is inteded to be initialized once and not changed!";
return;
}
m_to = to;
emit toChanged();
}
const QString& RoleRename::to() const
{
return m_to;
}
RolesRenamingModel::RolesRenamingModel(QObject* parent)
: QIdentityProxyModel{parent}
{
}
QQmlListProperty<RoleRename> RolesRenamingModel::mapping()
{
QQmlListProperty<RoleRename> list(this, &m_mapping);
list.replace = nullptr;
list.clear = nullptr;
list.removeLast = nullptr;
list.append = [](auto listProperty, auto element) {
RolesRenamingModel* model = qobject_cast<RolesRenamingModel*>(
listProperty->object);
if (model->m_rolesFetched) {
qWarning() << "RolesRenamingModel: role names mapping cannot be modified after fetching role names!";
return;
}
model->m_mapping.append(element);
};
return list;
}
QHash<int, QByteArray> RolesRenamingModel::roleNames() const
{
QHash<int, QByteArray> roles = sourceModel()
? sourceModel()->roleNames()
: QHash<int, QByteArray>{};
if (roles.empty())
return roles;
QHash<QString, RoleRename*> renameMap;
for (const auto rename : m_mapping)
renameMap.insert(rename->from(), rename);
QHash<int, QByteArray> remapped;
remapped.reserve(roles.size());
QSet<QByteArray> roleNamesSet;
roleNamesSet.reserve(roles.size());
for (auto i = roles.cbegin(), end = roles.cend(); i != end; ++i) {
RoleRename* rename = renameMap.take(i.value());
QByteArray roleName = rename ? rename->to().toUtf8() : i.value();
remapped.insert(i.key(), roleName);
roleNamesSet.insert(roleName);
}
if (roles.size() != roleNamesSet.size()) {
qWarning() << "RolesRenamingModel: model cannot contain duplicated role names!";
return {};
}
if (renameMap.size()) {
qWarning().nospace()
<< "RolesRenamingModel: specified source roles not found: "
<< renameMap.keys() << "!";
}
m_rolesFetched = true;
return remapped;
}

View File

@ -16,7 +16,9 @@ add_definitions(-DQUICK_TEST_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}")
add_executable(${PROJECT_NAME} main.cpp) add_executable(${PROJECT_NAME} main.cpp)
add_test(NAME ${PROJECT_NAME} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMMAND ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME} -input "${CMAKE_CURRENT_SOURCE_DIR}") add_test(NAME ${PROJECT_NAME} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
COMMAND ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME} -input "${CMAKE_CURRENT_SOURCE_DIR}")
add_custom_target("Run_${PROJECT_NAME}" COMMAND ${CMAKE_CTEST_COMMAND} --test-dir "${CMAKE_CURRENT_BINARY_DIR}") add_custom_target("Run_${PROJECT_NAME}" COMMAND ${CMAKE_CTEST_COMMAND} --test-dir "${CMAKE_CURRENT_BINARY_DIR}")
add_dependencies("Run_${PROJECT_NAME}" ${PROJECT_NAME}) add_dependencies("Run_${PROJECT_NAME}" ${PROJECT_NAME})
@ -38,3 +40,7 @@ target_compile_definitions(${PROJECT_NAME} PRIVATE
STATUSQ_MODULE_PATH="${STATUSQ_MODULE_PATH}" STATUSQ_MODULE_PATH="${STATUSQ_MODULE_PATH}"
STATUSQ_MODULE_IMPORT_PATH="${STATUSQ_MODULE_IMPORT_PATH}" STATUSQ_MODULE_IMPORT_PATH="${STATUSQ_MODULE_IMPORT_PATH}"
) )
add_executable(RolesRenamingModelTest tst_RolesRenamingModel.cpp)
target_link_libraries(RolesRenamingModelTest PRIVATE Qt5::Qml Qt5::Test StatusQ)
add_test(RolesRenamingModelTest COMMAND RolesRenamingModelTest)

View File

@ -0,0 +1,212 @@
#include <QSignalSpy>
#include <QTest>
#include <memory>
#include <StatusQ/rolesrenamingmodel.h>
namespace {
class TestSourceModel : public QAbstractListModel {
public:
explicit TestSourceModel(QList<QString> roles)
: m_roles(std::move(roles))
{
}
QVariant data(const QModelIndex& index, int role) const override
{
if(!index.isValid() || index.row() >= capacity)
return {};
return 42;
}
int rowCount(const QModelIndex& parent) const override
{
return capacity;
}
QHash<int, QByteArray> roleNames() const override
{
QHash<int, QByteArray> roles;
roles.remove(m_roles.size());
for (auto i = 0; i < m_roles.size(); i++)
roles.insert(i, m_roles.at(i).toUtf8());
return roles;
}
private:
static constexpr auto capacity = 5;
QList<QString> m_roles;
};
}
class TestRolesRenamingModel: public QObject
{
Q_OBJECT
private slots:
void initializationTest()
{
RolesRenamingModel model;
QQmlListProperty<RoleRename> mapping = model.mapping();
RoleRename rename;
rename.setFrom("someIdFrom");
rename.setTo("someIdTo");
mapping.append(&mapping, &rename);
QTest::ignoreMessage(QtWarningMsg, "RolesRenamingModel: specified source roles not found: (\"someIdFrom\")!");
QCOMPARE(model.roleNames(), {});
}
void remappingTest()
{
TestSourceModel sourceModel({"id", "name", "color"});
RolesRenamingModel model;
QQmlListProperty<RoleRename> mapping = model.mapping();
RoleRename rename_1;
rename_1.setFrom("id");
rename_1.setTo("tokenId");
mapping.append(&mapping, &rename_1);
RoleRename rename_2;
rename_2.setFrom("name");
rename_2.setTo("tokenName");
mapping.append(&mapping, &rename_2);
model.setSourceModel(&sourceModel);
QHash<int, QByteArray> expectedRoles = {{0, "tokenId"}, {1, "tokenName"}, {2, "color"}};
QCOMPARE(model.roleNames(), expectedRoles);
}
void addMappingAfterFetchingRoleNamesTest()
{
TestSourceModel sourceModel({"id", "name", "color"});
RolesRenamingModel model;
QQmlListProperty<RoleRename> mapping = model.mapping();
RoleRename rename_1;
rename_1.setFrom("id");
rename_1.setTo("tokenId");
mapping.append(&mapping, &rename_1);
model.setSourceModel(&sourceModel);
QHash<int, QByteArray> expectedRoles = {{0, "tokenId"}, {1, "name"}, {2, "color"}};
QCOMPARE(model.roleNames(), expectedRoles);
RoleRename rename_2;
rename_2.setFrom("name");
rename_2.setTo("tokenName");
QTest::ignoreMessage(QtWarningMsg, "RolesRenamingModel: role names mapping cannot be modified after fetching role names!");
mapping.append(&mapping, &rename_2);
QCOMPARE(model.roleNames(), expectedRoles);
}
void duplicatedNamesTest()
{
TestSourceModel sourceModel({"id", "name", "color"});
RolesRenamingModel model;
QQmlListProperty<RoleRename> mapping = model.mapping();
RoleRename rename_1;
rename_1.setFrom("id");
rename_1.setTo("name");
mapping.append(&mapping, &rename_1);
model.setSourceModel(&sourceModel);
QTest::ignoreMessage(QtWarningMsg, "RolesRenamingModel: model cannot contain duplicated role names!");
QCOMPARE(model.roleNames(), {});
}
void resettingFromToPropertiesTest()
{
RoleRename rename;
rename.setFrom("id");
QCOMPARE(rename.from(), "id");
QCOMPARE(rename.to(), "");
QTest::ignoreMessage(QtWarningMsg,
"RoleRename: property \"from\" is inteded to be initialized once and not changed!");
rename.setFrom("id2");
QCOMPARE(rename.from(), "id");
QCOMPARE(rename.to(), "");
rename.setTo("myId");
QCOMPARE(rename.from(), "id");
QCOMPARE(rename.to(), "myId");
QTest::ignoreMessage(QtWarningMsg, "RoleRename: property \"to\" is inteded to be initialized once and not changed!");
rename.setTo("myId2");
QCOMPARE(rename.from(), "id");
QCOMPARE(rename.to(), "myId");
}
void sourceModelDeletedTest()
{
auto sourceModel = std::make_unique<TestSourceModel>(
QList<QString>{"id", "name", "color"});
RolesRenamingModel model;
QQmlListProperty<RoleRename> mapping = model.mapping();
RoleRename rename_1;
rename_1.setFrom("id");
rename_1.setTo("tokenId");
mapping.append(&mapping, &rename_1);
RoleRename rename_2;
rename_2.setFrom("name");
rename_2.setTo("tokenName");
mapping.append(&mapping, &rename_2);
model.setSourceModel(sourceModel.get());
QHash<int, QByteArray> expectedRoles = {
{0, "tokenId"}, {1, "tokenName"}, {2, "color"}
};
QCOMPARE(model.roleNames(), expectedRoles);
QCOMPARE(model.rowCount(), 5);
QCOMPARE(model.data(model.index(0, 0), 0), 42);
QCOMPARE(model.data(model.index(0, 0), 1), 42);
QCOMPARE(model.data(model.index(5, 0), 0), {});
QCOMPARE(model.data(model.index(5, 0), 1), {});
QSignalSpy destroyedSpy(sourceModel.get(), &QObject::destroyed);
sourceModel.reset();
QCOMPARE(destroyedSpy.size(), 1);
QCOMPARE(model.roleNames(), {});
QCOMPARE(model.rowCount(), 0);
QCOMPARE(model.roleNames(), {});
QCOMPARE(model.data(model.index(0, 0), 0), {});
QCOMPARE(model.data(model.index(0, 0), 1), {});
QCOMPARE(model.data(model.index(5, 0), 0), {});
QCOMPARE(model.data(model.index(5, 0), 1), {});
}
};
QTEST_MAIN(TestRolesRenamingModel)
#include "tst_RolesRenamingModel.moc"