mirror of
https://github.com/WarmUpTill/SceneSwitcher.git
synced 2026-11-15 03:36:53 -06:00
Safeguard for rework to support nested groups. Two things to change with rework: * Dropping onto a collapsed group appends instead of prepending * Group running state is not updated right away when there is a collapsed group above it for some reason
286 lines
8.2 KiB
C++
286 lines
8.2 KiB
C++
#include "ui-test-helpers.hpp"
|
|
#include "catch.hpp"
|
|
|
|
#include <QApplication>
|
|
#include <QMouseEvent>
|
|
#include <QTimer>
|
|
#include <QWidget>
|
|
#include <QWindow>
|
|
|
|
namespace advss::test {
|
|
|
|
void ProcessEvents()
|
|
{
|
|
for (int i = 0; i < 3; i++) {
|
|
QCoreApplication::processEvents();
|
|
QCoreApplication::sendPostedEvents(nullptr,
|
|
QEvent::DeferredDelete);
|
|
}
|
|
}
|
|
|
|
static void sendMouseEvent(QWidget *widget, QEvent::Type type,
|
|
const QPoint &pos, Qt::MouseButton button,
|
|
Qt::MouseButtons buttons,
|
|
Qt::KeyboardModifiers modifiers)
|
|
{
|
|
auto topLevel = widget->window();
|
|
auto window = topLevel->windowHandle();
|
|
Q_ASSERT(window);
|
|
const QPointF windowPos = widget->mapTo(topLevel, pos);
|
|
const QPointF globalPos = widget->mapToGlobal(pos);
|
|
QMouseEvent event(type, windowPos, windowPos, globalPos, button,
|
|
buttons, modifiers);
|
|
QApplication::sendEvent(window, &event);
|
|
}
|
|
|
|
void MousePress(QWidget *widget, const QPoint &pos,
|
|
Qt::KeyboardModifiers modifiers)
|
|
{
|
|
sendMouseEvent(widget, QEvent::MouseButtonPress, pos, Qt::LeftButton,
|
|
Qt::LeftButton, modifiers);
|
|
}
|
|
|
|
void MouseRelease(QWidget *widget, const QPoint &pos,
|
|
Qt::KeyboardModifiers modifiers)
|
|
{
|
|
sendMouseEvent(widget, QEvent::MouseButtonRelease, pos, Qt::LeftButton,
|
|
Qt::NoButton, modifiers);
|
|
}
|
|
|
|
void MouseMove(QWidget *widget, const QPoint &pos, Qt::MouseButtons buttons,
|
|
Qt::KeyboardModifiers modifiers)
|
|
{
|
|
sendMouseEvent(widget, QEvent::MouseMove, pos, Qt::NoButton, buttons,
|
|
modifiers);
|
|
}
|
|
|
|
void Click(QWidget *widget, const QPoint &pos, Qt::KeyboardModifiers modifiers)
|
|
{
|
|
MousePress(widget, pos, modifiers);
|
|
MouseRelease(widget, pos, modifiers);
|
|
ProcessEvents();
|
|
}
|
|
|
|
void DoubleClick(QWidget *widget, const QPoint &pos)
|
|
{
|
|
MousePress(widget, pos);
|
|
MouseRelease(widget, pos);
|
|
sendMouseEvent(widget, QEvent::MouseButtonDblClick, pos, Qt::LeftButton,
|
|
Qt::LeftButton, Qt::NoModifier);
|
|
MouseRelease(widget, pos);
|
|
ProcessEvents();
|
|
}
|
|
|
|
bool DragAndDropSupported()
|
|
{
|
|
// QOffscreenDrag::drag() ignores all drags
|
|
return QGuiApplication::platformName() != "offscreen";
|
|
}
|
|
|
|
void DragAndDrop(QWidget *widget, const QPoint &from, const QPoint &to)
|
|
{
|
|
if (!DragAndDropSupported()) {
|
|
FAIL("drag and drop is not supported by the Qt platform");
|
|
}
|
|
|
|
MousePress(widget, from);
|
|
// The first move puts the view into dragging state, the second one
|
|
// starts the drag. QDrag::exec() then runs its own event loop, so the
|
|
// rest of the gesture has to be delivered from within that loop.
|
|
MouseMove(widget, from + QPoint(0, 1), Qt::LeftButton);
|
|
QTimer::singleShot(0, widget, [widget, to]() {
|
|
MouseMove(widget, to, Qt::LeftButton);
|
|
MouseMove(widget, to, Qt::LeftButton);
|
|
MouseRelease(widget, to);
|
|
});
|
|
const int distance = QApplication::startDragDistance() + 5;
|
|
MouseMove(widget, from + QPoint(0, distance), Qt::LeftButton);
|
|
ProcessEvents();
|
|
}
|
|
|
|
ModelChecker::ModelChecker(QAbstractItemModel *model, int nameRole)
|
|
: QObject(model),
|
|
_model(model),
|
|
_nameRole(nameRole)
|
|
{
|
|
connect(model, &QAbstractItemModel::rowsAboutToBeInserted, this,
|
|
&ModelChecker::RowsAboutToBeInserted);
|
|
connect(model, &QAbstractItemModel::rowsInserted, this,
|
|
&ModelChecker::RowsInserted);
|
|
connect(model, &QAbstractItemModel::rowsAboutToBeRemoved, this,
|
|
&ModelChecker::RowsAboutToBeRemoved);
|
|
connect(model, &QAbstractItemModel::rowsRemoved, this,
|
|
&ModelChecker::RowsRemoved);
|
|
connect(model, &QAbstractItemModel::rowsAboutToBeMoved, this,
|
|
&ModelChecker::RowsAboutToBeMoved);
|
|
connect(model, &QAbstractItemModel::rowsMoved, this,
|
|
&ModelChecker::RowsMoved);
|
|
connect(model, &QAbstractItemModel::modelReset, this,
|
|
[this]() { CheckStructure(); });
|
|
connect(model, &QAbstractItemModel::layoutChanged, this,
|
|
[this]() { CheckStructure(); });
|
|
CheckStructure();
|
|
}
|
|
|
|
void ModelChecker::CheckStructure()
|
|
{
|
|
CheckStructure(QModelIndex(), 0);
|
|
}
|
|
|
|
void ModelChecker::CheckStructure(const QModelIndex &parent, int depth)
|
|
{
|
|
if (depth > 100) {
|
|
Error("model structure too deep - cycle?");
|
|
return;
|
|
}
|
|
|
|
const int count = _model->rowCount(parent);
|
|
for (int row = 0; row < count; row++) {
|
|
const auto index = _model->index(row, 0, parent);
|
|
if (!index.isValid()) {
|
|
Error(QString("invalid index at row %1 below \"%2\"")
|
|
.arg(row)
|
|
.arg(Name(parent))
|
|
.toStdString());
|
|
continue;
|
|
}
|
|
if (_model->parent(index) != parent) {
|
|
Error(QString("parent of \"%1\" does not match \"%2\"")
|
|
.arg(Name(index))
|
|
.arg(Name(parent))
|
|
.toStdString());
|
|
}
|
|
if (Name(index).isEmpty()) {
|
|
Error(QString("no name at row %1 below \"%2\"")
|
|
.arg(row)
|
|
.arg(Name(parent))
|
|
.toStdString());
|
|
}
|
|
CheckStructure(index, depth + 1);
|
|
}
|
|
}
|
|
|
|
QString ModelChecker::Name(const QModelIndex &index) const
|
|
{
|
|
if (!index.isValid()) {
|
|
return "<root>";
|
|
}
|
|
return _model->data(index, _nameRole).toString();
|
|
}
|
|
|
|
void ModelChecker::Error(const std::string &message)
|
|
{
|
|
_errors.emplace_back(message);
|
|
}
|
|
|
|
void ModelChecker::RowsAboutToBeInserted(const QModelIndex &parent, int, int)
|
|
{
|
|
_pending = {};
|
|
_pending.parent = parent;
|
|
_pending.oldCount = _model->rowCount(parent);
|
|
}
|
|
|
|
void ModelChecker::RowsInserted(const QModelIndex &parent, int first, int last)
|
|
{
|
|
const int expected = _pending.oldCount + last - first + 1;
|
|
const int actual = _model->rowCount(parent);
|
|
if (actual != expected) {
|
|
Error(QString("insert below \"%1\": expected %2 rows, got %3")
|
|
.arg(Name(parent))
|
|
.arg(expected)
|
|
.arg(actual)
|
|
.toStdString());
|
|
}
|
|
CheckStructure();
|
|
}
|
|
|
|
void ModelChecker::RowsAboutToBeRemoved(const QModelIndex &parent, int first,
|
|
int last)
|
|
{
|
|
_pending = {};
|
|
_pending.parent = parent;
|
|
_pending.oldCount = _model->rowCount(parent);
|
|
if (last >= _pending.oldCount || first < 0) {
|
|
Error(QString("remove below \"%1\": rows %2-%3 out of range %4")
|
|
.arg(Name(parent))
|
|
.arg(first)
|
|
.arg(last)
|
|
.arg(_pending.oldCount)
|
|
.toStdString());
|
|
}
|
|
}
|
|
|
|
void ModelChecker::RowsRemoved(const QModelIndex &parent, int first, int last)
|
|
{
|
|
const int expected = _pending.oldCount - (last - first + 1);
|
|
const int actual = _model->rowCount(parent);
|
|
if (actual != expected) {
|
|
Error(QString("remove below \"%1\": expected %2 rows, got %3")
|
|
.arg(Name(parent))
|
|
.arg(expected)
|
|
.arg(actual)
|
|
.toStdString());
|
|
}
|
|
CheckStructure();
|
|
}
|
|
|
|
void ModelChecker::RowsAboutToBeMoved(const QModelIndex &sourceParent,
|
|
int sourceStart, int sourceEnd,
|
|
const QModelIndex &destinationParent, int)
|
|
{
|
|
_pending = {};
|
|
_pending.parent = sourceParent;
|
|
_pending.oldCount = _model->rowCount(sourceParent);
|
|
_pending.destinationParent = destinationParent;
|
|
_pending.oldDestinationCount = _model->rowCount(destinationParent);
|
|
for (int row = sourceStart; row <= sourceEnd; row++) {
|
|
_pending.names << Name(_model->index(row, 0, sourceParent));
|
|
}
|
|
}
|
|
|
|
void ModelChecker::RowsMoved(const QModelIndex &sourceParent, int sourceStart,
|
|
int sourceEnd,
|
|
const QModelIndex &destinationParent,
|
|
int destinationRow)
|
|
{
|
|
const int count = sourceEnd - sourceStart + 1;
|
|
const bool sameParent = sourceParent == destinationParent;
|
|
int newStart = destinationRow;
|
|
if (sameParent && destinationRow > sourceEnd) {
|
|
newStart -= count;
|
|
}
|
|
|
|
QStringList names;
|
|
for (int row = newStart; row < newStart + count; row++) {
|
|
names << Name(_model->index(row, 0, destinationParent));
|
|
}
|
|
if (names != _pending.names) {
|
|
Error(QString("move to \"%1\" row %2: expected [%3], got [%4]")
|
|
.arg(Name(destinationParent))
|
|
.arg(destinationRow)
|
|
.arg(_pending.names.join(", "))
|
|
.arg(names.join(", "))
|
|
.toStdString());
|
|
}
|
|
|
|
if (!sameParent) {
|
|
const int sourceCount = _model->rowCount(sourceParent);
|
|
const int destinationCount =
|
|
_model->rowCount(destinationParent);
|
|
if (sourceCount != _pending.oldCount - count ||
|
|
destinationCount != _pending.oldDestinationCount + count) {
|
|
Error(QString("move from \"%1\" to \"%2\": row counts do not match")
|
|
.arg(Name(sourceParent))
|
|
.arg(Name(destinationParent))
|
|
.toStdString());
|
|
}
|
|
} else if (_model->rowCount(sourceParent) != _pending.oldCount) {
|
|
Error(QString("move within \"%1\": row count changed")
|
|
.arg(Name(sourceParent))
|
|
.toStdString());
|
|
}
|
|
CheckStructure();
|
|
}
|
|
|
|
} // namespace advss::test
|