diff --git a/src/platform/qt/CMakeLists.txt b/src/platform/qt/CMakeLists.txt index 0dcfbaefb..72a569fc3 100644 --- a/src/platform/qt/CMakeLists.txt +++ b/src/platform/qt/CMakeLists.txt @@ -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) diff --git a/src/platform/qt/CheckBoxDelegate.cpp b/src/platform/qt/CheckBoxDelegate.cpp index bd6854ebe..442dba214 100644 --- a/src/platform/qt/CheckBoxDelegate.cpp +++ b/src/platform/qt/CheckBoxDelegate.cpp @@ -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 { diff --git a/src/platform/qt/ConfigController.cpp b/src/platform/qt/ConfigController.cpp index 5b65ac2ea..005f4dafc 100644 --- a/src/platform/qt/ConfigController.cpp +++ b/src/platform/qt/ConfigController.cpp @@ -123,7 +123,9 @@ QString ConfigController::s_configDir; ConfigController::ConfigController(QObject* parent) : QObject(parent) { - qRegisterMetaType(); +#ifdef ENABLE_SCRIPTING + AutorunScriptModel::registerMetaTypes(); +#endif QString fileName = configDir(); fileName.append(QDir::separator()); diff --git a/src/platform/qt/Window.cpp b/src/platform/qt/Window.cpp index 4632ca558..049079b87 100644 --- a/src/platform/qt/Window.cpp +++ b/src/platform/qt/Window.cpp @@ -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 Window::addGameAction(const QString& visibleName, const QString& name, Action::Function function, const QString& menu, const QKeySequence& shortcut) { diff --git a/src/platform/qt/scripting/AutorunScriptModel.cpp b/src/platform/qt/scripting/AutorunScriptModel.cpp index de8a7a06b..486e2f402 100644 --- a/src/platform/qt/scripting/AutorunScriptModel.cpp +++ b/src/platform/qt/scripting/AutorunScriptModel.cpp @@ -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 +#include + +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(); +#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) + qRegisterMetaTypeStreamOperators("QGBA::AutorunScriptModel::ScriptInfo"); +#endif +} + +AutorunScriptModel::AutorunScriptModel(QObject* parent) : QAbstractListModel(parent) - , m_config(config) { - QList autorun = m_config->getList("autorunSettings"); - for (const auto& item: autorun) { + // Nothing to do +} + +void AutorunScriptModel::deserialize(const QList& autorun) { + for (const auto& item : autorun) { if (!item.canConvert()) { continue; } + ScriptInfo info = qvariant_cast(item); + if (info.filename.isEmpty()) { + continue; + } m_scripts.append(qvariant_cast(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::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 AutorunScriptModel::activeScripts() const { - QList scripts; +QStringList AutorunScriptModel::activeScripts() const { + QStringList scripts; for (const auto& pair: m_scripts) { if (!pair.active) { continue; @@ -123,10 +177,14 @@ QList AutorunScriptModel::activeScripts() const { return scripts; } -void AutorunScriptModel::save() { +QList AutorunScriptModel::serialize() const { QList 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()); } diff --git a/src/platform/qt/scripting/AutorunScriptModel.h b/src/platform/qt/scripting/AutorunScriptModel.h index f269d2798..5896c19e3 100644 --- a/src/platform/qt/scripting/AutorunScriptModel.h +++ b/src/platform/qt/scripting/AutorunScriptModel.h @@ -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& autorun); + QList 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 activeScripts() const; + QStringList activeScripts() const; + +signals: + void scriptsChanged(const QList& serialized); private: - ConfigController* m_config; QList 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); diff --git a/src/platform/qt/scripting/AutorunScriptView.cpp b/src/platform/qt/scripting/AutorunScriptView.cpp index 10d3cfb25..997399d5e 100644 --- a/src/platform/qt/scripting/AutorunScriptView.cpp +++ b/src/platform/qt/scripting/AutorunScriptView.cpp @@ -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); } diff --git a/src/platform/qt/scripting/ScriptingController.cpp b/src/platform/qt/scripting/ScriptingController.cpp index 3c2d8fa4d..6f61ffd12 100644 --- a/src/platform/qt/scripting/ScriptingController.cpp +++ b/src/platform/qt/scripting/ScriptingController.cpp @@ -30,8 +30,12 @@ using namespace QGBA; ScriptingController::ScriptingController(ConfigController* config, QObject* parent) : QObject(parent) - , m_model(config) + , m_config(config) { + QList 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(log); @@ -505,3 +509,7 @@ uint16_t ScriptingController::qtToScriptingModifiers(Qt::KeyboardModifiers modif } return mod; } + +void ScriptingController::saveAutorun(const QList& autorun) { + m_config->setList("autorunSettings", autorun); +} diff --git a/src/platform/qt/scripting/ScriptingController.h b/src/platform/qt/scripting/ScriptingController.h index 8e2b111f9..6e938d450 100644 --- a/src/platform/qt/scripting/ScriptingController.h +++ b/src/platform/qt/scripting/ScriptingController.h @@ -75,6 +75,7 @@ protected: private slots: void updateGamepad(); void attach(); + void saveAutorun(const QList& autorun); private: void init(); @@ -101,6 +102,7 @@ private: AutorunScriptModel m_model; std::shared_ptr m_controller; InputController* m_inputController = nullptr; + ConfigController* m_config = nullptr; QTimer m_storageFlush; }; diff --git a/src/platform/qt/test/autoscript.cpp b/src/platform/qt/test/autoscript.cpp new file mode 100644 index 000000000..70d212a6d --- /dev/null +++ b/src/platform/qt/test/autoscript.cpp @@ -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 +#endif + +#include +#include +#include + +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))); + } + + 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(); + QCOMPARE(first.filename, "foo"); + QCOMPARE(first.active, true); + auto second = data[1].value(); + 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"