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
				
			
		| 
						 | 
					@ -18,6 +18,7 @@ Checks: >
 | 
				
			||||||
  -cppcoreguidelines-pro-bounds-array-to-pointer-decay,
 | 
					  -cppcoreguidelines-pro-bounds-array-to-pointer-decay,
 | 
				
			||||||
  -cppcoreguidelines-avoid-do-while,
 | 
					  -cppcoreguidelines-avoid-do-while,
 | 
				
			||||||
  -cppcoreguidelines-pro-type-reinterpret-cast,
 | 
					  -cppcoreguidelines-pro-type-reinterpret-cast,
 | 
				
			||||||
 | 
					  -cppcoreguidelines-pro-type-vararg,
 | 
				
			||||||
  google-global-names-in-headers,
 | 
					  google-global-names-in-headers,
 | 
				
			||||||
  google-readability-casting,
 | 
					  google-readability-casting,
 | 
				
			||||||
  google-runtime-int,
 | 
					  google-runtime-int,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -36,6 +36,7 @@ qt_add_library(quickshell-core STATIC
 | 
				
			||||||
	instanceinfo.cpp
 | 
						instanceinfo.cpp
 | 
				
			||||||
	common.cpp
 | 
						common.cpp
 | 
				
			||||||
	iconprovider.cpp
 | 
						iconprovider.cpp
 | 
				
			||||||
 | 
						scriptmodel.cpp
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
qt_add_qml_module(quickshell-core
 | 
					qt_add_qml_module(quickshell-core
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,6 +2,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include <qabstractitemmodel.h>
 | 
					#include <qabstractitemmodel.h>
 | 
				
			||||||
#include <qhash.h>
 | 
					#include <qhash.h>
 | 
				
			||||||
 | 
					#include <qnamespace.h>
 | 
				
			||||||
#include <qobject.h>
 | 
					#include <qobject.h>
 | 
				
			||||||
#include <qqmllist.h>
 | 
					#include <qqmllist.h>
 | 
				
			||||||
#include <qtmetamacros.h>
 | 
					#include <qtmetamacros.h>
 | 
				
			||||||
| 
						 | 
					@ -14,11 +15,13 @@ qint32 UntypedObjectModel::rowCount(const QModelIndex& parent) const {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
QVariant UntypedObjectModel::data(const QModelIndex& index, qint32 role) 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()));
 | 
						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() {
 | 
					QQmlListProperty<QObject> UntypedObjectModel::values() {
 | 
				
			||||||
	return QQmlListProperty<QObject>(
 | 
						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(transformwatcher transformwatcher.cpp)
 | 
				
			||||||
qs_test(ringbuffer ringbuf.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