feat(StatusQ): QML-oriented proxy model concatenating source models vertically
Closes: #12682
This commit is contained in:
parent
a915f48fd9
commit
e2fa702ec4
|
@ -0,0 +1,270 @@
|
|||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import StatusQ 0.1
|
||||
|
||||
import Qt.labs.qmlmodels 1.0
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
ListModel {
|
||||
id: firstModel
|
||||
|
||||
ListElement {
|
||||
name: "entry 1 (1)"
|
||||
}
|
||||
ListElement {
|
||||
name: "entry 2 (1)"
|
||||
}
|
||||
ListElement {
|
||||
name: "entry 3 (1)"
|
||||
}
|
||||
ListElement {
|
||||
name: "entry 4 (1)"
|
||||
}
|
||||
ListElement {
|
||||
name: "entry 5 (1)"
|
||||
}
|
||||
ListElement {
|
||||
name: "entry 6 (1)"
|
||||
}
|
||||
ListElement {
|
||||
name: "entry 7 (1)"
|
||||
}
|
||||
ListElement {
|
||||
name: "entry 8 (1)"
|
||||
}
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: secondModel
|
||||
|
||||
ListElement {
|
||||
name: "entry 1 (2)"
|
||||
key: 1
|
||||
}
|
||||
ListElement {
|
||||
key: 2
|
||||
name: "entry 2 (2)"
|
||||
}
|
||||
ListElement {
|
||||
key: 3
|
||||
name: "entry 3 (2)"
|
||||
}
|
||||
}
|
||||
|
||||
ConcatModel {
|
||||
id: concatModel
|
||||
|
||||
sources: [
|
||||
SourceModel {
|
||||
model: firstModel
|
||||
markerRoleValue: "first_model"
|
||||
},
|
||||
SourceModel {
|
||||
model: secondModel
|
||||
markerRoleValue: "second_model"
|
||||
}
|
||||
]
|
||||
|
||||
markerRoleName: "which_model"
|
||||
expectedRoles: ["key", "name"]
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
ColumnLayout {
|
||||
|
||||
Layout.preferredWidth: parent.width / 3
|
||||
|
||||
ListView {
|
||||
id: firstModelListView
|
||||
|
||||
spacing: 15
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
model: firstModel
|
||||
|
||||
delegate: RowLayout {
|
||||
width: ListView.view.width
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
|
||||
font.bold: true
|
||||
text: model.name
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
|
||||
onClicked: {
|
||||
firstModel.setProperty(model.index, "name",
|
||||
firstModel.get(model.index).name + "_")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "append"
|
||||
|
||||
onClicked: {
|
||||
firstModel.append({name: "appended entry (1)"})
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "insert at 1"
|
||||
|
||||
onClicked: {
|
||||
firstModel.insert(1, {name: "inserted entry (1)"})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
|
||||
Layout.preferredWidth: parent.width / 3
|
||||
Layout.fillHeight: true
|
||||
|
||||
ListView {
|
||||
id: secondModelListView
|
||||
|
||||
spacing: 15
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
model: secondModel
|
||||
|
||||
delegate: RowLayout {
|
||||
width: ListView.view.width
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
|
||||
font.bold: true
|
||||
text: model.name
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
|
||||
onClicked: secondModel.setProperty(
|
||||
model.index, "name",
|
||||
secondModel.get(model.index).name + "_")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "append"
|
||||
|
||||
onClicked: {
|
||||
secondModel.append({name: "appended entry (1)", key: 34})
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "insert at 1"
|
||||
|
||||
onClicked: {
|
||||
secondModel.insert(1, {name: "inserted entry (1)", key: 999})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
|
||||
Layout.preferredWidth: parent.width / 3
|
||||
Layout.fillHeight: true
|
||||
|
||||
ListView {
|
||||
id: concatListView
|
||||
|
||||
spacing: 15
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
model: concatModel
|
||||
|
||||
ScrollBar.vertical: ScrollBar {}
|
||||
|
||||
section.property: "which_model"
|
||||
section.delegate: ColumnLayout {
|
||||
Label {
|
||||
height: implicitHeight * 2
|
||||
text: section + " inset"
|
||||
font.pixelSize: 20
|
||||
font.bold: true
|
||||
font.underline: true
|
||||
|
||||
color: "darkred"
|
||||
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
CheckBox {
|
||||
text: "some switch here"
|
||||
}
|
||||
CheckBox {
|
||||
text: "some other switch here"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delegate: DelegateChooser {
|
||||
id: chooser
|
||||
role: "which_model"
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "first_model"
|
||||
|
||||
RowLayout {
|
||||
width: ListView.view.width
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
|
||||
font.bold: true
|
||||
text: model.name + ", " + model.which_model
|
||||
color: "darkgreen"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "second_model"
|
||||
|
||||
RowLayout {
|
||||
width: ListView.view.width
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
|
||||
font.bold: true
|
||||
text: model.name + ", " + model.which_model
|
||||
+ " (" + model.key + ")"
|
||||
color: "darkblue"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
text: concatListView.count
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// category: Models
|
|
@ -89,6 +89,7 @@ endif()
|
|||
add_library(StatusQ SHARED
|
||||
${STATUSQ_QRC_COMPILED}
|
||||
include/StatusQ/QClipboardProxy.h
|
||||
include/StatusQ/concatmodel.h
|
||||
include/StatusQ/leftjoinmodel.h
|
||||
include/StatusQ/modelutilsinternal.h
|
||||
include/StatusQ/permissionutilsinternal.h
|
||||
|
@ -102,6 +103,7 @@ add_library(StatusQ SHARED
|
|||
include/StatusQ/singleroleaggregator.h
|
||||
include/StatusQ/sumaggregator.h
|
||||
src/QClipboardProxy.cpp
|
||||
src/concatmodel.cpp
|
||||
src/leftjoinmodel.cpp
|
||||
src/modelutilsinternal.cpp
|
||||
src/permissionutilsinternal.cpp
|
||||
|
|
|
@ -0,0 +1,115 @@
|
|||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QQmlListProperty>
|
||||
#include <QQmlParserStatus>
|
||||
|
||||
class SourceModel : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(QAbstractItemModel* model READ model
|
||||
WRITE setModel NOTIFY modelChanged)
|
||||
|
||||
Q_PROPERTY(QString markerRoleValue READ markerRoleValue
|
||||
WRITE setMarkerRoleValue NOTIFY markerRoleValueChanged)
|
||||
|
||||
public:
|
||||
explicit SourceModel(QObject* parent = nullptr);
|
||||
|
||||
void setModel(QAbstractItemModel* model);
|
||||
QAbstractItemModel* model() const;
|
||||
|
||||
void setMarkerRoleValue(const QString& markerRoleValue);
|
||||
const QString& markerRoleValue() const;
|
||||
|
||||
signals:
|
||||
void modelAboutToBeChanged();
|
||||
void modelChanged(bool deleted);
|
||||
void markerRoleValueChanged();
|
||||
|
||||
private:
|
||||
void onModelDestroyed();
|
||||
|
||||
QAbstractItemModel* m_model = nullptr;
|
||||
QString m_markerRoleValue;
|
||||
};
|
||||
|
||||
class ConcatModel : public QAbstractListModel, public QQmlParserStatus
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_INTERFACES(QQmlParserStatus)
|
||||
|
||||
Q_PROPERTY(QQmlListProperty<SourceModel> sources READ sources CONSTANT)
|
||||
|
||||
Q_PROPERTY(QString markerRoleName READ markerRoleName
|
||||
WRITE setMarkerRoleName NOTIFY markerRoleNameChanged)
|
||||
|
||||
Q_PROPERTY(QStringList expectedRoles READ expectedRoles
|
||||
WRITE setExpectedRoles NOTIFY expectedRolesChanged)
|
||||
|
||||
public:
|
||||
explicit ConcatModel(QObject *parent = nullptr);
|
||||
|
||||
QQmlListProperty<SourceModel> sources();
|
||||
|
||||
void setMarkerRoleName(const QString& markerRoleName);
|
||||
const QString& markerRoleName() const;
|
||||
|
||||
void setExpectedRoles(const QStringList& expectedRoles);
|
||||
const QStringList& expectedRoles() const;
|
||||
|
||||
Q_INVOKABLE int sourceModelRow(int row) const;
|
||||
Q_INVOKABLE QAbstractItemModel* sourceModel(int row) const;
|
||||
Q_INVOKABLE int fromSourceRow(const QAbstractItemModel* model, int row) const;
|
||||
|
||||
// QAbstractItemModel interface
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex &index, int role) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
// QQmlParserStatus interface
|
||||
void classBegin() override;
|
||||
void componentComplete() override;
|
||||
|
||||
signals:
|
||||
void markerRoleNameChanged();
|
||||
void expectedRolesChanged();
|
||||
|
||||
private:
|
||||
static constexpr auto s_defaultMarkerRoleName = "whichModel";
|
||||
|
||||
std::pair<SourceModel*, int> sourceForIndex(int index) const;
|
||||
|
||||
void initRoles();
|
||||
|
||||
void initRolesMapping(int index, QAbstractItemModel* model);
|
||||
void initRolesMapping();
|
||||
|
||||
void initAllModelsSlots();
|
||||
void connectModelSlots(int index, QAbstractItemModel* model);
|
||||
void disconnectModelSlots(QAbstractItemModel* model);
|
||||
|
||||
int rowCountInternal() const;
|
||||
int countPrefix(int sourceIndex) const;
|
||||
void fetchRowCounts();
|
||||
|
||||
QVector<int> mapFromSourceRoles(int sourceIndex,
|
||||
const QVector<int>& sourceRoles) const;
|
||||
|
||||
QList<SourceModel*> m_sources;
|
||||
QStringList m_expectedRoles;
|
||||
|
||||
QString m_markerRoleName = s_defaultMarkerRoleName;
|
||||
int m_markerRole = 0;
|
||||
|
||||
bool m_initialized = false;
|
||||
|
||||
QHash<int, QByteArray> m_roleNames;
|
||||
std::unordered_map<QByteArray, int> m_nameRoles;
|
||||
|
||||
std::vector<std::unordered_map<int, int>> m_rolesMappingFromSource;
|
||||
std::vector<std::unordered_map<int, int>> m_rolesMappingToSource;
|
||||
std::vector<bool> m_rolesMappingInitializationFlags;
|
||||
std::vector<int> m_rowCounts;
|
||||
};
|
|
@ -0,0 +1,681 @@
|
|||
#include "StatusQ/concatmodel.h"
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
/*!
|
||||
\qmltype SourceModel
|
||||
\instantiates SourceModel
|
||||
\inqmlmodule StatusQ
|
||||
\inherits QtObject
|
||||
\brief Wraps arbitrary QAbstractItemModel to be concatenated with other
|
||||
models within a \l {ConcatModel}.
|
||||
|
||||
It allows assigning a value of a special marker role in ConcatModel for the
|
||||
given model.
|
||||
*/
|
||||
|
||||
/*!
|
||||
\qmltype ConcatModel
|
||||
\instantiates ConcatModel
|
||||
\inqmlmodule StatusQ
|
||||
\inherits QAbstractListModel
|
||||
\brief Proxy model concatenating vertically multiple source models.
|
||||
|
||||
It allows concatenating multiple source models, with same roles, partially
|
||||
different roles or even totally different roles. The model performs necessary
|
||||
roles mapping internally.
|
||||
|
||||
The proxy is similar to \l {QConcatenateTablesProxyModel} but QML-ready,
|
||||
performing all necessary role names remapping.
|
||||
|
||||
Roles are established when the first item appears in one of the sources.
|
||||
Expected roles can be also declared up-front using expectedRoles property
|
||||
(because on first insertion some roles may be not yet available via
|
||||
roleNames() on other models).
|
||||
|
||||
Additionally the model introduces an extra role with a name configurable via
|
||||
\l {CocatModel::markerRoleName}. Value of this role may be set separately
|
||||
for each source model in \l {SourceModel} wrapper. This allows to easily
|
||||
create inserts between models using \l {ListView}'s sections mechanism.
|
||||
|
||||
\qml
|
||||
ListModel {
|
||||
id: firstModel
|
||||
|
||||
ListElement { name: "entry 1_1" }
|
||||
ListElement { name: "entry 1_2" }
|
||||
ListElement { name: "entry 1_3" }
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: secondModel
|
||||
|
||||
ListElement {
|
||||
name: "entry 1_2"
|
||||
key: 1
|
||||
}
|
||||
ListElement {
|
||||
key: 2
|
||||
name: "entry 2 _2"
|
||||
}
|
||||
}
|
||||
|
||||
ConcatModel {
|
||||
id: concatModel
|
||||
|
||||
sources: [
|
||||
SourceModel {
|
||||
model: firstModel
|
||||
markerRoleValue: "first_model"
|
||||
},
|
||||
SourceModel {
|
||||
model: secondModel
|
||||
markerRoleValue: "second_model"
|
||||
}
|
||||
]
|
||||
|
||||
markerRoleName: "which_model"
|
||||
expectedRoles: ["key", "name"]
|
||||
}
|
||||
\endqml
|
||||
*/
|
||||
|
||||
SourceModel::SourceModel(QObject* parent)
|
||||
: QObject{parent}
|
||||
{
|
||||
}
|
||||
|
||||
void SourceModel::setModel(QAbstractItemModel* model)
|
||||
{
|
||||
if (m_model == model)
|
||||
return;
|
||||
|
||||
if (model)
|
||||
connect(model, &QObject::destroyed, this, &SourceModel::onModelDestroyed);
|
||||
|
||||
if (m_model)
|
||||
disconnect(m_model, &QObject::destroyed, this, &SourceModel::onModelDestroyed);
|
||||
|
||||
emit modelAboutToBeChanged();
|
||||
m_model = model;
|
||||
emit modelChanged(false);
|
||||
}
|
||||
|
||||
|
||||
/*!
|
||||
\qmlproperty any StatusQ::SourceModel::model
|
||||
|
||||
The model that will be concatenated with other models within ConcatModel.
|
||||
*/
|
||||
QAbstractItemModel* SourceModel::model() const
|
||||
{
|
||||
return m_model;
|
||||
}
|
||||
|
||||
void SourceModel::setMarkerRoleValue(const QString& markerRoleValue)
|
||||
{
|
||||
if (m_markerRoleValue == markerRoleValue)
|
||||
return;
|
||||
|
||||
m_markerRoleValue = markerRoleValue;
|
||||
emit markerRoleValueChanged();
|
||||
}
|
||||
|
||||
/*!
|
||||
\qmlproperty string StatusQ::SourceModel::markerRoleValue
|
||||
|
||||
The value that will be exposed from the ConcatModel through the role named
|
||||
according to \l {ConcatModel::markerRoleName} for the entries coming from
|
||||
the model defined in SourceModel::model.
|
||||
*/
|
||||
const QString& SourceModel::markerRoleValue() const
|
||||
{
|
||||
return m_markerRoleValue;
|
||||
}
|
||||
|
||||
void SourceModel::onModelDestroyed()
|
||||
{
|
||||
m_model = nullptr;
|
||||
emit modelChanged(true);
|
||||
}
|
||||
|
||||
|
||||
ConcatModel::ConcatModel(QObject* parent)
|
||||
: QAbstractListModel{parent}
|
||||
{
|
||||
}
|
||||
|
||||
/*!
|
||||
\qmlproperty list<SourceModel> StatusQ::ConcatModel::sources
|
||||
|
||||
This property holds the list of \l {SourceModel} wrappers. Every wrapper
|
||||
holds model which is intended to be concatenated with others within the
|
||||
proxy.
|
||||
*/
|
||||
QQmlListProperty<SourceModel> ConcatModel::sources()
|
||||
{
|
||||
QQmlListProperty<SourceModel> listProperty(this, &m_sources);
|
||||
|
||||
listProperty.replace = nullptr;
|
||||
listProperty.clear = nullptr;
|
||||
listProperty.removeLast = nullptr;
|
||||
|
||||
listProperty.append = [](auto listProperty, auto element) {
|
||||
ConcatModel* model = qobject_cast<ConcatModel*>(listProperty->object);
|
||||
|
||||
if (model->m_initialized) {
|
||||
qWarning() << "Adding sources dynamically is not supported.";
|
||||
return;
|
||||
}
|
||||
|
||||
model->m_sources.append(element);
|
||||
};
|
||||
|
||||
return listProperty;
|
||||
}
|
||||
|
||||
void ConcatModel::setMarkerRoleName(const QString& markerRoleName)
|
||||
{
|
||||
if (m_markerRoleName == markerRoleName)
|
||||
return;
|
||||
|
||||
if (m_markerRoleName != s_defaultMarkerRoleName || m_initialized) {
|
||||
qWarning() << "Property \"markerRoleName\" is intended to be "
|
||||
"initialized once before roles initialization and not "
|
||||
"modified later.";
|
||||
return;
|
||||
}
|
||||
|
||||
m_markerRoleName = markerRoleName;
|
||||
emit markerRoleNameChanged();
|
||||
}
|
||||
|
||||
/*!
|
||||
\qmlproperty string StatusQ::ConcatModel::markerRoleName
|
||||
|
||||
This property contains the name of an extra role allowing to distinguish
|
||||
source models from the delegate level.
|
||||
*/
|
||||
const QString& ConcatModel::markerRoleName() const
|
||||
{
|
||||
return m_markerRoleName;
|
||||
}
|
||||
|
||||
void ConcatModel::setExpectedRoles(const QStringList& expectedRoles)
|
||||
{
|
||||
if (m_expectedRoles == expectedRoles)
|
||||
return;
|
||||
|
||||
if (!m_expectedRoles.isEmpty()) {
|
||||
qWarning() << "Property \"expectedRoles\" is intended "
|
||||
"to be initialized once and not changed!";
|
||||
return;
|
||||
}
|
||||
|
||||
m_expectedRoles = expectedRoles;
|
||||
emit expectedRolesChanged();
|
||||
}
|
||||
|
||||
/*!
|
||||
\qmlproperty list<string> StatusQ::ConcatModel::expectedRoles
|
||||
|
||||
This property allows to predefine a set of roles exposed by ConcatModel.
|
||||
This is useful when roles are not initially defined for some source models.
|
||||
For example, for ListModel, roles are not defined as long as the model is
|
||||
empty.
|
||||
*/
|
||||
const QStringList& ConcatModel::expectedRoles() const
|
||||
{
|
||||
return m_expectedRoles;
|
||||
}
|
||||
|
||||
/*!
|
||||
\qmlmethod int StatusQ::ConcatModel::sourceModelRow(row)
|
||||
|
||||
Returns the row index inside the source model for a given row of the proxy.
|
||||
*/
|
||||
int ConcatModel::sourceModelRow(int row) const
|
||||
{
|
||||
auto source = sourceForIndex(row);
|
||||
return source.first != nullptr ? source.second : -1;
|
||||
}
|
||||
|
||||
/*!
|
||||
\qmlmethod QAbstractItemModel* StatusQ::ConcatModel::sourceModel(row)
|
||||
|
||||
Returns the source model for a given row of the proxy.
|
||||
*/
|
||||
QAbstractItemModel* ConcatModel::sourceModel(int row) const
|
||||
{
|
||||
auto source = sourceForIndex(row);
|
||||
return source.first != nullptr ? source.first->model() : nullptr;
|
||||
}
|
||||
|
||||
/*!
|
||||
\qmlmethod int StatusQ::ConcatModel::fromSourceRow(model, row)
|
||||
|
||||
Returns the row number of the ConcatModel for a given source model and
|
||||
source model's row index.
|
||||
*/
|
||||
int ConcatModel::fromSourceRow(const QAbstractItemModel* model, int row) const
|
||||
{
|
||||
if (model == nullptr || row < 0 || model->rowCount() <= row)
|
||||
return -1;
|
||||
|
||||
auto it = std::find_if(m_sources.cbegin(), m_sources.cend(),
|
||||
[model](auto source) {
|
||||
return source->model() == model;
|
||||
});
|
||||
|
||||
if (it == m_sources.cend())
|
||||
return -1;
|
||||
|
||||
return countPrefix(it - m_sources.begin()) + row;
|
||||
}
|
||||
|
||||
int ConcatModel::rowCount(const QModelIndex& parent) const
|
||||
{
|
||||
if (!m_initialized)
|
||||
return 0;
|
||||
|
||||
return rowCountInternal();
|
||||
}
|
||||
|
||||
QVariant ConcatModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!checkIndex(index, CheckIndexOption::IndexIsValid))
|
||||
return {};
|
||||
|
||||
auto row = index.row();
|
||||
int rowCount = 0;
|
||||
|
||||
for (int i = 0; i < m_sources.size(); i++) {
|
||||
const int subRowCount = m_rowCounts[i];
|
||||
|
||||
if (rowCount + subRowCount > row) {
|
||||
auto source = m_sources[i];
|
||||
|
||||
if (role == m_markerRole)
|
||||
return source->markerRoleValue();
|
||||
|
||||
auto model = source->model();
|
||||
|
||||
if (model == nullptr)
|
||||
return {};
|
||||
|
||||
auto& mapping = m_rolesMappingToSource.at(i);
|
||||
auto it = mapping.find(role);
|
||||
|
||||
if (it == mapping.end())
|
||||
return {};
|
||||
|
||||
return model->data(model->index(row - rowCount, 0), it->second);
|
||||
}
|
||||
|
||||
rowCount += subRowCount;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> ConcatModel::roleNames() const
|
||||
{
|
||||
return m_roleNames;
|
||||
}
|
||||
|
||||
void ConcatModel::classBegin()
|
||||
{
|
||||
}
|
||||
|
||||
void ConcatModel::componentComplete()
|
||||
{
|
||||
if (m_initialized)
|
||||
return;
|
||||
|
||||
for (auto i = 0; i < m_sources.size(); i++) {
|
||||
SourceModel* source = m_sources[i];
|
||||
|
||||
connect(source, &SourceModel::modelAboutToBeChanged, this,
|
||||
[this, i, source]()
|
||||
{
|
||||
auto model = source->model();
|
||||
|
||||
if (model != nullptr)
|
||||
disconnectModelSlots(model);
|
||||
|
||||
if (auto count = m_rowCounts[i]) {
|
||||
auto prefix = countPrefix(i);
|
||||
beginRemoveRows({}, prefix, prefix + count - 1);
|
||||
}
|
||||
});
|
||||
|
||||
connect(source, &SourceModel::modelChanged, this,
|
||||
[this, source, i](bool deleted)
|
||||
{
|
||||
auto previousRowCount = m_rowCounts[i];
|
||||
|
||||
if (deleted) {
|
||||
auto prefix = countPrefix(i);
|
||||
beginRemoveRows({}, prefix, prefix + previousRowCount - 1);
|
||||
}
|
||||
|
||||
if (previousRowCount) {
|
||||
m_rowCounts[i] = 0;
|
||||
endRemoveRows();
|
||||
}
|
||||
|
||||
auto model = source->model();
|
||||
|
||||
if (model == nullptr)
|
||||
return;
|
||||
|
||||
auto rowCount = model->rowCount();
|
||||
|
||||
if (rowCount > 0) {
|
||||
auto prefix = countPrefix(i);
|
||||
|
||||
beginInsertRows({}, prefix, prefix + rowCount - 1);
|
||||
|
||||
m_rowCounts[i] = rowCount;
|
||||
|
||||
if (!m_initialized) {
|
||||
initRoles();
|
||||
initRolesMapping();
|
||||
m_initialized = true;
|
||||
} else {
|
||||
initRolesMapping(i, model);
|
||||
}
|
||||
|
||||
endInsertRows();
|
||||
}
|
||||
|
||||
connectModelSlots(i, model);
|
||||
});
|
||||
|
||||
connect(source, &SourceModel::markerRoleValueChanged, this,
|
||||
[this, source, i]
|
||||
{
|
||||
auto count = this->m_rowCounts[i];
|
||||
|
||||
if (count == 0)
|
||||
return;
|
||||
|
||||
auto prefix = this->countPrefix(i);
|
||||
|
||||
emit this->dataChanged(this->index(prefix),
|
||||
this->index(prefix + count - 1),
|
||||
{ this->m_markerRole });
|
||||
});
|
||||
}
|
||||
|
||||
initAllModelsSlots();
|
||||
fetchRowCounts();
|
||||
|
||||
auto count = rowCountInternal();
|
||||
|
||||
if (count == 0)
|
||||
return;
|
||||
|
||||
beginInsertRows({}, 0, count - 1);
|
||||
|
||||
initRoles();
|
||||
initRolesMapping();
|
||||
m_initialized = true;
|
||||
|
||||
endInsertRows();
|
||||
}
|
||||
|
||||
std::pair<SourceModel*, int> ConcatModel::sourceForIndex(int index) const
|
||||
{
|
||||
if (index < 0)
|
||||
return {};
|
||||
|
||||
int rowCount = 0;
|
||||
|
||||
for (int i = 0; i < m_sources.size(); i++) {
|
||||
const int subRowCount = m_rowCounts[i];
|
||||
|
||||
if (rowCount + subRowCount > index)
|
||||
return {m_sources[i], index - rowCount};
|
||||
|
||||
rowCount += subRowCount;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
void ConcatModel::initRoles()
|
||||
{
|
||||
Q_ASSERT(m_roleNames.empty());
|
||||
Q_ASSERT(m_nameRoles.empty());
|
||||
|
||||
m_nameRoles.reserve(m_expectedRoles.size() + 1);
|
||||
|
||||
for (auto& expectedRoleName : qAsConst(m_expectedRoles))
|
||||
m_nameRoles.try_emplace(expectedRoleName.toUtf8(), m_nameRoles.size());
|
||||
|
||||
for (auto sourceModel : qAsConst(m_sources)) {
|
||||
auto model = sourceModel->model();
|
||||
|
||||
if (model == nullptr)
|
||||
continue;
|
||||
|
||||
auto roleNames = model->roleNames();
|
||||
|
||||
for (auto& role : roleNames)
|
||||
m_nameRoles.try_emplace(role, m_nameRoles.size());
|
||||
}
|
||||
|
||||
auto it = m_nameRoles.try_emplace(m_markerRoleName.toUtf8(),
|
||||
m_nameRoles.size()).first;
|
||||
m_markerRole = it->second;
|
||||
|
||||
m_roleNames.reserve(m_nameRoles.size());
|
||||
|
||||
for (auto& [name, role] : m_nameRoles)
|
||||
m_roleNames.insert(role, name);
|
||||
}
|
||||
|
||||
void ConcatModel::initRolesMapping(int index, QAbstractItemModel* model)
|
||||
{
|
||||
Q_ASSERT(model != nullptr);
|
||||
|
||||
auto roleNames = model->roleNames();
|
||||
auto rowCount = model->rowCount();
|
||||
|
||||
std::unordered_map<int, int> fromSource;
|
||||
std::unordered_map<int, int> toSource;
|
||||
|
||||
for (auto i = roleNames.cbegin(), end = roleNames.cend(); i != end; ++i) {
|
||||
auto it = std::as_const(m_nameRoles).find(i.value());
|
||||
|
||||
if (it == m_nameRoles.cend())
|
||||
continue;
|
||||
|
||||
auto globalRole = it->second;
|
||||
|
||||
fromSource.insert({i.key(), globalRole});
|
||||
toSource.insert({globalRole, i.key()});
|
||||
}
|
||||
|
||||
bool initialized = !roleNames.empty() || rowCount > 0;
|
||||
|
||||
m_rolesMappingFromSource[index] = std::move(fromSource);
|
||||
m_rolesMappingToSource[index] = std::move(toSource);
|
||||
m_rolesMappingInitializationFlags[index] = initialized;
|
||||
}
|
||||
|
||||
void ConcatModel::initRolesMapping()
|
||||
{
|
||||
Q_ASSERT(m_rolesMappingFromSource.empty());
|
||||
Q_ASSERT(m_rolesMappingToSource.empty());
|
||||
Q_ASSERT(m_rolesMappingInitializationFlags.empty());
|
||||
|
||||
m_rolesMappingFromSource.resize(m_sources.size());
|
||||
m_rolesMappingToSource.resize(m_sources.size());
|
||||
m_rolesMappingInitializationFlags.resize(m_sources.size(), false);
|
||||
|
||||
for (auto i = 0; i < m_sources.size(); ++i) {
|
||||
auto sourceModelWrapper = m_sources.at(i);
|
||||
auto sourceModel = sourceModelWrapper->model();
|
||||
|
||||
if (sourceModel == nullptr)
|
||||
continue;
|
||||
|
||||
initRolesMapping(i, sourceModel);
|
||||
}
|
||||
}
|
||||
|
||||
void ConcatModel::initAllModelsSlots()
|
||||
{
|
||||
for (auto sourceIndex = 0; sourceIndex < m_sources.size(); ++sourceIndex) {
|
||||
auto sourceModelWrapper = m_sources.at(sourceIndex);
|
||||
auto sourceModel = sourceModelWrapper->model();
|
||||
|
||||
if (sourceModel)
|
||||
connectModelSlots(sourceIndex, sourceModel);
|
||||
}
|
||||
}
|
||||
|
||||
void ConcatModel::connectModelSlots(int index, QAbstractItemModel *model)
|
||||
{
|
||||
connect(model, &QAbstractItemModel::rowsAboutToBeInserted, this,
|
||||
[this, index](const QModelIndex &parent, int first, int last)
|
||||
{
|
||||
auto prefix = this->countPrefix(index);
|
||||
this->beginInsertRows({}, first + prefix, last + prefix);
|
||||
});
|
||||
|
||||
connect(model, &QAbstractItemModel::rowsInserted, this,
|
||||
[this, model, index](const QModelIndex &parent, int first, int last)
|
||||
{
|
||||
m_rowCounts[index] += last - first + 1;
|
||||
|
||||
if (!m_initialized) {
|
||||
initRoles();
|
||||
initRolesMapping();
|
||||
m_initialized = true;
|
||||
} else if (!m_rolesMappingInitializationFlags.at(index)) {
|
||||
initRolesMapping(index, model);
|
||||
}
|
||||
|
||||
this->endInsertRows();
|
||||
});
|
||||
|
||||
connect(model, &QAbstractItemModel::rowsAboutToBeRemoved, this,
|
||||
[this, index](const QModelIndex &parent, int first, int last)
|
||||
{
|
||||
auto prefix = this->countPrefix(index);
|
||||
this->beginRemoveRows({}, first + prefix, last + prefix);
|
||||
});
|
||||
|
||||
connect(model, &QAbstractItemModel::rowsRemoved, this,
|
||||
[this, index](const QModelIndex &parent, int first, int last)
|
||||
{
|
||||
m_rowCounts[index] -= last - first + 1;
|
||||
this->endRemoveRows();
|
||||
});
|
||||
|
||||
connect(model, &QAbstractItemModel::rowsAboutToBeMoved, this,
|
||||
[this, index](
|
||||
const QModelIndex&, int sourceStart, int sourceEnd,
|
||||
const QModelIndex&, int destinationRow)
|
||||
{
|
||||
auto prefix = this->countPrefix(index);
|
||||
this->beginMoveRows({}, sourceStart + prefix, sourceEnd + prefix,
|
||||
{}, destinationRow + prefix);
|
||||
});
|
||||
|
||||
connect(model, &QAbstractItemModel::rowsMoved, this,
|
||||
[this, index]
|
||||
{
|
||||
this->endMoveRows();
|
||||
});
|
||||
|
||||
connect(model, &QAbstractItemModel::layoutAboutToBeChanged, this, [this]
|
||||
{
|
||||
emit this->layoutAboutToBeChanged();
|
||||
});
|
||||
|
||||
connect(model, &QAbstractItemModel::layoutAboutToBeChanged, this, [this]
|
||||
{
|
||||
emit this->layoutChanged();
|
||||
});
|
||||
|
||||
connect(model, &QAbstractItemModel::modelAboutToBeReset, this, [this]
|
||||
{
|
||||
this->beginResetModel();
|
||||
});
|
||||
|
||||
connect(model, &QAbstractItemModel::modelReset, this, [this, model, index]
|
||||
{
|
||||
m_rowCounts[index] = model->rowCount();
|
||||
m_rolesMappingInitializationFlags[index] = false;
|
||||
|
||||
this->endResetModel();
|
||||
});
|
||||
|
||||
connect(model, &QAbstractItemModel::dataChanged, this,
|
||||
[this, index](auto& topLeft, const auto& bottomRight, auto& roles)
|
||||
{
|
||||
auto prefix = this->countPrefix(index);
|
||||
auto rolesMapped = mapFromSourceRoles(index, roles);
|
||||
|
||||
if (rolesMapped.empty())
|
||||
return;
|
||||
|
||||
emit this->dataChanged(this->index(prefix + topLeft.row()),
|
||||
this->index(prefix + bottomRight.row()),
|
||||
rolesMapped);
|
||||
});
|
||||
}
|
||||
|
||||
void ConcatModel::disconnectModelSlots(QAbstractItemModel* model)
|
||||
{
|
||||
Q_ASSERT(model != nullptr);
|
||||
bool disconnected = disconnect(model, nullptr, this, nullptr);
|
||||
Q_UNUSED(disconnected);
|
||||
Q_ASSERT(disconnected);
|
||||
}
|
||||
|
||||
int ConcatModel::rowCountInternal() const
|
||||
{
|
||||
return std::reduce(m_rowCounts.cbegin(), m_rowCounts.cend());
|
||||
}
|
||||
|
||||
int ConcatModel::countPrefix(int sourceIndex) const
|
||||
{
|
||||
Q_ASSERT(sourceIndex >= 0 && sourceIndex < m_sources.size());
|
||||
return std::reduce(m_rowCounts.cbegin(), m_rowCounts.cbegin() + sourceIndex);
|
||||
}
|
||||
|
||||
void ConcatModel::fetchRowCounts()
|
||||
{
|
||||
m_rowCounts.resize(m_sources.size());
|
||||
|
||||
for (auto i = 0; i < m_sources.size(); ++i) {
|
||||
auto sourceModelWrapper = m_sources.at(i);
|
||||
auto sourceModel = sourceModelWrapper->model();
|
||||
|
||||
m_rowCounts[i] = (sourceModel == nullptr) ? 0 : sourceModel->rowCount();
|
||||
}
|
||||
}
|
||||
|
||||
QVector<int> ConcatModel::mapFromSourceRoles(
|
||||
int sourceIndex, const QVector<int>& sourceRoles) const
|
||||
{
|
||||
QVector<int> mapped;
|
||||
mapped.reserve(sourceRoles.size());
|
||||
|
||||
auto& mapping = m_rolesMappingFromSource[sourceIndex];
|
||||
|
||||
for (auto role : sourceRoles) {
|
||||
auto it = mapping.find(role);
|
||||
|
||||
if (it != mapping.end())
|
||||
mapped << it->second;
|
||||
}
|
||||
|
||||
return mapped;
|
||||
}
|
|
@ -4,6 +4,7 @@
|
|||
#include <qqmlsortfilterproxymodeltypes.h>
|
||||
|
||||
#include "StatusQ/QClipboardProxy.h"
|
||||
#include "StatusQ/concatmodel.h"
|
||||
#include "StatusQ/leftjoinmodel.h"
|
||||
#include "StatusQ/modelutilsinternal.h"
|
||||
#include "StatusQ/permissionutilsinternal.h"
|
||||
|
@ -35,6 +36,8 @@ public:
|
|||
qmlRegisterType<ManageTokensController>("StatusQ.Models", 0, 1, "ManageTokensController");
|
||||
qmlRegisterType<ManageTokensModel>("StatusQ.Models", 0, 1, "ManageTokensModel");
|
||||
|
||||
qmlRegisterType<SourceModel>("StatusQ", 0, 1, "SourceModel");
|
||||
qmlRegisterType<ConcatModel>("StatusQ", 0, 1, "ConcatModel");
|
||||
qmlRegisterType<LeftJoinModel>("StatusQ", 0, 1, "LeftJoinModel");
|
||||
qmlRegisterType<SubmodelProxyModel>("StatusQ", 0, 1, "SubmodelProxyModel");
|
||||
qmlRegisterType<RoleRename>("StatusQ", 0, 1, "RoleRename");
|
||||
|
|
|
@ -64,3 +64,7 @@ add_test(NAME SingleRoleAggregatorTest COMMAND SingleRoleAggregatorTest)
|
|||
add_executable(SumAggregatorTest tst_SumAggregator.cpp)
|
||||
target_link_libraries(SumAggregatorTest PRIVATE Qt5::Test StatusQ)
|
||||
add_test(NAME SumAggregatorTest COMMAND SumAggregatorTest)
|
||||
|
||||
add_executable(ConcatModelTest tst_ConcatModel.cpp)
|
||||
target_link_libraries(ConcatModelTest PRIVATE Qt5::Qml Qt5::Test StatusQ)
|
||||
add_test(NAME ConcatModelTest COMMAND ConcatModelTest)
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue