forked from quickshell/quickshell
		
	core/scriptmodel: add expression model for unique lists
This commit is contained in:
		
							parent
							
								
									2f194b7894
								
							
						
					
					
						commit
						08836ca1f3
					
				
					 8 changed files with 431 additions and 2 deletions
				
			
		| 
						 | 
				
			
			@ -36,6 +36,7 @@ qt_add_library(quickshell-core STATIC
 | 
			
		|||
	instanceinfo.cpp
 | 
			
		||||
	common.cpp
 | 
			
		||||
	iconprovider.cpp
 | 
			
		||||
	scriptmodel.cpp
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
qt_add_qml_module(quickshell-core
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,6 +2,7 @@
 | 
			
		|||
 | 
			
		||||
#include <qabstractitemmodel.h>
 | 
			
		||||
#include <qhash.h>
 | 
			
		||||
#include <qnamespace.h>
 | 
			
		||||
#include <qobject.h>
 | 
			
		||||
#include <qqmllist.h>
 | 
			
		||||
#include <qtmetamacros.h>
 | 
			
		||||
| 
						 | 
				
			
			@ -14,11 +15,13 @@ qint32 UntypedObjectModel::rowCount(const QModelIndex& parent) const {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
QVariant UntypedObjectModel::data(const QModelIndex& index, qint32 role) const {
 | 
			
		||||
	if (role != 0) return QVariant();
 | 
			
		||||
	if (role != Qt::UserRole) return QVariant();
 | 
			
		||||
	return QVariant::fromValue(this->valuesList.at(index.row()));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
QHash<int, QByteArray> UntypedObjectModel::roleNames() const { return {{0, "modelData"}}; }
 | 
			
		||||
QHash<int, QByteArray> UntypedObjectModel::roleNames() const {
 | 
			
		||||
	return {{Qt::UserRole, "modelData"}};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
QQmlListProperty<QObject> UntypedObjectModel::values() {
 | 
			
		||||
	return QQmlListProperty<QObject>(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										133
									
								
								src/core/scriptmodel.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								src/core/scriptmodel.cpp
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,133 @@
 | 
			
		|||
#include "scriptmodel.hpp"
 | 
			
		||||
#include <algorithm>
 | 
			
		||||
#include <iterator>
 | 
			
		||||
 | 
			
		||||
#include <qabstractitemmodel.h>
 | 
			
		||||
#include <qcontainerfwd.h>
 | 
			
		||||
#include <qlist.h>
 | 
			
		||||
#include <qnamespace.h>
 | 
			
		||||
#include <qtmetamacros.h>
 | 
			
		||||
#include <qtversionchecks.h>
 | 
			
		||||
#include <qtypes.h>
 | 
			
		||||
#include <qvariant.h>
 | 
			
		||||
 | 
			
		||||
void ScriptModel::updateValuesUnique(const QVariantList& newValues) {
 | 
			
		||||
	this->mValues.reserve(newValues.size());
 | 
			
		||||
 | 
			
		||||
	auto iter = this->mValues.begin();
 | 
			
		||||
	auto newIter = newValues.begin();
 | 
			
		||||
 | 
			
		||||
	while (true) {
 | 
			
		||||
		if (newIter == newValues.end()) {
 | 
			
		||||
			if (iter == this->mValues.end()) break;
 | 
			
		||||
 | 
			
		||||
			auto startIndex = static_cast<qint32>(newValues.length());
 | 
			
		||||
			auto endIndex = static_cast<qint32>(this->mValues.length() - 1);
 | 
			
		||||
 | 
			
		||||
			this->beginRemoveRows(QModelIndex(), startIndex, endIndex);
 | 
			
		||||
			this->mValues.erase(iter, this->mValues.end());
 | 
			
		||||
			this->endRemoveRows();
 | 
			
		||||
 | 
			
		||||
			break;
 | 
			
		||||
		} else if (iter == this->mValues.end()) {
 | 
			
		||||
			// Prior branch ensures length is at least 1.
 | 
			
		||||
			auto startIndex = static_cast<qint32>(this->mValues.length());
 | 
			
		||||
			auto endIndex = static_cast<qint32>(newValues.length() - 1);
 | 
			
		||||
 | 
			
		||||
			this->beginInsertRows(QModelIndex(), startIndex, endIndex);
 | 
			
		||||
			this->mValues.append(newValues.sliced(startIndex));
 | 
			
		||||
			this->endInsertRows();
 | 
			
		||||
 | 
			
		||||
			break;
 | 
			
		||||
		} else if (*newIter != *iter) {
 | 
			
		||||
			auto oldIter = std::find(iter, this->mValues.end(), *newIter);
 | 
			
		||||
 | 
			
		||||
			if (oldIter != this->mValues.end()) {
 | 
			
		||||
				if (std::find(newIter, newValues.end(), *iter) == newValues.end()) {
 | 
			
		||||
					// Remove any entries we would otherwise move around that aren't in the new list.
 | 
			
		||||
					auto startIter = iter;
 | 
			
		||||
 | 
			
		||||
					do {
 | 
			
		||||
						++iter;
 | 
			
		||||
					} while (iter != this->mValues.end()
 | 
			
		||||
					         && std::find(newIter, newValues.end(), *iter) == newValues.end());
 | 
			
		||||
 | 
			
		||||
					auto index = static_cast<qint32>(std::distance(this->mValues.begin(), iter));
 | 
			
		||||
					auto startIndex = static_cast<qint32>(std::distance(this->mValues.begin(), startIter));
 | 
			
		||||
 | 
			
		||||
					this->beginRemoveRows(QModelIndex(), startIndex, index - 1);
 | 
			
		||||
					iter = this->mValues.erase(startIter, iter);
 | 
			
		||||
					this->endRemoveRows();
 | 
			
		||||
				} else {
 | 
			
		||||
					// Advance iters to capture a whole move sequence as a single operation if possible.
 | 
			
		||||
					auto oldStartIter = oldIter;
 | 
			
		||||
					do {
 | 
			
		||||
						++oldIter;
 | 
			
		||||
						++newIter;
 | 
			
		||||
					} while (oldIter != this->mValues.end() && newIter != newValues.end()
 | 
			
		||||
					         && *oldIter == *newIter);
 | 
			
		||||
 | 
			
		||||
					auto index = static_cast<qint32>(std::distance(this->mValues.begin(), iter));
 | 
			
		||||
					auto oldStartIndex =
 | 
			
		||||
					    static_cast<qint32>(std::distance(this->mValues.begin(), oldStartIter));
 | 
			
		||||
					auto oldIndex = static_cast<qint32>(std::distance(this->mValues.begin(), oldIter));
 | 
			
		||||
					auto len = oldIndex - oldStartIndex;
 | 
			
		||||
 | 
			
		||||
					this->beginMoveRows(QModelIndex(), oldStartIndex, oldIndex - 1, QModelIndex(), index);
 | 
			
		||||
 | 
			
		||||
					// While it is possible to optimize this further, it is currently not worth the time.
 | 
			
		||||
					for (auto i = 0; i != len; i++) {
 | 
			
		||||
						this->mValues.move(oldStartIndex + i, index + i);
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					iter = this->mValues.begin() + (index + len);
 | 
			
		||||
					this->endMoveRows();
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				auto startNewIter = newIter;
 | 
			
		||||
 | 
			
		||||
				do {
 | 
			
		||||
					newIter++;
 | 
			
		||||
				} while (newIter != newValues.end()
 | 
			
		||||
				         && std::find(iter, this->mValues.end(), *newIter) == this->mValues.end());
 | 
			
		||||
 | 
			
		||||
				auto index = static_cast<qint32>(std::distance(this->mValues.begin(), iter));
 | 
			
		||||
				auto newIndex = static_cast<qint32>(std::distance(newValues.begin(), newIter));
 | 
			
		||||
				auto startNewIndex = static_cast<qint32>(std::distance(newValues.begin(), startNewIter));
 | 
			
		||||
				auto len = newIndex - startNewIndex;
 | 
			
		||||
 | 
			
		||||
				this->beginInsertRows(QModelIndex(), index, index + len - 1);
 | 
			
		||||
#if QT_VERSION <= QT_VERSION_CHECK(6, 8, 0)
 | 
			
		||||
				this->mValues.resize(this->mValues.length() + len);
 | 
			
		||||
#else
 | 
			
		||||
				this->mValues.resizeForOverwrite(this->mValues.length() + len);
 | 
			
		||||
#endif
 | 
			
		||||
				iter = this->mValues.begin() + index; // invalidated
 | 
			
		||||
				std::move_backward(iter, this->mValues.end() - len, this->mValues.end());
 | 
			
		||||
				iter = std::copy(startNewIter, newIter, iter);
 | 
			
		||||
				this->endInsertRows();
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			++iter;
 | 
			
		||||
			++newIter;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ScriptModel::setValues(const QVariantList& newValues) {
 | 
			
		||||
	if (newValues == this->mValues) return;
 | 
			
		||||
	this->updateValuesUnique(newValues);
 | 
			
		||||
	emit this->valuesChanged();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
qint32 ScriptModel::rowCount(const QModelIndex& parent) const {
 | 
			
		||||
	if (parent != QModelIndex()) return 0;
 | 
			
		||||
	return static_cast<qint32>(this->mValues.length());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
QVariant ScriptModel::data(const QModelIndex& index, qint32 role) const {
 | 
			
		||||
	if (role != Qt::UserRole) return QVariant();
 | 
			
		||||
	return this->mValues.at(index.row());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
QHash<int, QByteArray> ScriptModel::roleNames() const { return {{Qt::UserRole, "modelData"}}; }
 | 
			
		||||
							
								
								
									
										74
									
								
								src/core/scriptmodel.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								src/core/scriptmodel.hpp
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,74 @@
 | 
			
		|||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <qabstractitemmodel.h>
 | 
			
		||||
#include <qcontainerfwd.h>
 | 
			
		||||
#include <qproperty.h>
 | 
			
		||||
#include <qqmlintegration.h>
 | 
			
		||||
#include <qtmetamacros.h>
 | 
			
		||||
 | 
			
		||||
///! QML model reflecting a javascript expression
 | 
			
		||||
/// ScriptModel is a QML [Data Model] that generates model operations based on changes
 | 
			
		||||
/// to a javascript expression attached to @@values.
 | 
			
		||||
///
 | 
			
		||||
/// ### When should I use this
 | 
			
		||||
/// ScriptModel should be used when you would otherwise use a javascript expression as a model,
 | 
			
		||||
/// [QAbstractItemModel] is accepted, and the data is likely to change over the lifetime of the program.
 | 
			
		||||
///
 | 
			
		||||
/// When directly using a javascript expression as a model, types like @@QtQuick.Repeater or @@QtQuick.ListView
 | 
			
		||||
/// will destroy all created delegates, and re-create the entire list. In the case of @@QtQuick.ListView this
 | 
			
		||||
/// will also prevent animations from working. If you wrap your expression with ScriptModel, only new items
 | 
			
		||||
/// will be created, and ListView animations will work as expected.
 | 
			
		||||
///
 | 
			
		||||
/// ### Example
 | 
			
		||||
/// ```qml
 | 
			
		||||
/// // Will cause all delegates to be re-created every time filterText changes.
 | 
			
		||||
/// @@QtQuick.Repeater {
 | 
			
		||||
///   model: myList.filter(entry => entry.name.startsWith(filterText))
 | 
			
		||||
///   delegate: // ...
 | 
			
		||||
/// }
 | 
			
		||||
///
 | 
			
		||||
/// // Will add and remove delegates only when required.
 | 
			
		||||
/// @@QtQuick.Repeater {
 | 
			
		||||
///   model: ScriptModel {
 | 
			
		||||
///     values: myList.filter(entry => entry.name.startsWith(filterText))
 | 
			
		||||
///   }
 | 
			
		||||
///
 | 
			
		||||
///   delegate: // ...
 | 
			
		||||
/// }
 | 
			
		||||
/// ```
 | 
			
		||||
class ScriptModel: public QAbstractListModel {
 | 
			
		||||
	Q_OBJECT;
 | 
			
		||||
	/// The list of values to reflect in the model.
 | 
			
		||||
	/// > [!WARNING] ScriptModel currently only works with lists of *unique* values.
 | 
			
		||||
	/// > There must not be any duplicates in the given list, or behavior of the model is undefined.
 | 
			
		||||
	///
 | 
			
		||||
	/// > [!TIP] @@ObjectModel$s supplied by Quickshell types will only contain unique values,
 | 
			
		||||
	/// > and can be used like so:
 | 
			
		||||
	/// >
 | 
			
		||||
	/// > ```qml
 | 
			
		||||
	/// > ScriptModel {
 | 
			
		||||
	/// >   values: DesktopEntries.applications.values.filter(...)
 | 
			
		||||
	/// > }
 | 
			
		||||
	/// > ```
 | 
			
		||||
	/// >
 | 
			
		||||
	/// > Note that we are using @@DesktopEntries.values because it will cause @@ScriptModel.values
 | 
			
		||||
	/// > to receive an update on change.
 | 
			
		||||
	Q_PROPERTY(QVariantList values READ values WRITE setValues NOTIFY valuesChanged);
 | 
			
		||||
	QML_ELEMENT;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
	[[nodiscard]] const QVariantList& values() const { return this->mValues; }
 | 
			
		||||
	void setValues(const QVariantList& newValues);
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] qint32 rowCount(const QModelIndex& parent) const override;
 | 
			
		||||
	[[nodiscard]] QVariant data(const QModelIndex& index, qint32 role) const override;
 | 
			
		||||
	[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
 | 
			
		||||
 | 
			
		||||
signals:
 | 
			
		||||
	void valuesChanged();
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
	QVariantList mValues;
 | 
			
		||||
 | 
			
		||||
	void updateValuesUnique(const QVariantList& newValues);
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -6,3 +6,4 @@ endfunction()
 | 
			
		|||
 | 
			
		||||
qs_test(transformwatcher transformwatcher.cpp)
 | 
			
		||||
qs_test(ringbuffer ringbuf.cpp)
 | 
			
		||||
qs_test(scriptmodel scriptmodel.cpp)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										179
									
								
								src/core/test/scriptmodel.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										179
									
								
								src/core/test/scriptmodel.cpp
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,179 @@
 | 
			
		|||
#include "scriptmodel.hpp"
 | 
			
		||||
 | 
			
		||||
#include <qabstractitemmodel.h>
 | 
			
		||||
#include <qabstractitemmodeltester.h>
 | 
			
		||||
#include <qcontainerfwd.h>
 | 
			
		||||
#include <qdebug.h>
 | 
			
		||||
#include <qlist.h>
 | 
			
		||||
#include <qlogging.h>
 | 
			
		||||
#include <qobject.h>
 | 
			
		||||
#include <qsignalspy.h>
 | 
			
		||||
#include <qstring.h>
 | 
			
		||||
#include <qtest.h>
 | 
			
		||||
#include <qtestcase.h>
 | 
			
		||||
#include <qtypes.h>
 | 
			
		||||
 | 
			
		||||
#include "../scriptmodel.hpp"
 | 
			
		||||
 | 
			
		||||
using OpList = QList<ModelOperation>;
 | 
			
		||||
 | 
			
		||||
bool ModelOperation::operator==(const ModelOperation& other) const {
 | 
			
		||||
	return other.operation == this->operation && other.index == this->index
 | 
			
		||||
	    && other.length == this->length && other.destIndex == this->destIndex;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
QDebug& operator<<(QDebug& debug, const ModelOperation& op) {
 | 
			
		||||
	auto saver = QDebugStateSaver(debug);
 | 
			
		||||
	debug.nospace();
 | 
			
		||||
 | 
			
		||||
	switch (op.operation) {
 | 
			
		||||
	case ModelOperation::Insert: debug << "Insert"; break;
 | 
			
		||||
	case ModelOperation::Remove: debug << "Remove"; break;
 | 
			
		||||
	case ModelOperation::Move: debug << "Move"; break;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	debug << "(i: " << op.index << ", l: " << op.length;
 | 
			
		||||
 | 
			
		||||
	if (op.destIndex != -1) {
 | 
			
		||||
		debug << ", d: " << op.destIndex;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	debug << ')';
 | 
			
		||||
 | 
			
		||||
	return debug;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
QDebug& operator<<(QDebug& debug, const QVariantList& list) {
 | 
			
		||||
	auto str = QString();
 | 
			
		||||
 | 
			
		||||
	for (const auto& var: list) {
 | 
			
		||||
		if (var.canConvert<QChar>()) {
 | 
			
		||||
			str += var.value<QChar>();
 | 
			
		||||
		} else {
 | 
			
		||||
			qFatal() << "QVariantList debug overridden in test";
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	debug << str;
 | 
			
		||||
	return debug;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void TestScriptModel::unique_data() {
 | 
			
		||||
	QTest::addColumn<QString>("oldstr");
 | 
			
		||||
	QTest::addColumn<QString>("newstr");
 | 
			
		||||
	QTest::addColumn<OpList>("operations");
 | 
			
		||||
 | 
			
		||||
	QTest::addRow("append") << "ABCD" << "ABCDEFG" << OpList({{ModelOperation::Insert, 4, 3}});
 | 
			
		||||
 | 
			
		||||
	QTest::addRow("prepend") << "EFG" << "ABCDEFG" << OpList({{ModelOperation::Insert, 0, 4}});
 | 
			
		||||
 | 
			
		||||
	QTest::addRow("insert") << "ABFG" << "ABCDEFG" << OpList({{ModelOperation::Insert, 2, 3}});
 | 
			
		||||
 | 
			
		||||
	QTest::addRow("chop") << "ABCDEFG" << "ABCD" << OpList({{ModelOperation::Remove, 4, 3}});
 | 
			
		||||
 | 
			
		||||
	QTest::addRow("slice") << "ABCDEFG" << "DEFG" << OpList({{ModelOperation::Remove, 0, 3}});
 | 
			
		||||
 | 
			
		||||
	QTest::addRow("remove_mid") << "ABCDEFG" << "ABFG" << OpList({{ModelOperation::Remove, 2, 3}});
 | 
			
		||||
 | 
			
		||||
	QTest::addRow("move_single") << "ABCDEFG" << "AFBCDEG"
 | 
			
		||||
	                             << OpList({{ModelOperation::Move, 5, 1, 1}});
 | 
			
		||||
 | 
			
		||||
	QTest::addRow("move_range") << "ABCDEFG" << "ADEFBCG"
 | 
			
		||||
	                            << OpList({{ModelOperation::Move, 3, 3, 1}});
 | 
			
		||||
 | 
			
		||||
	// beginning to end is the same operation
 | 
			
		||||
	QTest::addRow("move_end_to_beginning")
 | 
			
		||||
	    << "ABCDEFG" << "EFGABCD" << OpList({{ModelOperation::Move, 4, 3, 0}});
 | 
			
		||||
 | 
			
		||||
	QTest::addRow("move_overlapping")
 | 
			
		||||
	    << "ABCDEFG" << "ABDEFCG" << OpList({{ModelOperation::Move, 3, 3, 2}});
 | 
			
		||||
 | 
			
		||||
	// Ensure iterators arent skipping anything at the end of operations by performing
 | 
			
		||||
	// multiple back to back.
 | 
			
		||||
 | 
			
		||||
	QTest::addRow("insert_state_ok") << "ABCDEFG" << "ABXXEFG"
 | 
			
		||||
	                                 << OpList({
 | 
			
		||||
	                                        {ModelOperation::Insert, 2, 2}, // ABXXCDEFG
 | 
			
		||||
	                                        {ModelOperation::Remove, 4, 2}, // ABXXEFG
 | 
			
		||||
	                                    });
 | 
			
		||||
 | 
			
		||||
	QTest::addRow("remove_state_ok") << "ABCDEFG" << "ABFGE"
 | 
			
		||||
	                                 << OpList({
 | 
			
		||||
	                                        {ModelOperation::Remove, 2, 2},  // ABEFG
 | 
			
		||||
	                                        {ModelOperation::Move, 3, 2, 2}, // ABFGE
 | 
			
		||||
	                                    });
 | 
			
		||||
 | 
			
		||||
	QTest::addRow("move_state_ok") << "ABCDEFG" << "ABEFXYCDG"
 | 
			
		||||
	                               << OpList({
 | 
			
		||||
	                                      {ModelOperation::Move, 4, 2, 2}, // ABEFCDG
 | 
			
		||||
	                                      {ModelOperation::Insert, 4, 2},  // ABEFXYCDG
 | 
			
		||||
	                                  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void TestScriptModel::unique() {
 | 
			
		||||
	QFETCH(const QString, oldstr);
 | 
			
		||||
	QFETCH(const QString, newstr);
 | 
			
		||||
	QFETCH(OpList, operations);
 | 
			
		||||
 | 
			
		||||
	auto strToVariantList = [](const QString& str) -> QVariantList {
 | 
			
		||||
		QVariantList list;
 | 
			
		||||
 | 
			
		||||
		for (auto c: str) {
 | 
			
		||||
			list.emplace_back(c);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return list;
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	auto oldlist = strToVariantList(oldstr);
 | 
			
		||||
	auto newlist = strToVariantList(newstr);
 | 
			
		||||
 | 
			
		||||
	auto model = ScriptModel();
 | 
			
		||||
	auto modelTester = QAbstractItemModelTester(&model);
 | 
			
		||||
 | 
			
		||||
	OpList actualOperations;
 | 
			
		||||
 | 
			
		||||
	auto onInsert = [&](const QModelIndex& parent, int first, int last) {
 | 
			
		||||
		QCOMPARE(parent, QModelIndex());
 | 
			
		||||
		actualOperations << ModelOperation(ModelOperation::Insert, first, last - first + 1);
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	auto onRemove = [&](const QModelIndex& parent, int first, int last) {
 | 
			
		||||
		QCOMPARE(parent, QModelIndex());
 | 
			
		||||
		actualOperations << ModelOperation(ModelOperation::Remove, first, last - first + 1);
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	auto onMove = [&](const QModelIndex& sourceParent,
 | 
			
		||||
	                  int sourceStart,
 | 
			
		||||
	                  int sourceEnd,
 | 
			
		||||
	                  const QModelIndex& destParent,
 | 
			
		||||
	                  int destStart) {
 | 
			
		||||
		QCOMPARE(sourceParent, QModelIndex());
 | 
			
		||||
		QCOMPARE(destParent, QModelIndex());
 | 
			
		||||
		actualOperations << ModelOperation(
 | 
			
		||||
		    ModelOperation::Move,
 | 
			
		||||
		    sourceStart,
 | 
			
		||||
		    sourceEnd - sourceStart + 1,
 | 
			
		||||
		    destStart
 | 
			
		||||
		);
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	QObject::connect(&model, &QAbstractItemModel::rowsInserted, &model, onInsert);
 | 
			
		||||
	QObject::connect(&model, &QAbstractItemModel::rowsRemoved, &model, onRemove);
 | 
			
		||||
	QObject::connect(&model, &QAbstractItemModel::rowsMoved, &model, onMove);
 | 
			
		||||
 | 
			
		||||
	model.setValues(oldlist);
 | 
			
		||||
	QCOMPARE_EQ(model.values(), oldlist);
 | 
			
		||||
	QCOMPARE_EQ(
 | 
			
		||||
	    actualOperations,
 | 
			
		||||
	    OpList({{ModelOperation::Insert, 0, static_cast<qint32>(oldlist.length())}})
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	actualOperations.clear();
 | 
			
		||||
 | 
			
		||||
	model.setValues(newlist);
 | 
			
		||||
	QCOMPARE_EQ(model.values(), newlist);
 | 
			
		||||
	QCOMPARE_EQ(actualOperations, operations);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
QTEST_MAIN(TestScriptModel);
 | 
			
		||||
							
								
								
									
										37
									
								
								src/core/test/scriptmodel.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/core/test/scriptmodel.hpp
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,37 @@
 | 
			
		|||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <qdebug.h>
 | 
			
		||||
#include <qobject.h>
 | 
			
		||||
#include <qtmetamacros.h>
 | 
			
		||||
#include <qtypes.h>
 | 
			
		||||
 | 
			
		||||
struct ModelOperation {
 | 
			
		||||
	enum Enum : quint8 {
 | 
			
		||||
		Insert,
 | 
			
		||||
		Remove,
 | 
			
		||||
		Move,
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	ModelOperation(Enum operation, qint32 index, qint32 length, qint32 destIndex = -1)
 | 
			
		||||
	    : operation(operation)
 | 
			
		||||
	    , index(index)
 | 
			
		||||
	    , length(length)
 | 
			
		||||
	    , destIndex(destIndex) {}
 | 
			
		||||
 | 
			
		||||
	Enum operation;
 | 
			
		||||
	qint32 index = 0;
 | 
			
		||||
	qint32 length = 0;
 | 
			
		||||
	qint32 destIndex = -1;
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] bool operator==(const ModelOperation& other) const;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
QDebug& operator<<(QDebug& debug, const ModelOperation& op);
 | 
			
		||||
 | 
			
		||||
class TestScriptModel: public QObject {
 | 
			
		||||
	Q_OBJECT;
 | 
			
		||||
 | 
			
		||||
private slots:
 | 
			
		||||
	static void unique_data(); // NOLINT
 | 
			
		||||
	static void unique();
 | 
			
		||||
};
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue