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
|
add_library(StatusQ SHARED
|
||||||
${STATUSQ_QRC_COMPILED}
|
${STATUSQ_QRC_COMPILED}
|
||||||
include/StatusQ/QClipboardProxy.h
|
include/StatusQ/QClipboardProxy.h
|
||||||
|
include/StatusQ/concatmodel.h
|
||||||
include/StatusQ/leftjoinmodel.h
|
include/StatusQ/leftjoinmodel.h
|
||||||
include/StatusQ/modelutilsinternal.h
|
include/StatusQ/modelutilsinternal.h
|
||||||
include/StatusQ/permissionutilsinternal.h
|
include/StatusQ/permissionutilsinternal.h
|
||||||
|
@ -102,6 +103,7 @@ add_library(StatusQ SHARED
|
||||||
include/StatusQ/singleroleaggregator.h
|
include/StatusQ/singleroleaggregator.h
|
||||||
include/StatusQ/sumaggregator.h
|
include/StatusQ/sumaggregator.h
|
||||||
src/QClipboardProxy.cpp
|
src/QClipboardProxy.cpp
|
||||||
|
src/concatmodel.cpp
|
||||||
src/leftjoinmodel.cpp
|
src/leftjoinmodel.cpp
|
||||||
src/modelutilsinternal.cpp
|
src/modelutilsinternal.cpp
|
||||||
src/permissionutilsinternal.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 <qqmlsortfilterproxymodeltypes.h>
|
||||||
|
|
||||||
#include "StatusQ/QClipboardProxy.h"
|
#include "StatusQ/QClipboardProxy.h"
|
||||||
|
#include "StatusQ/concatmodel.h"
|
||||||
#include "StatusQ/leftjoinmodel.h"
|
#include "StatusQ/leftjoinmodel.h"
|
||||||
#include "StatusQ/modelutilsinternal.h"
|
#include "StatusQ/modelutilsinternal.h"
|
||||||
#include "StatusQ/permissionutilsinternal.h"
|
#include "StatusQ/permissionutilsinternal.h"
|
||||||
|
@ -35,6 +36,8 @@ public:
|
||||||
qmlRegisterType<ManageTokensController>("StatusQ.Models", 0, 1, "ManageTokensController");
|
qmlRegisterType<ManageTokensController>("StatusQ.Models", 0, 1, "ManageTokensController");
|
||||||
qmlRegisterType<ManageTokensModel>("StatusQ.Models", 0, 1, "ManageTokensModel");
|
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<LeftJoinModel>("StatusQ", 0, 1, "LeftJoinModel");
|
||||||
qmlRegisterType<SubmodelProxyModel>("StatusQ", 0, 1, "SubmodelProxyModel");
|
qmlRegisterType<SubmodelProxyModel>("StatusQ", 0, 1, "SubmodelProxyModel");
|
||||||
qmlRegisterType<RoleRename>("StatusQ", 0, 1, "RoleRename");
|
qmlRegisterType<RoleRename>("StatusQ", 0, 1, "RoleRename");
|
||||||
|
|
|
@ -64,3 +64,7 @@ add_test(NAME SingleRoleAggregatorTest COMMAND SingleRoleAggregatorTest)
|
||||||
add_executable(SumAggregatorTest tst_SumAggregator.cpp)
|
add_executable(SumAggregatorTest tst_SumAggregator.cpp)
|
||||||
target_link_libraries(SumAggregatorTest PRIVATE Qt5::Test StatusQ)
|
target_link_libraries(SumAggregatorTest PRIVATE Qt5::Test StatusQ)
|
||||||
add_test(NAME SumAggregatorTest COMMAND SumAggregatorTest)
|
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