StatusQ: MovableModel proxy for setting custom order over source model
Proxy decorating source mode with additional method move(from, to, count) similar to that available in ListModel. The custom order is stored within a proxy, not altering the original model. May be useful whenever UI needs to allow user to set custom order. Temporary state can be held in the proxy, and send to the backend when changes are accepted. Closes: #12686
This commit is contained in:
parent
22dcf8269f
commit
e51667911d
|
@ -0,0 +1,255 @@
|
|||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import QtQml 2.15
|
||||
|
||||
import StatusQ 0.1
|
||||
|
||||
import Models 1.0
|
||||
import Storybook 1.0
|
||||
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
ListModel {
|
||||
id: simpleSourceModel
|
||||
|
||||
ListElement {
|
||||
name: "entry 1"
|
||||
}
|
||||
ListElement {
|
||||
name: "entry 2"
|
||||
}
|
||||
ListElement {
|
||||
name: "entry 3"
|
||||
}
|
||||
ListElement {
|
||||
name: "entry 4"
|
||||
}
|
||||
ListElement {
|
||||
name: "entry 5"
|
||||
}
|
||||
ListElement {
|
||||
name: "entry 6"
|
||||
}
|
||||
ListElement {
|
||||
name: "entry 7"
|
||||
}
|
||||
ListElement {
|
||||
name: "entry 8"
|
||||
}
|
||||
}
|
||||
|
||||
MovableModel {
|
||||
id: movableModel
|
||||
|
||||
sourceModel: simpleSourceModel
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
|
||||
spacing: 50
|
||||
|
||||
ColumnLayout {
|
||||
Layout.preferredWidth: parent.width / 2
|
||||
Layout.fillHeight: true
|
||||
|
||||
Label {
|
||||
text: "SOURCE MODEL"
|
||||
|
||||
font.bold: true
|
||||
font.pixelSize: 17
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: sourceListView
|
||||
|
||||
spacing: 5
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
model: simpleSourceModel
|
||||
|
||||
ScrollBar.vertical: ScrollBar {}
|
||||
|
||||
delegate: RowLayout {
|
||||
width: ListView.view.width
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
|
||||
font.bold: true
|
||||
text: model.name
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "delete"
|
||||
|
||||
onClicked: simpleSourceModel.remove(model.index)
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "alter"
|
||||
|
||||
onClicked: simpleSourceModel.setProperty(
|
||||
index, "name", simpleSourceModel.get(index).name + "_")
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "⬆️"
|
||||
|
||||
onClicked: {
|
||||
if (index !== 0)
|
||||
simpleSourceModel.move(index, index - 1, 1)
|
||||
}
|
||||
}
|
||||
Button {
|
||||
text: "⬇️"
|
||||
|
||||
onClicked: {
|
||||
if (index !== simpleSourceModel.count - 1)
|
||||
simpleSourceModel.move(index, index + 1, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.preferredWidth: parent.width / 2
|
||||
Layout.fillHeight: true
|
||||
|
||||
Label {
|
||||
text: "DETACHED-ORDER MODEL"
|
||||
|
||||
font.bold: true
|
||||
font.pixelSize: 17
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: transformedListView
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
spacing: 5
|
||||
|
||||
model: movableModel
|
||||
|
||||
ScrollBar.vertical: ScrollBar {}
|
||||
|
||||
delegate: MouseArea {
|
||||
id: dragArea
|
||||
|
||||
property bool held: false
|
||||
readonly property int idx: model.index
|
||||
|
||||
anchors {
|
||||
left: parent ? parent.left : undefined
|
||||
right: parent ? parent.right : undefined
|
||||
}
|
||||
height: content.implicitHeight
|
||||
|
||||
drag.target: held ? content : undefined
|
||||
drag.axis: Drag.YAxis
|
||||
|
||||
onPressAndHold: held = true
|
||||
onReleased: held = false
|
||||
|
||||
|
||||
RowLayout {
|
||||
id: content
|
||||
|
||||
anchors {
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
width: dragArea.width
|
||||
|
||||
Drag.active: dragArea.held
|
||||
Drag.source: dragArea
|
||||
Drag.hotSpot.x: width / 2
|
||||
Drag.hotSpot.y: height / 2
|
||||
|
||||
states: State {
|
||||
when: dragArea.held
|
||||
|
||||
ParentChange { target: content; parent: root }
|
||||
AnchorChanges {
|
||||
target: content
|
||||
anchors {
|
||||
horizontalCenter: undefined
|
||||
verticalCenter: undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
|
||||
font.bold: true
|
||||
text: model.name
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "⬆️"
|
||||
enabled: index > 0
|
||||
|
||||
onClicked: movableModel.move(index, index - 1)
|
||||
}
|
||||
Button {
|
||||
text: "⬇️"
|
||||
enabled: index < transformedListView.count - 1
|
||||
|
||||
onClicked: movableModel.move(index, index + 1)
|
||||
}
|
||||
}
|
||||
|
||||
DropArea {
|
||||
anchors { fill: parent; margins: 10 }
|
||||
|
||||
onEntered: {
|
||||
const from = drag.source.idx
|
||||
const to = dragArea.idx
|
||||
|
||||
if (from === to)
|
||||
return
|
||||
|
||||
movableModel.move(from, to)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: 10
|
||||
|
||||
Button {
|
||||
text: "append to source model"
|
||||
|
||||
onClicked: simpleSourceModel.append({ name: "X" })
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "detach order explicitely"
|
||||
|
||||
onClicked: {
|
||||
movableModel.detach()
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
text: "Detached: " + movableModel.detached
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// category: Models
|
|
@ -101,6 +101,7 @@ add_library(StatusQ SHARED
|
|||
include/StatusQ/formatteddoubleproperty.h
|
||||
include/StatusQ/leftjoinmodel.h
|
||||
include/StatusQ/modelutilsinternal.h
|
||||
include/StatusQ/movablemodel.h
|
||||
include/StatusQ/permissionutilsinternal.h
|
||||
include/StatusQ/rolesrenamingmodel.h
|
||||
include/StatusQ/rxvalidator.h
|
||||
|
@ -120,6 +121,7 @@ add_library(StatusQ SHARED
|
|||
src/formatteddoubleproperty.cpp
|
||||
src/leftjoinmodel.cpp
|
||||
src/modelutilsinternal.cpp
|
||||
src/movablemodel.cpp
|
||||
src/permissionutilsinternal.cpp
|
||||
src/plugin.cpp
|
||||
src/rolesrenamingmodel.cpp
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QPointer>
|
||||
|
||||
#include <vector>
|
||||
|
||||
class MovableModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QAbstractItemModel* sourceModel READ sourceModel
|
||||
WRITE setSourceModel NOTIFY sourceModelChanged)
|
||||
|
||||
Q_PROPERTY(bool detached READ detached NOTIFY detachedChanged)
|
||||
public:
|
||||
explicit MovableModel(QObject *parent = nullptr);
|
||||
|
||||
void setSourceModel(QAbstractItemModel *sourceModel);
|
||||
QAbstractItemModel *sourceModel() const;
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex &index, int role) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
Q_INVOKABLE void detach();
|
||||
Q_INVOKABLE void move(int from, int to, int count = 1);
|
||||
Q_INVOKABLE QVector<int> order() const;
|
||||
|
||||
bool detached() const;
|
||||
|
||||
signals:
|
||||
void sourceModelChanged();
|
||||
void detachedChanged();
|
||||
|
||||
protected slots:
|
||||
void resetInternalData();
|
||||
|
||||
private:
|
||||
QPointer<QAbstractItemModel> m_sourceModel;
|
||||
|
||||
bool m_detached = false;
|
||||
std::vector<QPersistentModelIndex> m_indexes;
|
||||
};
|
|
@ -0,0 +1,280 @@
|
|||
#include "StatusQ/movablemodel.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
MovableModel::MovableModel(QObject* parent)
|
||||
: QAbstractListModel{parent}
|
||||
{
|
||||
}
|
||||
|
||||
void MovableModel::setSourceModel(QAbstractItemModel* sourceModel)
|
||||
{
|
||||
if (m_sourceModel == sourceModel)
|
||||
return;
|
||||
|
||||
beginResetModel();
|
||||
|
||||
if (m_sourceModel != nullptr)
|
||||
disconnect(m_sourceModel, nullptr, this, nullptr);
|
||||
|
||||
m_sourceModel = sourceModel;
|
||||
|
||||
if (sourceModel != nullptr) {
|
||||
connect(sourceModel, &QAbstractItemModel::rowsAboutToBeInserted, this,
|
||||
&MovableModel::beginInsertRows);
|
||||
|
||||
connect(sourceModel, &QAbstractItemModel::rowsInserted, this,
|
||||
&MovableModel::endInsertRows);
|
||||
|
||||
connect(sourceModel, &QAbstractItemModel::rowsAboutToBeRemoved, this,
|
||||
&MovableModel::beginRemoveRows);
|
||||
|
||||
connect(sourceModel, &QAbstractItemModel::rowsRemoved, this,
|
||||
&MovableModel::endRemoveRows);
|
||||
|
||||
connect(sourceModel, &QAbstractItemModel::rowsAboutToBeMoved, this,
|
||||
&MovableModel::beginMoveRows);
|
||||
|
||||
connect(sourceModel, &QAbstractItemModel::rowsMoved, this,
|
||||
&MovableModel::endMoveRows);
|
||||
|
||||
connect(sourceModel, &QAbstractItemModel::dataChanged, this,
|
||||
&MovableModel::dataChanged);
|
||||
|
||||
connect(sourceModel, &QAbstractItemModel::layoutAboutToBeChanged, this,
|
||||
&MovableModel::layoutAboutToBeChanged);
|
||||
|
||||
connect(sourceModel, &QAbstractItemModel::layoutChanged, this,
|
||||
&MovableModel::layoutChanged);
|
||||
|
||||
connect(sourceModel, &QAbstractItemModel::modelAboutToBeReset, this,
|
||||
&MovableModel::beginResetModel);
|
||||
|
||||
connect(sourceModel, &QAbstractItemModel::modelReset, this,
|
||||
&MovableModel::endResetModel);
|
||||
}
|
||||
|
||||
emit sourceModelChanged();
|
||||
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
QAbstractItemModel* MovableModel::sourceModel() const
|
||||
{
|
||||
return m_sourceModel;
|
||||
}
|
||||
|
||||
int MovableModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
if (m_detached)
|
||||
return m_indexes.size();
|
||||
|
||||
if (m_sourceModel == nullptr)
|
||||
return 0;
|
||||
|
||||
return m_sourceModel->rowCount();
|
||||
}
|
||||
|
||||
QVariant MovableModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!checkIndex(index, CheckIndexOption::IndexIsValid))
|
||||
return {};
|
||||
|
||||
if (m_detached)
|
||||
return m_indexes.at(index.row()).data(role);
|
||||
|
||||
if (m_sourceModel == nullptr)
|
||||
return {};
|
||||
|
||||
return m_sourceModel->data(index, role);
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> MovableModel::roleNames() const
|
||||
{
|
||||
if (m_sourceModel == nullptr)
|
||||
return {};
|
||||
|
||||
return m_sourceModel->roleNames();
|
||||
}
|
||||
|
||||
void MovableModel::detach()
|
||||
{
|
||||
if (m_detached || m_sourceModel == nullptr)
|
||||
return;
|
||||
|
||||
disconnect(m_sourceModel, &QAbstractItemModel::rowsAboutToBeInserted, this,
|
||||
&MovableModel::beginInsertRows);
|
||||
|
||||
disconnect(m_sourceModel, &QAbstractItemModel::rowsInserted, this,
|
||||
&MovableModel::endInsertRows);
|
||||
|
||||
disconnect(m_sourceModel, &QAbstractItemModel::rowsAboutToBeRemoved, this,
|
||||
&MovableModel::beginRemoveRows);
|
||||
|
||||
disconnect(m_sourceModel, &QAbstractItemModel::rowsRemoved, this,
|
||||
&MovableModel::endRemoveRows);
|
||||
|
||||
disconnect(m_sourceModel, &QAbstractItemModel::rowsAboutToBeMoved, this,
|
||||
&MovableModel::beginMoveRows);
|
||||
|
||||
disconnect(m_sourceModel, &QAbstractItemModel::rowsMoved, this,
|
||||
&MovableModel::endMoveRows);
|
||||
|
||||
disconnect(m_sourceModel, &QAbstractItemModel::dataChanged, this,
|
||||
&MovableModel::dataChanged);
|
||||
|
||||
disconnect(m_sourceModel, &QAbstractItemModel::layoutAboutToBeChanged, this,
|
||||
&MovableModel::layoutAboutToBeChanged);
|
||||
|
||||
disconnect(m_sourceModel, &QAbstractItemModel::layoutChanged, this,
|
||||
&MovableModel::layoutChanged);
|
||||
|
||||
connect(m_sourceModel, &QAbstractItemModel::dataChanged, this,
|
||||
[this](const QModelIndex& topLeft, const QModelIndex& bottomRight,
|
||||
const QVector<int>& roles) {
|
||||
emit dataChanged(index(0), index(rowCount() - 1), roles);
|
||||
});
|
||||
|
||||
connect(m_sourceModel, &QAbstractItemModel::rowsInserted, this,
|
||||
[this](const QModelIndex &parent, int first, int last) {
|
||||
|
||||
beginInsertRows({}, first, last);
|
||||
|
||||
int oldCount = m_indexes.size();
|
||||
int insertCount = last - first + 1;
|
||||
|
||||
m_indexes.reserve(m_indexes.size() + insertCount);
|
||||
|
||||
for (auto i = first; i <= last; i++)
|
||||
m_indexes.emplace_back(m_sourceModel->index(i, 0));
|
||||
|
||||
std::rotate(m_indexes.begin() + first, m_indexes.begin() + oldCount,
|
||||
m_indexes.end());
|
||||
|
||||
endInsertRows();
|
||||
});
|
||||
|
||||
connect(m_sourceModel, &QAbstractItemModel::rowsAboutToBeRemoved, this,
|
||||
[this] (const QModelIndex &parent, int first, int last) {
|
||||
std::vector<int> indicesToRemove;
|
||||
indicesToRemove.reserve(last - first + 1);
|
||||
|
||||
for (auto i = 0; i < m_indexes.size(); i++) {
|
||||
const QPersistentModelIndex& idx = m_indexes.at(i);
|
||||
|
||||
if (idx.row() >= first && idx.row() <= last)
|
||||
indicesToRemove.push_back(i);
|
||||
}
|
||||
|
||||
std::vector<std::pair<int, int>> sequences;
|
||||
auto sequenceBegin = indicesToRemove.front();
|
||||
auto sequenceEnd = sequenceBegin;
|
||||
|
||||
for (auto i = 1; i < indicesToRemove.size(); i++) {
|
||||
auto idxToRemove = indicesToRemove[i];
|
||||
|
||||
auto idxDiff = idxToRemove - sequenceEnd;
|
||||
if (idxDiff == 1)
|
||||
sequenceEnd = idxToRemove;
|
||||
|
||||
if (idxDiff != 1 || i == indicesToRemove.size() - 1) {
|
||||
sequences.emplace_back(sequenceBegin, sequenceEnd);
|
||||
sequenceBegin = idxToRemove;
|
||||
sequenceEnd = idxToRemove;
|
||||
}
|
||||
}
|
||||
|
||||
if (sequences.empty())
|
||||
sequences.emplace_back(sequenceBegin, sequenceEnd);
|
||||
|
||||
auto end = sequences.crend();
|
||||
|
||||
for (auto it = sequences.crbegin(); it != end; it++) {
|
||||
beginRemoveRows({}, it->first, it->second);
|
||||
|
||||
m_indexes.erase(m_indexes.begin() + it->first,
|
||||
m_indexes.begin() + it->second + 1);
|
||||
|
||||
endRemoveRows();
|
||||
}
|
||||
});
|
||||
|
||||
auto count = m_sourceModel->rowCount();
|
||||
|
||||
m_indexes.clear();
|
||||
m_indexes.reserve(count);
|
||||
|
||||
for (auto i = 0; i < count; i++)
|
||||
m_indexes.emplace_back(m_sourceModel->index(i, 0));
|
||||
|
||||
m_detached = true;
|
||||
emit detachedChanged();
|
||||
}
|
||||
|
||||
void MovableModel::move(int from, int to, int count)
|
||||
{
|
||||
const int rows = rowCount();
|
||||
if (from < 0 || to < 0 || count <= 0
|
||||
|| from + count > rows || to + count > rows) {
|
||||
qWarning() << "MovableModel: move: out of range";
|
||||
return;
|
||||
}
|
||||
|
||||
if (from == to)
|
||||
return;
|
||||
|
||||
const int sourceFirst = from;
|
||||
const int sourceLast = from + count - 1;
|
||||
const int destinationRow = to < from ? to : to + count;
|
||||
|
||||
if (!m_detached)
|
||||
detach();
|
||||
|
||||
beginMoveRows({}, sourceFirst, sourceLast, {}, destinationRow);
|
||||
|
||||
std::vector<QPersistentModelIndex> movedIndexes;
|
||||
movedIndexes.reserve(count);
|
||||
|
||||
std::move(m_indexes.begin() + sourceFirst,
|
||||
m_indexes.begin() + sourceLast + 1,
|
||||
std::back_insert_iterator(movedIndexes));
|
||||
m_indexes.erase(m_indexes.begin() + sourceFirst,
|
||||
m_indexes.begin() + sourceLast + 1);
|
||||
m_indexes.insert(m_indexes.begin() + to,
|
||||
std::move_iterator(movedIndexes.begin()),
|
||||
std::move_iterator(movedIndexes.end()));
|
||||
|
||||
endMoveRows();
|
||||
}
|
||||
|
||||
QVector<int> MovableModel::order() const
|
||||
{
|
||||
QVector<int> order(rowCount());
|
||||
|
||||
if (m_detached)
|
||||
std::transform(m_indexes.begin(), m_indexes.end(), order.begin(),
|
||||
[](auto& idx) { return idx.row(); });
|
||||
else
|
||||
std::iota(order.begin(), order.end(), 0);
|
||||
|
||||
return order;
|
||||
}
|
||||
|
||||
bool MovableModel::detached() const
|
||||
{
|
||||
return m_detached;
|
||||
}
|
||||
|
||||
void MovableModel::resetInternalData()
|
||||
{
|
||||
QAbstractListModel::resetInternalData();
|
||||
|
||||
m_indexes.clear();
|
||||
|
||||
if (m_detached) {
|
||||
m_detached = false;
|
||||
emit detachedChanged();
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@
|
|||
#include "StatusQ/fastexpressionsorter.h"
|
||||
#include "StatusQ/leftjoinmodel.h"
|
||||
#include "StatusQ/modelutilsinternal.h"
|
||||
#include "StatusQ/movablemodel.h"
|
||||
#include "StatusQ/permissionutilsinternal.h"
|
||||
#include "StatusQ/rolesrenamingmodel.h"
|
||||
#include "StatusQ/rxvalidator.h"
|
||||
|
@ -42,6 +43,7 @@ public:
|
|||
|
||||
qmlRegisterType<SourceModel>("StatusQ", 0, 1, "SourceModel");
|
||||
qmlRegisterType<ConcatModel>("StatusQ", 0, 1, "ConcatModel");
|
||||
qmlRegisterType<MovableModel>("StatusQ", 0, 1, "MovableModel");
|
||||
|
||||
qmlRegisterType<FastExpressionFilter>("StatusQ", 0, 1, "FastExpressionFilter");
|
||||
qmlRegisterType<FastExpressionRole>("StatusQ", 0, 1, "FastExpressionRole");
|
||||
|
|
|
@ -85,5 +85,9 @@ target_link_libraries(ConcatModelTest PRIVATE StatusQ StatusQTestLib)
|
|||
add_test(NAME ConcatModelTest COMMAND ConcatModelTest)
|
||||
|
||||
add_executable(WritableProxyModelTest tst_WritableProxyModel.cpp)
|
||||
target_link_libraries(WritableProxyModelTest PRIVATE Qt5::Qml Qt5::Test StatusQ)
|
||||
target_link_libraries(WritableProxyModelTest PRIVATE StatusQ StatusQTestLib)
|
||||
add_test(NAME WritableProxyModelTest COMMAND WritableProxyModelTest)
|
||||
|
||||
add_executable(MovableModelTest tst_MovableModel.cpp)
|
||||
target_link_libraries(MovableModelTest PRIVATE StatusQ StatusQTestLib)
|
||||
add_test(NAME MovableModelTest COMMAND MovableModelTest)
|
||||
|
|
|
@ -0,0 +1,577 @@
|
|||
#include <QSignalSpy>
|
||||
#include <QTest>
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QQmlEngine>
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
#include <StatusQ/movablemodel.h>
|
||||
|
||||
#include <TestHelpers/listmodelwrapper.h>
|
||||
#include <TestHelpers/modelsignalsspy.h>
|
||||
#include <TestHelpers/modeltestutils.h>
|
||||
#include <TestHelpers/snapshotmodel.h>
|
||||
|
||||
class TestMovableModel : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
int roleForName(const QHash<int, QByteArray>& roles,
|
||||
const QByteArray& name) const
|
||||
{
|
||||
auto keys = roles.keys(name);
|
||||
|
||||
if (keys.empty())
|
||||
return -1;
|
||||
|
||||
return keys.first();
|
||||
}
|
||||
|
||||
static constexpr QModelIndex InvalidIdx{};
|
||||
|
||||
private slots:
|
||||
void initializationTest()
|
||||
{
|
||||
QQmlEngine engine;
|
||||
|
||||
ListModelWrapper sourceModel(engine, R"([
|
||||
{ "name": "A", "subname": "a1" },
|
||||
{ "name": "A", "subname": "a2" },
|
||||
{ "name": "B", "subname": "b1" },
|
||||
{ "name": "C", "subname": "c1" },
|
||||
{ "name": "C", "subname": "c2" },
|
||||
{ "name": "C", "subname": "c3" }
|
||||
])");
|
||||
|
||||
MovableModel model;
|
||||
model.setSourceModel(sourceModel);
|
||||
|
||||
QCOMPARE(model.detached(), false);
|
||||
QVERIFY(isSame(&model, sourceModel));
|
||||
}
|
||||
|
||||
void detachTest()
|
||||
{
|
||||
QQmlEngine engine;
|
||||
|
||||
ListModelWrapper sourceModel(engine, R"([
|
||||
{ "name": "A", "subname": "a1" },
|
||||
{ "name": "A", "subname": "a2" },
|
||||
{ "name": "B", "subname": "b1" },
|
||||
{ "name": "C", "subname": "c1" },
|
||||
{ "name": "C", "subname": "c2" },
|
||||
{ "name": "C", "subname": "c3" }
|
||||
])");
|
||||
|
||||
MovableModel model;
|
||||
model.setSourceModel(sourceModel);
|
||||
|
||||
QSignalSpy detachChangedSpy(&model, &MovableModel::detachedChanged);
|
||||
model.detach();
|
||||
|
||||
QCOMPARE(detachChangedSpy.count(), 1);
|
||||
QCOMPARE(model.detached(), true);
|
||||
QVERIFY(isSame(&model, sourceModel));
|
||||
|
||||
model.setSourceModel(nullptr);
|
||||
QCOMPARE(detachChangedSpy.count(), 2);
|
||||
QCOMPARE(model.detached(), false);
|
||||
QCOMPARE(model.rowCount(), 0);
|
||||
}
|
||||
|
||||
void moveDownTest()
|
||||
{
|
||||
QQmlEngine engine;
|
||||
|
||||
auto source = R"([
|
||||
{ "name": "A", "subname": "a1" },
|
||||
{ "name": "A", "subname": "a2" },
|
||||
{ "name": "B", "subname": "b1" },
|
||||
{ "name": "C", "subname": "c1" },
|
||||
{ "name": "C", "subname": "c2" },
|
||||
{ "name": "C", "subname": "c3" }
|
||||
])";
|
||||
|
||||
ListModelWrapper sourceModel(engine, source);
|
||||
ListModelWrapper sourceModelCopy(engine, source);
|
||||
|
||||
MovableModel model;
|
||||
model.setSourceModel(sourceModel);
|
||||
model.detach();
|
||||
|
||||
ModelSignalsSpy signalsSpy(&model);
|
||||
ModelSignalsSpy referenceSignalsSpy(sourceModelCopy);
|
||||
|
||||
model.move(0, 2, 2);
|
||||
sourceModelCopy.move(0, 2, 2);
|
||||
|
||||
QCOMPARE(signalsSpy.count(), 2);
|
||||
QCOMPARE(referenceSignalsSpy.count(), 2);
|
||||
QCOMPARE(signalsSpy.rowsAboutToBeMovedSpy.count(), 1);
|
||||
QCOMPARE(signalsSpy.rowsMovedSpy.count(), 1);
|
||||
QCOMPARE(referenceSignalsSpy.rowsAboutToBeMovedSpy.count(), 1);
|
||||
QCOMPARE(referenceSignalsSpy.rowsMovedSpy.count(), 1);
|
||||
|
||||
QCOMPARE(signalsSpy.rowsAboutToBeMovedSpy.first(),
|
||||
referenceSignalsSpy.rowsAboutToBeMovedSpy.first());
|
||||
QCOMPARE(signalsSpy.rowsMovedSpy.first(),
|
||||
referenceSignalsSpy.rowsMovedSpy.first());
|
||||
|
||||
ListModelWrapper expected(engine, R"([
|
||||
{ "name": "B", "subname": "b1" },
|
||||
{ "name": "C", "subname": "c1" },
|
||||
{ "name": "A", "subname": "a1" },
|
||||
{ "name": "A", "subname": "a2" },
|
||||
{ "name": "C", "subname": "c2" },
|
||||
{ "name": "C", "subname": "c3" }
|
||||
])");
|
||||
|
||||
QVERIFY(isSame(sourceModelCopy, expected));
|
||||
QVERIFY(isSame(&model, expected));
|
||||
QVERIFY(isSame(&model, sourceModelCopy));
|
||||
}
|
||||
|
||||
void moveUpTest()
|
||||
{
|
||||
QQmlEngine engine;
|
||||
|
||||
auto source = R"([
|
||||
{ "name": "A", "subname": "a1" },
|
||||
{ "name": "A", "subname": "a2" },
|
||||
{ "name": "B", "subname": "b1" },
|
||||
{ "name": "C", "subname": "c1" },
|
||||
{ "name": "C", "subname": "c2" },
|
||||
{ "name": "C", "subname": "c3" }
|
||||
])";
|
||||
|
||||
ListModelWrapper sourceModel(engine, source);
|
||||
ListModelWrapper sourceModelCopy(engine, source);
|
||||
|
||||
MovableModel model;
|
||||
model.setSourceModel(sourceModel);
|
||||
model.detach();
|
||||
|
||||
ModelSignalsSpy signalsSpy(&model);
|
||||
ModelSignalsSpy referenceSignalsSpy(sourceModelCopy);
|
||||
|
||||
model.move(3, 1, 2);
|
||||
sourceModelCopy.move(3, 1, 2);
|
||||
|
||||
QCOMPARE(signalsSpy.count(), 2);
|
||||
QCOMPARE(referenceSignalsSpy.count(), 2);
|
||||
QCOMPARE(signalsSpy.rowsAboutToBeMovedSpy.count(), 1);
|
||||
QCOMPARE(signalsSpy.rowsMovedSpy.count(), 1);
|
||||
QCOMPARE(referenceSignalsSpy.rowsAboutToBeMovedSpy.count(), 1);
|
||||
QCOMPARE(referenceSignalsSpy.rowsMovedSpy.count(), 1);
|
||||
|
||||
QCOMPARE(signalsSpy.rowsAboutToBeMovedSpy.first(),
|
||||
referenceSignalsSpy.rowsAboutToBeMovedSpy.first());
|
||||
QCOMPARE(signalsSpy.rowsMovedSpy.first(),
|
||||
referenceSignalsSpy.rowsMovedSpy.first());
|
||||
|
||||
ListModelWrapper expected(engine, R"([
|
||||
{ "name": "A", "subname": "a1" },
|
||||
{ "name": "C", "subname": "c1" },
|
||||
{ "name": "C", "subname": "c2" },
|
||||
{ "name": "A", "subname": "a2" },
|
||||
{ "name": "B", "subname": "b1" },
|
||||
{ "name": "C", "subname": "c3" }
|
||||
])");
|
||||
|
||||
QVERIFY(isSame(sourceModelCopy, expected));
|
||||
QVERIFY(isSame(&model, expected));
|
||||
QVERIFY(isSame(&model, sourceModelCopy));
|
||||
}
|
||||
|
||||
void moveToEndTest()
|
||||
{
|
||||
QQmlEngine engine;
|
||||
|
||||
auto source = R"([
|
||||
{ "name": "A", "subname": "a1" },
|
||||
{ "name": "A", "subname": "a2" },
|
||||
{ "name": "B", "subname": "b1" },
|
||||
{ "name": "C", "subname": "c1" },
|
||||
{ "name": "C", "subname": "c2" },
|
||||
{ "name": "C", "subname": "c3" }
|
||||
])";
|
||||
|
||||
ListModelWrapper sourceModel(engine, source);
|
||||
ListModelWrapper sourceModelCopy(engine, source);
|
||||
|
||||
MovableModel model;
|
||||
model.setSourceModel(sourceModel);
|
||||
model.detach();
|
||||
|
||||
ModelSignalsSpy signalsSpy(&model);
|
||||
ModelSignalsSpy referenceSignalsSpy(sourceModelCopy);
|
||||
|
||||
model.move(1, 4, 2);
|
||||
sourceModelCopy.move(1, 4, 2);
|
||||
|
||||
QCOMPARE(signalsSpy.count(), 2);
|
||||
QCOMPARE(referenceSignalsSpy.count(), 2);
|
||||
QCOMPARE(signalsSpy.rowsAboutToBeMovedSpy.count(), 1);
|
||||
QCOMPARE(signalsSpy.rowsMovedSpy.count(), 1);
|
||||
QCOMPARE(referenceSignalsSpy.rowsAboutToBeMovedSpy.count(), 1);
|
||||
QCOMPARE(referenceSignalsSpy.rowsMovedSpy.count(), 1);
|
||||
|
||||
ListModelWrapper expected(engine, R"([
|
||||
{ "name": "A", "subname": "a1" },
|
||||
{ "name": "C", "subname": "c1" },
|
||||
{ "name": "C", "subname": "c2" },
|
||||
{ "name": "C", "subname": "c3" },
|
||||
{ "name": "A", "subname": "a2" },
|
||||
{ "name": "B", "subname": "b1" }
|
||||
])");
|
||||
|
||||
QVERIFY(isSame(sourceModelCopy, expected));
|
||||
QVERIFY(isSame(&model, expected));
|
||||
QVERIFY(isSame(&model, sourceModelCopy));
|
||||
}
|
||||
|
||||
void moveToBeginningTest()
|
||||
{
|
||||
QQmlEngine engine;
|
||||
|
||||
auto source = R"([
|
||||
{ "name": "A", "subname": "a1" },
|
||||
{ "name": "A", "subname": "a2" },
|
||||
{ "name": "B", "subname": "b1" },
|
||||
{ "name": "C", "subname": "c1" },
|
||||
{ "name": "C", "subname": "c2" },
|
||||
{ "name": "C", "subname": "c3" }
|
||||
])";
|
||||
|
||||
ListModelWrapper sourceModel(engine, source);
|
||||
ListModelWrapper sourceModelCopy(engine, source);
|
||||
|
||||
MovableModel model;
|
||||
model.setSourceModel(sourceModel);
|
||||
model.detach();
|
||||
|
||||
ModelSignalsSpy signalsSpy(&model);
|
||||
ModelSignalsSpy referenceSignalsSpy(sourceModelCopy);
|
||||
|
||||
model.move(3, 0, 3);
|
||||
sourceModelCopy.move(3, 0, 3);
|
||||
|
||||
QCOMPARE(signalsSpy.count(), 2);
|
||||
QCOMPARE(referenceSignalsSpy.count(), 2);
|
||||
QCOMPARE(signalsSpy.rowsAboutToBeMovedSpy.count(), 1);
|
||||
QCOMPARE(signalsSpy.rowsMovedSpy.count(), 1);
|
||||
QCOMPARE(referenceSignalsSpy.rowsAboutToBeMovedSpy.count(), 1);
|
||||
QCOMPARE(referenceSignalsSpy.rowsMovedSpy.count(), 1);
|
||||
|
||||
ListModelWrapper expected(engine, R"([
|
||||
{ "name": "C", "subname": "c1" },
|
||||
{ "name": "C", "subname": "c2" },
|
||||
{ "name": "C", "subname": "c3" },
|
||||
{ "name": "A", "subname": "a1" },
|
||||
{ "name": "A", "subname": "a2" },
|
||||
{ "name": "B", "subname": "b1" }
|
||||
])");
|
||||
|
||||
QVERIFY(isSame(sourceModelCopy, expected));
|
||||
QVERIFY(isSame(&model, expected));
|
||||
QVERIFY(isSame(&model, sourceModelCopy));
|
||||
}
|
||||
|
||||
void sortingTest()
|
||||
{
|
||||
QQmlEngine engine;
|
||||
|
||||
auto source = R"([
|
||||
{ "name": "A", "subname": "a1" },
|
||||
{ "name": "A", "subname": "a2" },
|
||||
{ "name": "B", "subname": "b1" },
|
||||
{ "name": "C", "subname": "c1" },
|
||||
{ "name": "C", "subname": "c2" },
|
||||
{ "name": "C", "subname": "c3" }
|
||||
])";
|
||||
|
||||
ListModelWrapper sourceModel(engine, source);
|
||||
ListModelWrapper sourceModelCopy(engine, source);
|
||||
|
||||
QSortFilterProxyModel sfpm;
|
||||
sfpm.setSourceModel(sourceModel);
|
||||
|
||||
MovableModel model;
|
||||
model.setSourceModel(&sfpm);
|
||||
model.detach();
|
||||
|
||||
model.move(2, 1);
|
||||
sourceModelCopy.move(2, 1);
|
||||
|
||||
ModelSignalsSpy signalsSpy(&model);
|
||||
|
||||
sfpm.setSortRole(1);
|
||||
sfpm.sort(0, Qt::DescendingOrder);
|
||||
|
||||
ListModelWrapper expectedSorted(engine, R"([
|
||||
{ "name": "C", "subname": "c3" },
|
||||
{ "name": "C", "subname": "c2" },
|
||||
{ "name": "C", "subname": "c1" },
|
||||
{ "name": "B", "subname": "b1" },
|
||||
{ "name": "A", "subname": "a2" },
|
||||
{ "name": "A", "subname": "a1" }
|
||||
])");
|
||||
|
||||
QCOMPARE(signalsSpy.count(), 0);
|
||||
|
||||
QCOMPARE(sfpm.roleNames().value(1), "subname");
|
||||
QVERIFY(isSame(&sfpm, expectedSorted));
|
||||
QVERIFY(isSame(&model, sourceModelCopy));
|
||||
}
|
||||
|
||||
void insertionTest()
|
||||
{
|
||||
QQmlEngine engine;
|
||||
|
||||
ListModelWrapper sourceModel(engine, R"([
|
||||
{ "name": "A", "subname": "a1" },
|
||||
{ "name": "A", "subname": "a2" },
|
||||
{ "name": "B", "subname": "b1" },
|
||||
{ "name": "C", "subname": "c1" },
|
||||
{ "name": "C", "subname": "c2" },
|
||||
{ "name": "C", "subname": "c3" }
|
||||
])");
|
||||
|
||||
MovableModel model;
|
||||
model.setSourceModel(sourceModel);
|
||||
model.detach();
|
||||
model.move(4, 1);
|
||||
|
||||
SnapshotModel snapshot(model);
|
||||
|
||||
QObject context;
|
||||
connect(&model, &QAbstractItemModel::rowsAboutToBeInserted, &context,
|
||||
[m = &model, s = &snapshot] {
|
||||
QVERIFY(isSame(m, s));
|
||||
});
|
||||
|
||||
ModelSignalsSpy signalsSpy(&model);
|
||||
|
||||
sourceModel.insert(3, QJsonArray {
|
||||
QJsonObject {{ "name", "D"}, { "subname", "d1" }},
|
||||
QJsonObject {{ "name", "D"}, { "subname", "d2" }}
|
||||
});
|
||||
|
||||
ListModelWrapper expected(engine, R"([
|
||||
{ "name": "A", "subname": "a1" },
|
||||
{ "name": "C", "subname": "c2" },
|
||||
{ "name": "A", "subname": "a2" },
|
||||
{ "name": "D", "subname": "d1" },
|
||||
{ "name": "D", "subname": "d2" },
|
||||
{ "name": "B", "subname": "b1" },
|
||||
{ "name": "C", "subname": "c1" },
|
||||
{ "name": "C", "subname": "c3" }
|
||||
])");
|
||||
|
||||
QCOMPARE(signalsSpy.count(), 2);
|
||||
QCOMPARE(signalsSpy.rowsAboutToBeInsertedSpy.count(), 1);
|
||||
QCOMPARE(signalsSpy.rowsInsertedSpy.count(), 1);
|
||||
|
||||
QCOMPARE(signalsSpy.rowsAboutToBeInsertedSpy.first().at(0), QModelIndex{});
|
||||
QCOMPARE(signalsSpy.rowsAboutToBeInsertedSpy.first().at(1), 3);
|
||||
QCOMPARE(signalsSpy.rowsAboutToBeInsertedSpy.first().at(2), 4);
|
||||
|
||||
QCOMPARE(signalsSpy.rowsInsertedSpy.first().at(0), QModelIndex{});
|
||||
QCOMPARE(signalsSpy.rowsInsertedSpy.first().at(1), 3);
|
||||
QCOMPARE(signalsSpy.rowsInsertedSpy.first().at(2), 4);
|
||||
|
||||
QVERIFY(isSame(&model, expected));
|
||||
}
|
||||
|
||||
void removalTest()
|
||||
{
|
||||
QQmlEngine engine;
|
||||
|
||||
ListModelWrapper sourceModel(engine, R"([
|
||||
{ "name": "A", "subname": "a1" },
|
||||
{ "name": "A", "subname": "a2" },
|
||||
{ "name": "B", "subname": "b1" },
|
||||
{ "name": "C", "subname": "c1" },
|
||||
{ "name": "C", "subname": "c2" },
|
||||
{ "name": "C", "subname": "c3" }
|
||||
])");
|
||||
|
||||
MovableModel model;
|
||||
model.setSourceModel(sourceModel);
|
||||
model.move(3, 0, 3);
|
||||
|
||||
ListModelWrapper expectedIntermediate(engine, R"([
|
||||
{ "name": "C", "subname": "c1" },
|
||||
{ "name": "C", "subname": "c2" },
|
||||
{ "name": "C", "subname": "c3" },
|
||||
{ "name": "A", "subname": "a1" },
|
||||
{ "name": "A", "subname": "a2" },
|
||||
{ "name": "B", "subname": "b1" }
|
||||
])");
|
||||
|
||||
QVERIFY(isSame(&model, expectedIntermediate));
|
||||
|
||||
ModelSignalsSpy signalsSpy(&model);
|
||||
|
||||
sourceModel.remove(1, 4);
|
||||
|
||||
ListModelWrapper expected(engine, R"([
|
||||
{ "name": "C", "subname": "c3" },
|
||||
{ "name": "A", "subname": "a1" }
|
||||
])");
|
||||
|
||||
QVERIFY(isSame(&model, expected));
|
||||
|
||||
QCOMPARE(signalsSpy.count(), 4);
|
||||
QCOMPARE(signalsSpy.rowsAboutToBeRemovedSpy.count(), 2);
|
||||
QCOMPARE(signalsSpy.rowsRemovedSpy.count(), 2);
|
||||
|
||||
QCOMPARE(signalsSpy.rowsAboutToBeRemovedSpy.at(0).at(0), QModelIndex{});
|
||||
QCOMPARE(signalsSpy.rowsAboutToBeRemovedSpy.at(0).at(1), 4);
|
||||
QCOMPARE(signalsSpy.rowsAboutToBeRemovedSpy.at(0).at(2), 5);
|
||||
|
||||
QCOMPARE(signalsSpy.rowsAboutToBeRemovedSpy.at(1).at(0), QModelIndex{});
|
||||
QCOMPARE(signalsSpy.rowsAboutToBeRemovedSpy.at(1).at(1), 0);
|
||||
QCOMPARE(signalsSpy.rowsAboutToBeRemovedSpy.at(1).at(2), 1);
|
||||
|
||||
QCOMPARE(signalsSpy.rowsRemovedSpy.at(0).at(0), QModelIndex{});
|
||||
QCOMPARE(signalsSpy.rowsRemovedSpy.at(0).at(1), 4);
|
||||
QCOMPARE(signalsSpy.rowsRemovedSpy.at(0).at(2), 5);
|
||||
|
||||
QCOMPARE(signalsSpy.rowsRemovedSpy.at(1).at(0), QModelIndex{});
|
||||
QCOMPARE(signalsSpy.rowsRemovedSpy.at(1).at(1), 0);
|
||||
QCOMPARE(signalsSpy.rowsRemovedSpy.at(1).at(2), 1);
|
||||
}
|
||||
|
||||
void dataChangeTest()
|
||||
{
|
||||
QQmlEngine engine;
|
||||
|
||||
ListModelWrapper sourceModel(engine, R"([
|
||||
{ "name": "A", "subname": "a1" },
|
||||
{ "name": "A", "subname": "a2" },
|
||||
{ "name": "B", "subname": "b1" },
|
||||
{ "name": "C", "subname": "c1" },
|
||||
{ "name": "C", "subname": "c2" },
|
||||
{ "name": "C", "subname": "c3" }
|
||||
])");
|
||||
|
||||
MovableModel model;
|
||||
model.setSourceModel(sourceModel);
|
||||
model.detach();
|
||||
|
||||
ModelSignalsSpy signalsSpy(&model);
|
||||
|
||||
sourceModel.setProperty(1, "subname", "a2_");
|
||||
|
||||
ListModelWrapper expected(engine, R"([
|
||||
{ "name": "A", "subname": "a1" },
|
||||
{ "name": "A", "subname": "a2_" },
|
||||
{ "name": "B", "subname": "b1" },
|
||||
{ "name": "C", "subname": "c1" },
|
||||
{ "name": "C", "subname": "c2" },
|
||||
{ "name": "C", "subname": "c3" }
|
||||
])");
|
||||
|
||||
auto subnameRole = roleForName(model.roleNames(), "subname");
|
||||
|
||||
QCOMPARE(signalsSpy.count(), 1);
|
||||
QCOMPARE(signalsSpy.dataChangedSpy.count(), 1);
|
||||
QCOMPARE(signalsSpy.dataChangedSpy.first().at(0), model.index(0));
|
||||
QCOMPARE(signalsSpy.dataChangedSpy.first().at(1), model.index(model.rowCount() - 1));
|
||||
QCOMPARE(signalsSpy.dataChangedSpy.first().at(2).value<QVector<int>>(),
|
||||
{subnameRole});
|
||||
|
||||
QVERIFY(isSame(&model, expected));
|
||||
}
|
||||
|
||||
void orderTest()
|
||||
{
|
||||
QQmlEngine engine;
|
||||
|
||||
ListModelWrapper sourceModel(engine, R"([
|
||||
{ "name": "A", "subname": "a1" },
|
||||
{ "name": "A", "subname": "a2" },
|
||||
{ "name": "B", "subname": "b1" },
|
||||
{ "name": "C", "subname": "c1" },
|
||||
{ "name": "C", "subname": "c2" },
|
||||
{ "name": "C", "subname": "c3" }
|
||||
])");
|
||||
|
||||
MovableModel model;
|
||||
model.setSourceModel(sourceModel);
|
||||
|
||||
QVector<int> expectedOrder = {0, 1, 2, 3, 4, 5};
|
||||
QCOMPARE(model.order(), expectedOrder);
|
||||
|
||||
sourceModel.move(0, 1);
|
||||
QCOMPARE(model.order(), expectedOrder);
|
||||
|
||||
sourceModel.move(0, 1); // restore original source order
|
||||
model.move(0, 1);
|
||||
expectedOrder = {1, 0, 2, 3, 4, 5};
|
||||
QCOMPARE(model.order(), expectedOrder);
|
||||
|
||||
sourceModel.move(0, 1);
|
||||
expectedOrder = {0, 1, 2, 3, 4, 5};
|
||||
QCOMPARE(model.order(), expectedOrder);
|
||||
|
||||
sourceModel.move(0, 1); // restore original source order
|
||||
model.move(0, 1);
|
||||
expectedOrder = {0, 1, 2, 3, 4, 5};
|
||||
QCOMPARE(model.order(), expectedOrder);
|
||||
|
||||
model.move(1, 3, 2);
|
||||
expectedOrder = {0, 3, 4, 1, 2, 5};
|
||||
QCOMPARE(model.order(), expectedOrder);
|
||||
|
||||
sourceModel.remove(4);
|
||||
expectedOrder = {0, 3, 1, 2, 4};
|
||||
QCOMPARE(model.order(), expectedOrder);
|
||||
}
|
||||
|
||||
void invalidMoveTest()
|
||||
{
|
||||
QQmlEngine engine;
|
||||
|
||||
ListModelWrapper sourceModel(engine, R"([
|
||||
{ "name": "A", "subname": "a1" },
|
||||
{ "name": "A", "subname": "a2" },
|
||||
{ "name": "B", "subname": "b1" },
|
||||
{ "name": "C", "subname": "c1" },
|
||||
{ "name": "C", "subname": "c2" },
|
||||
{ "name": "C", "subname": "c3" }
|
||||
])");
|
||||
|
||||
MovableModel model;
|
||||
model.setSourceModel(sourceModel);
|
||||
|
||||
{
|
||||
QTest::ignoreMessage(QtWarningMsg,
|
||||
"MovableModel: move: out of range");
|
||||
model.move(2, -1);
|
||||
}
|
||||
{
|
||||
QTest::ignoreMessage(QtWarningMsg,
|
||||
"MovableModel: move: out of range");
|
||||
model.move(-2, 1);
|
||||
}
|
||||
{
|
||||
QTest::ignoreMessage(QtWarningMsg,
|
||||
"MovableModel: move: out of range");
|
||||
model.move(5, 0, 2);
|
||||
}
|
||||
{
|
||||
QTest::ignoreMessage(QtWarningMsg,
|
||||
"MovableModel: move: out of range");
|
||||
model.move(0, 5, 2);
|
||||
}
|
||||
|
||||
// QTest::failOnWarning(QRegularExpression(".?")); // Qt 6.3
|
||||
sourceModel.move(0, 0, 2);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestMovableModel)
|
||||
#include "tst_MovableModel.moc"
|
Loading…
Reference in New Issue