Qt: improve AutorunScriptModel, add unit tests

This commit is contained in:
Adam Higerd 2025-04-28 22:24:30 -05:00 committed by Vicki Pfau
parent 8c6a8cd63f
commit a3a8b3a8f8
10 changed files with 297 additions and 48 deletions

View File

@ -307,6 +307,10 @@ if(ENABLE_SCRIPTING)
list(APPEND UI_FILES
scripting/AutorunScriptView.ui
scripting/ScriptingView.ui)
set(TEST_QT_autoscript_SRC
test/autoscript.cpp
scripting/AutorunScriptModel.cpp)
endif()
if(TARGET Qt6::Core)

View File

@ -13,7 +13,7 @@ using namespace QGBA;
CheckBoxDelegate::CheckBoxDelegate(QObject* parent)
: QStyledItemDelegate(parent)
{
// initializers only
// Nothing to do
}
void CheckBoxDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const {

View File

@ -123,7 +123,9 @@ QString ConfigController::s_configDir;
ConfigController::ConfigController(QObject* parent)
: QObject(parent)
{
qRegisterMetaType<AutorunScriptModel::ScriptInfo>();
#ifdef ENABLE_SCRIPTING
AutorunScriptModel::registerMetaTypes();
#endif
QString fileName = configDir();
fileName.append(QDir::separator());

View File

@ -554,10 +554,12 @@ void Window::openSettingsWindow(SettingsView::Page page) {
#ifdef USE_SQLITE3
connect(settingsWindow, &SettingsView::libraryCleared, m_libraryView, &LibraryController::clear);
#endif
#ifdef ENABLE_SCRIPTING
connect(settingsWindow, &SettingsView::openAutorunScripts, this, [this]() {
ensureScripting();
m_scripting->openAutorunEdit();
});
#endif
connect(this, &Window::shaderSelectorAdded, settingsWindow, &SettingsView::setShaderSelector);
openView(settingsWindow);
settingsWindow->selectPage(page);
@ -2056,6 +2058,7 @@ void Window::updateMRU() {
}
void Window::ensureScripting() {
#ifdef ENABLE_SCRIPTING
if (m_scripting) {
return;
}
@ -2072,6 +2075,7 @@ void Window::ensureScripting() {
}
connect(m_scripting.get(), &ScriptingController::autorunScriptsOpened, this, &Window::openView);
#endif
}
std::shared_ptr<Action> Window::addGameAction(const QString& visibleName, const QString& name, Action::Function function, const QString& menu, const QKeySequence& shortcut) {

View File

@ -5,22 +5,59 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "scripting/AutorunScriptModel.h"
#include "ConfigController.h"
#include "LogController.h"
#include <QtDebug>
#include <iostream>
QDataStream& operator<<(QDataStream& stream, const QGBA::AutorunScriptModel::ScriptInfo& object) {
stream << QGBA::AutorunScriptModel::ScriptInfo::VERSION;
stream << object.filename.toUtf8();
stream << object.active;
return stream;
}
QDataStream& operator>>(QDataStream& stream, QGBA::AutorunScriptModel::ScriptInfo& object) {
uint16_t version = 0;
stream >> version;
if (version == 1) {
QByteArray filename;
stream >> filename;
object.filename = QString::fromUtf8(filename);
} else {
qCritical() << QGBA::AutorunScriptModel::tr("Could not load autorun script settings: unknown script info format %1").arg(version);
stream.setStatus(QDataStream::ReadCorruptData);
return stream;
}
stream >> object.active;
return stream;
}
using namespace QGBA;
AutorunScriptModel::AutorunScriptModel(ConfigController* config, QObject* parent)
void AutorunScriptModel::registerMetaTypes() {
qRegisterMetaType<AutorunScriptModel::ScriptInfo>();
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
qRegisterMetaTypeStreamOperators<AutorunScriptModel::ScriptInfo>("QGBA::AutorunScriptModel::ScriptInfo");
#endif
}
AutorunScriptModel::AutorunScriptModel(QObject* parent)
: QAbstractListModel(parent)
, m_config(config)
{
QList<QVariant> autorun = m_config->getList("autorunSettings");
for (const auto& item: autorun) {
// Nothing to do
}
void AutorunScriptModel::deserialize(const QList<QVariant>& autorun) {
for (const auto& item : autorun) {
if (!item.canConvert<ScriptInfo>()) {
continue;
}
ScriptInfo info = qvariant_cast<ScriptInfo>(item);
if (info.filename.isEmpty()) {
continue;
}
m_scripts.append(qvariant_cast<ScriptInfo>(item));
}
emitScriptsChanged();
}
int AutorunScriptModel::rowCount(const QModelIndex& parent) const {
@ -32,13 +69,14 @@ int AutorunScriptModel::rowCount(const QModelIndex& parent) const {
bool AutorunScriptModel::setData(const QModelIndex& index, const QVariant& data, int role) {
if (!index.isValid() || index.parent().isValid() || index.row() >= m_scripts.count()) {
return {};
return false;
}
switch (role) {
case Qt::CheckStateRole:
m_scripts[index.row()].active = data.value<Qt::CheckState>() == Qt::Checked;
save();
emit dataChanged(index, index);
emitScriptsChanged();
return true;
}
return false;
@ -61,23 +99,27 @@ QVariant AutorunScriptModel::data(const QModelIndex& index, int role) const {
Qt::ItemFlags AutorunScriptModel::flags(const QModelIndex& index) const {
if (!index.isValid() || index.parent().isValid()) {
return Qt::NoItemFlags;
return Qt::ItemIsDropEnabled;
}
return Qt::ItemIsSelectable | Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemNeverHasChildren;
return Qt::ItemIsSelectable | Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemNeverHasChildren | Qt::ItemIsDragEnabled;
}
Qt::DropActions AutorunScriptModel::supportedDropActions() const {
return Qt::CopyAction | Qt::MoveAction;
}
bool AutorunScriptModel::removeRows(int row, int count, const QModelIndex& parent) {
if (parent.isValid()) {
return false;
}
if (m_scripts.size() < row) {
int lastRow = row + count - 1;
if (row < 0 || lastRow >= m_scripts.size() || count < 0) {
return false;
}
if (m_scripts.size() < row + count) {
count = m_scripts.size() - row;
}
beginRemoveRows(QModelIndex(), row, lastRow);
m_scripts.erase(m_scripts.begin() + row, m_scripts.begin() + row + count);
save();
endRemoveRows();
emitScriptsChanged();
return true;
}
@ -86,22 +128,34 @@ bool AutorunScriptModel::moveRows(const QModelIndex& sourceParent, int sourceRow
return false;
}
if (sourceRow < 0 || destinationChild < 0) {
if (sourceRow < 0 || destinationChild < 0 || count <= 0) {
return false;
}
if (sourceRow >= m_scripts.size() || destinationChild >= m_scripts.size()) {
int lastSource = sourceRow + count - 1;
if (lastSource >= m_scripts.size() || destinationChild > m_scripts.size()) {
return false;
}
if (count > 1) {
LOG(QT, WARN) << tr("Moving more than one row at once is not yet supported");
if (sourceRow == destinationChild - 1) {
return false;
}
auto item = m_scripts.takeAt(sourceRow);
m_scripts.insert(destinationChild, item);
save();
bool ok = beginMoveRows(QModelIndex(), sourceRow, lastSource, QModelIndex(), destinationChild);
if (!ok) {
return false;
}
if (destinationChild < sourceRow) {
sourceRow = lastSource;
} else {
destinationChild -= 1;
}
for (int i = 0; i < count; i++) {
m_scripts.move(sourceRow, destinationChild);
}
endMoveRows();
emitScriptsChanged();
return true;
}
@ -109,11 +163,11 @@ void AutorunScriptModel::addScript(const QString& filename) {
beginInsertRows({}, m_scripts.count(), m_scripts.count());
m_scripts.append(ScriptInfo { filename, true });
endInsertRows();
save();
emitScriptsChanged();
}
QList<QString> AutorunScriptModel::activeScripts() const {
QList<QString> scripts;
QStringList AutorunScriptModel::activeScripts() const {
QStringList scripts;
for (const auto& pair: m_scripts) {
if (!pair.active) {
continue;
@ -123,10 +177,14 @@ QList<QString> AutorunScriptModel::activeScripts() const {
return scripts;
}
void AutorunScriptModel::save() {
QList<QVariant> AutorunScriptModel::serialize() const {
QList<QVariant> list;
for (const auto& script : m_scripts) {
list.append(QVariant::fromValue(script));
}
m_config->setList("autorunSettings", list);
return list;
}
void AutorunScriptModel::emitScriptsChanged() {
emit scriptsChanged(serialize());
}

View File

@ -17,42 +17,42 @@ Q_OBJECT
public:
struct ScriptInfo {
static const uint16_t VERSION = 1;
QString filename;
bool active;
friend QDataStream& operator<<(QDataStream& stream, const ScriptInfo& object) {
stream << object.filename;
stream << object.active;
return stream;
}
friend QDataStream& operator>>(QDataStream& stream, ScriptInfo& object) {
stream >> object.filename;
stream >> object.active;
return stream;
}
};
AutorunScriptModel(ConfigController* config, QObject* parent = nullptr);
static void registerMetaTypes();
AutorunScriptModel(QObject* parent = nullptr);
void deserialize(const QList<QVariant>& autorun);
QList<QVariant> serialize() const;
virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override;
virtual bool setData(const QModelIndex& index, const QVariant& data, int role = Qt::DisplayRole) override;
virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
virtual Qt::ItemFlags flags(const QModelIndex& index) const override;
virtual Qt::DropActions supportedDropActions() const override;
virtual bool removeRows(int row, int count, const QModelIndex& parent = QModelIndex()) override;
virtual bool moveRows(const QModelIndex& sourceParent, int sourceRow, int count, const QModelIndex& destinationParent, int destinationChild) override;
void addScript(const QString& filename);
QList<QString> activeScripts() const;
QStringList activeScripts() const;
signals:
void scriptsChanged(const QList<QVariant>& serialized);
private:
ConfigController* m_config;
QList<ScriptInfo> m_scripts;
void save();
void emitScriptsChanged();
};
}
Q_DECLARE_METATYPE(QGBA::AutorunScriptModel::ScriptInfo);
QDataStream& operator<<(QDataStream& stream, const QGBA::AutorunScriptModel::ScriptInfo& object);
QDataStream& operator>>(QDataStream& stream, QGBA::AutorunScriptModel::ScriptInfo& object);

View File

@ -18,6 +18,10 @@ AutorunScriptView::AutorunScriptView(AutorunScriptModel* model, ScriptingControl
m_ui.setupUi(this);
m_ui.autorunList->setModel(model);
m_ui.autorunList->setDragEnabled(true);
m_ui.autorunList->viewport()->setAcceptDrops(true);
m_ui.autorunList->setDropIndicatorShown(true);
m_ui.autorunList->setDragDropMode(QAbstractItemView::InternalMove);
}
void AutorunScriptView::addScript() {
@ -48,5 +52,5 @@ void AutorunScriptView::moveUp() {
void AutorunScriptView::moveDown() {
QModelIndex index = m_ui.autorunList->currentIndex();
QAbstractItemModel* model = m_ui.autorunList->model();
model->moveRows(index.parent(), index.row(), 1, index.parent(), index.row() + 1);
model->moveRows(index.parent(), index.row(), 1, index.parent(), index.row() + 2);
}

View File

@ -30,8 +30,12 @@ using namespace QGBA;
ScriptingController::ScriptingController(ConfigController* config, QObject* parent)
: QObject(parent)
, m_model(config)
, m_config(config)
{
QList<QVariant> autorun = m_config->getList("autorunSettings");
m_model.deserialize(autorun);
QObject::connect(&m_model, &AutorunScriptModel::scriptsChanged, this, &ScriptingController::saveAutorun);
m_logger.p = this;
m_logger.log = [](mLogger* log, int, enum mLogLevel level, const char* format, va_list args) {
Logger* logger = static_cast<Logger*>(log);
@ -505,3 +509,7 @@ uint16_t ScriptingController::qtToScriptingModifiers(Qt::KeyboardModifiers modif
}
return mod;
}
void ScriptingController::saveAutorun(const QList<QVariant>& autorun) {
m_config->setList("autorunSettings", autorun);
}

View File

@ -75,6 +75,7 @@ protected:
private slots:
void updateGamepad();
void attach();
void saveAutorun(const QList<QVariant>& autorun);
private:
void init();
@ -101,6 +102,7 @@ private:
AutorunScriptModel m_model;
std::shared_ptr<CoreController> m_controller;
InputController* m_inputController = nullptr;
ConfigController* m_config = nullptr;
QTimer m_storageFlush;
};

View File

@ -0,0 +1,167 @@
/* Copyright (c) 2013-2025 Jeffrey Pfau
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "platform/qt/scripting/AutorunScriptModel.h"
#if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)
#include <QAbstractItemModelTester>
#endif
#include <QDataStream>
#include <QSignalSpy>
#include <QTest>
using namespace QGBA;
class AutorunScriptModelTest : public QObject {
Q_OBJECT
private:
AutorunScriptModel* model = nullptr;
QSignalSpy* spy = nullptr;
void addEntries(bool deactivateSecond) {
model->addScript("foo");
model->addScript("bar");
model->addScript("baz");
QCOMPARE(spy->count(), 3);
if (deactivateSecond) {
bool ok = model->setData(model->index(1, 0), Qt::Unchecked, Qt::CheckStateRole);
QCOMPARE(ok, true);
}
}
void checkScripts(const QStringList& names) {
int count = names.size();
for (int i = 0; i < count; i++) {
QCOMPARE(model->data(model->index(i, 0)), names[i]);
}
}
QVariantList parseRawData(const char* source, int size) {
QByteArray rawData(source, size);
QVariantList data;
QDataStream ds(&rawData, QIODevice::ReadOnly);
ds >> data;
return data;
}
private slots:
void initTestCase() {
AutorunScriptModel::registerMetaTypes();
}
void init() {
model = new AutorunScriptModel(nullptr);
#if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)
new QAbstractItemModelTester(model, QAbstractItemModelTester::FailureReportingMode::QtTest, model);
#endif
spy = new QSignalSpy(model, SIGNAL(scriptsChanged(QList<QVariant>)));
}
void cleanup() {
delete model;
model = nullptr;
delete spy;
spy = nullptr;
}
void testAdd() {
addEntries(false);
QCOMPARE(model->rowCount(), 3);
}
void testEdit() {
addEntries(true);
int before = spy->count();
bool ok = model->setData(model->index(0, 0), Qt::Unchecked, Qt::CheckStateRole);
QCOMPARE(ok, true);
QCOMPARE(model->data(model->index(0, 0), Qt::CheckStateRole), Qt::Unchecked);
QCOMPARE(model->activeScripts(), (QStringList{ "baz" }));
QCOMPARE(spy->count() - before, 1);
}
void testMoveUp() {
addEntries(true);
int before = spy->count();
bool ok = model->moveRow(QModelIndex(), 1, QModelIndex(), 0);
QCOMPARE(ok, true);
checkScripts({ "bar", "foo", "baz" });
QCOMPARE(model->data(model->index(0, 0), Qt::CheckStateRole), Qt::Unchecked);
QCOMPARE(spy->count() - before, 1);
}
void testMoveDown() {
addEntries(true);
int before = spy->count();
bool ok = model->moveRow(QModelIndex(), 1, QModelIndex(), 3);
QCOMPARE(ok, true);
checkScripts({ "foo", "baz", "bar" });
QCOMPARE(model->data(model->index(2, 0), Qt::CheckStateRole), Qt::Unchecked);
QCOMPARE(spy->count() - before, 1);
}
void testRemove() {
addEntries(false);
int before = spy->count();
bool ok = model->removeRow(1);
QCOMPARE(ok, true);
checkScripts({ "foo", "baz" });
QCOMPARE(model->activeScripts(), (QStringList{ "foo", "baz" }));
QCOMPARE(spy->count() - before, 1);
}
void testSerialize() {
addEntries(true);
int before = spy->count();
auto data = model->serialize();
QCOMPARE(data.size(), 3);
auto first = data.first().value<AutorunScriptModel::ScriptInfo>();
QCOMPARE(first.filename, "foo");
QCOMPARE(first.active, true);
auto second = data[1].value<AutorunScriptModel::ScriptInfo>();
QCOMPARE(second.filename, "bar");
QCOMPARE(second.active, false);
QCOMPARE(spy->count() - before, 0);
}
void testDeserialize() {
QVariantList data;
data << QVariant::fromValue(AutorunScriptModel::ScriptInfo{ "foo", true });
data << QVariant::fromValue(AutorunScriptModel::ScriptInfo{ "bar", false });
QByteArray rawData;
QDataStream ds(&rawData, QIODevice::WriteOnly);
ds << data;
model->deserialize(data);
QCOMPARE(model->rowCount(), 2);
checkScripts({ "foo", "bar" });
QCOMPARE(model->data(model->index(0, 0), Qt::CheckStateRole), Qt::Checked);
QCOMPARE(model->data(model->index(1, 0), Qt::CheckStateRole), Qt::Unchecked);
QCOMPARE(spy->count(), 1);
}
void testDeserializeInvalid() {
static const char v0Data[] =
"\0\0\0\1"
"\0\0\4\0\0\0\0\0%QGBA::AutorunScriptModel::ScriptInfo\0\0\0\0\0\3foo\1";
model->deserialize(parseRawData(v0Data, sizeof(v0Data)));
QCOMPARE(model->rowCount(), 0);
}
void testDeserializeV1() {
static const char v1Data[] =
"\0\0\0\2"
"\0\0\4\0\0\0\0\0%QGBA::AutorunScriptModel::ScriptInfo\0\0\1\0\0\0\3foo\1"
"\0\0\4\0\0\0\0\0%QGBA::AutorunScriptModel::ScriptInfo\0\0\1\0\0\0\3bar\0";
model->deserialize(parseRawData(v1Data, sizeof(v1Data)));
QCOMPARE(model->rowCount(), 2);
checkScripts({ "foo", "bar" });
QCOMPARE(model->data(model->index(0, 0), Qt::CheckStateRole), Qt::Checked);
QCOMPARE(model->data(model->index(1, 0), Qt::CheckStateRole), Qt::Unchecked);
}
};
QTEST_MAIN(AutorunScriptModelTest)
#include "autoscript.moc"