diff --git a/src/platform/qt/CMakeLists.txt b/src/platform/qt/CMakeLists.txt index 4399bfcfb..f45414220 100644 --- a/src/platform/qt/CMakeLists.txt +++ b/src/platform/qt/CMakeLists.txt @@ -247,11 +247,21 @@ if(USE_DISCORD_RPC) list(APPEND SOURCE_FILES DiscordCoordinator.cpp) endif() +if(ENABLE_SCRIPTING) + list(APPEND SOURCE_FILES + ScriptingController.cpp + ScriptingView.cpp) + + list(APPEND UI_FILES + ScriptingView.ui) +endif() + if(TARGET Qt6::Core) qt_add_resources(RESOURCES resources.qrc) else() qt5_add_resources(RESOURCES resources.qrc) endif() + if(BUILD_UPDATER) file(GENERATE OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/updater.qrc INPUT ${CMAKE_CURRENT_SOURCE_DIR}/updater.qrc.in) if(TARGET Qt6::Core) diff --git a/src/platform/qt/LogWidget.cpp b/src/platform/qt/LogWidget.cpp index 136df178c..4b562b3b0 100644 --- a/src/platform/qt/LogWidget.cpp +++ b/src/platform/qt/LogWidget.cpp @@ -11,14 +11,58 @@ using namespace QGBA; +QTextCharFormat LogWidget::s_warn; +QTextCharFormat LogWidget::s_error; +QTextCharFormat LogWidget::s_prompt; + LogWidget::LogWidget(QWidget* parent) - : QTextEdit(parent) + : QPlainTextEdit(parent) { setFont(GBAApp::app()->monospaceFont()); + + QPalette palette = QApplication::palette(); + s_warn.setFontWeight(QFont::DemiBold); + s_warn.setFontItalic(true); + s_warn.setForeground(Qt::yellow); + s_warn.setBackground(QColor(255, 255, 0, 64)); + s_error.setFontWeight(QFont::Bold); + s_error.setForeground(Qt::red); + s_error.setBackground(QColor(255, 0, 0, 64)); + s_prompt.setForeground(palette.brush(QPalette::Disabled, QPalette::Text)); } void LogWidget::log(const QString& line) { moveCursor(QTextCursor::End); - insertPlainText(line); + textCursor().insertText(line, {}); + if (m_newlineTerminated) { + textCursor().insertText("\n"); + } + verticalScrollBar()->setValue(verticalScrollBar()->maximum()); +} + +void LogWidget::warn(const QString& line) { + moveCursor(QTextCursor::End); + textCursor().insertText(WARN_PREFIX + line, s_warn); + if (m_newlineTerminated) { + textCursor().insertText("\n"); + } + verticalScrollBar()->setValue(verticalScrollBar()->maximum()); +} + +void LogWidget::error(const QString& line) { + moveCursor(QTextCursor::End); + textCursor().insertText(ERROR_PREFIX + line, s_error); + if (m_newlineTerminated) { + textCursor().insertText("\n"); + } + verticalScrollBar()->setValue(verticalScrollBar()->maximum()); +} + +void LogWidget::echo(const QString& line) { + moveCursor(QTextCursor::End); + textCursor().insertText(PROMPT_PREFIX + line, s_prompt); + if (m_newlineTerminated) { + textCursor().insertText("\n"); + } verticalScrollBar()->setValue(verticalScrollBar()->maximum()); } diff --git a/src/platform/qt/LogWidget.h b/src/platform/qt/LogWidget.h index 700f7aaf6..d193a318c 100644 --- a/src/platform/qt/LogWidget.h +++ b/src/platform/qt/LogWidget.h @@ -5,16 +5,36 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #pragma once -#include +#include namespace QGBA { -class LogWidget : public QTextEdit { +class LogHighlighter; + +class LogWidget : public QPlainTextEdit { +Q_OBJECT + public: + static constexpr const char* WARN_PREFIX = "[WARNING] "; + static constexpr const char* ERROR_PREFIX = "[ERROR] "; + static constexpr const char* PROMPT_PREFIX = "> "; + LogWidget(QWidget* parent = nullptr); + void setNewlineTerminated(bool newlineTerminated) { m_newlineTerminated = newlineTerminated; } + public slots: void log(const QString&); + void warn(const QString&); + void error(const QString&); + void echo(const QString&); + +private: + static QTextCharFormat s_warn; + static QTextCharFormat s_error; + static QTextCharFormat s_prompt; + + bool m_newlineTerminated = false; }; } diff --git a/src/platform/qt/ScriptingController.cpp b/src/platform/qt/ScriptingController.cpp new file mode 100644 index 000000000..21289c6ee --- /dev/null +++ b/src/platform/qt/ScriptingController.cpp @@ -0,0 +1,102 @@ +/* Copyright (c) 2013-2022 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 "ScriptingController.h" + +#include "CoreController.h" + +using namespace QGBA; + +ScriptingController::ScriptingController(QObject* parent) + : QObject(parent) +{ + mScriptContextInit(&m_scriptContext); + mScriptContextAttachStdlib(&m_scriptContext); + mScriptContextRegisterEngines(&m_scriptContext); + + m_logger.p = this; + m_logger.log = [](mLogger* log, int, enum mLogLevel level, const char* format, va_list args) { + Logger* logger = static_cast(log); + va_list argc; + va_copy(argc, args); + QString message = QString::vasprintf(format, argc); + va_end(argc); + switch (level) { + case mLOG_WARN: + emit logger->p->warn(message); + break; + case mLOG_ERROR: + emit logger->p->error(message); + break; + default: + emit logger->p->log(message); + break; + } + }; + + mScriptContextAttachLogger(&m_scriptContext, &m_logger); + + HashTableEnumerate(&m_scriptContext.engines, [](const char* key, void* engine, void* context) { + ScriptingController* self = static_cast(context); + self->m_engines[QString::fromUtf8(key)] = static_cast(engine); + }, this); + + if (m_engines.count() == 1) { + m_activeEngine = *m_engines.begin(); + } +} + +ScriptingController::~ScriptingController() { + clearController(); + mScriptContextDeinit(&m_scriptContext); +} + +void ScriptingController::setController(std::shared_ptr controller) { + if (controller == m_controller) { + return; + } + clearController(); + m_controller = controller; + CoreController::Interrupter interrupter(m_controller); + m_controller->thread()->scriptContext = &m_scriptContext; + if (m_controller->hasStarted()) { + mScriptContextAttachCore(&m_scriptContext, m_controller->thread()->core); + } + connect(m_controller.get(), &CoreController::stopping, this, &ScriptingController::clearController); +} + +bool ScriptingController::loadFile(const QString& path) { + VFileDevice vf(path, QIODevice::ReadOnly); + return load(vf); +} + +bool ScriptingController::load(VFileDevice& vf) { + if (!m_activeEngine) { + return false; + } + CoreController::Interrupter interrupter(m_controller); + if (!m_activeEngine->load(m_activeEngine, vf) || !m_activeEngine->run(m_activeEngine)) { + emit error(QString::fromUtf8(m_activeEngine->getError(m_activeEngine))); + return false; + } + return true; +} + +void ScriptingController::clearController() { + if (!m_controller) { + return; + } + { + CoreController::Interrupter interrupter(m_controller); + mScriptContextDetachCore(&m_scriptContext); + m_controller->thread()->scriptContext = nullptr; + } + m_controller.reset(); +} + +void ScriptingController::runCode(const QString& code) { + VFileDevice vf(code.toUtf8()); + load(vf); +} diff --git a/src/platform/qt/ScriptingController.h b/src/platform/qt/ScriptingController.h new file mode 100644 index 000000000..3e6afffcc --- /dev/null +++ b/src/platform/qt/ScriptingController.h @@ -0,0 +1,57 @@ +/* Copyright (c) 2013-2022 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/. */ +#pragma once + +#include +#include + +#include +#include + +#include "VFileDevice.h" + +#include + +namespace QGBA { + +class CoreController; +class ScriptingController : public QObject { +Q_OBJECT + +public: + ScriptingController(QObject* parent = nullptr); + ~ScriptingController(); + + void setController(std::shared_ptr controller); + + bool loadFile(const QString& path); + bool load(VFileDevice& vf); + + mScriptContext* context() { return &m_scriptContext; } + +signals: + void log(const QString&); + void warn(const QString&); + void error(const QString&); + +public slots: + void clearController(); + void runCode(const QString& code); + +private: + struct Logger : mLogger { + ScriptingController* p; + } m_logger{}; + + mScriptContext m_scriptContext; + + mScriptEngineContext* m_activeEngine = nullptr; + QHash m_engines; + + std::shared_ptr m_controller; +}; + +} diff --git a/src/platform/qt/ScriptingView.cpp b/src/platform/qt/ScriptingView.cpp new file mode 100644 index 000000000..eb8dac8fe --- /dev/null +++ b/src/platform/qt/ScriptingView.cpp @@ -0,0 +1,51 @@ +/* Copyright (c) 2013-2022 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 "ScriptingView.h" + +#include "GBAApp.h" +#include "ScriptingController.h" + +using namespace QGBA; + +ScriptingView::ScriptingView(ScriptingController* controller, QWidget* parent) + : QMainWindow(parent) + , m_controller(controller) +{ + m_ui.setupUi(this); + + m_ui.prompt->setFont(GBAApp::app()->monospaceFont()); + m_ui.log->setNewlineTerminated(true); + + connect(m_ui.prompt, &QLineEdit::returnPressed, this, &ScriptingView::submitRepl); + connect(m_ui.runButton, &QAbstractButton::clicked, this, &ScriptingView::submitRepl); + connect(m_controller, &ScriptingController::log, m_ui.log, &LogWidget::log); + connect(m_controller, &ScriptingController::warn, m_ui.log, &LogWidget::warn); + connect(m_controller, &ScriptingController::error, m_ui.log, &LogWidget::error); + + connect(m_ui.load, &QAction::triggered, this, &ScriptingView::load); +} + +void ScriptingView::submitRepl() { + m_ui.log->echo(m_ui.prompt->text()); + m_controller->runCode(m_ui.prompt->text()); + m_ui.prompt->clear(); +} + +void ScriptingView::load() { + QString filename = GBAApp::app()->getOpenFileName(this, tr("Select script to load"), getFilters()); + if (!filename.isEmpty()) { + m_controller->loadFile(filename); + } +} + +QString ScriptingView::getFilters() const { + QStringList filters; +#ifdef USE_LUA + filters.append(tr("Lua scripts (*.lua)")); +#endif + filters.append(tr("All files (*.*)")); + return filters.join(";;"); +} diff --git a/src/platform/qt/ScriptingView.h b/src/platform/qt/ScriptingView.h new file mode 100644 index 000000000..969f51c62 --- /dev/null +++ b/src/platform/qt/ScriptingView.h @@ -0,0 +1,31 @@ +/* Copyright (c) 2013-2022 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/. */ +#pragma once + +#include "ui_ScriptingView.h" + +namespace QGBA { + +class ScriptingController; + +class ScriptingView : public QMainWindow { +Q_OBJECT + +public: + ScriptingView(ScriptingController* controller, QWidget* parent = nullptr); + +private slots: + void submitRepl(); + void load(); + +private: + QString getFilters() const; + Ui::ScriptingView m_ui; + + ScriptingController* m_controller; +}; + +} diff --git a/src/platform/qt/ScriptingView.ui b/src/platform/qt/ScriptingView.ui new file mode 100644 index 000000000..32ea124cf --- /dev/null +++ b/src/platform/qt/ScriptingView.ui @@ -0,0 +1,97 @@ + + + ScriptingView + + + + 0 + 0 + 800 + 600 + + + + Scripting + + + + + + + + + + Run + + + + + + + + 0 + 0 + + + + true + + + + + + + + 0 + 0 + + + + + 180 + 16777215 + + + + + Console + + + + + + + + + + 0 + 0 + 800 + 29 + + + + + File + + + + + + + + + Load script... + + + + + + QGBA::LogWidget + QPlainTextEdit +
LogWidget.h
+
+
+ + +
diff --git a/src/platform/qt/Window.cpp b/src/platform/qt/Window.cpp index 77ad13fb5..128435831 100644 --- a/src/platform/qt/Window.cpp +++ b/src/platform/qt/Window.cpp @@ -51,6 +51,7 @@ #include "ReportView.h" #include "ROMInfo.h" #include "SaveConverter.h" +#include "ScriptingView.h" #include "SensorView.h" #include "ShaderSelector.h" #include "ShortcutController.h" @@ -653,6 +654,19 @@ void Window::consoleOpen() { } #endif +#ifdef ENABLE_SCRIPTING +void Window::scriptingOpen() { + if (!m_scripting) { + m_scripting = std::make_unique(); + if (m_controller) { + m_scripting->setController(m_controller); + } + } + ScriptingView* view = new ScriptingView(m_scripting.get()); + openView(view); +} +#endif + void Window::keyPressEvent(QKeyEvent* event) { if (event->isAutoRepeat()) { QWidget::keyPressEvent(event); @@ -1642,15 +1656,20 @@ void Window::setupMenu(QMenuBar* menubar) { m_actions.addAction(tr("Settings..."), "settings", this, &Window::openSettingsWindow, "tools")->setRole(Action::Role::SETTINGS); m_actions.addAction(tr("Make portable"), "makePortable", this, &Window::tryMakePortable, "tools"); -#ifdef USE_DEBUGGERS m_actions.addSeparator("tools"); +#ifdef USE_DEBUGGERS m_actions.addAction(tr("Open debugger console..."), "debuggerWindow", this, &Window::consoleOpen, "tools"); #ifdef USE_GDB_STUB Action* gdbWindow = addGameAction(tr("Start &GDB server..."), "gdbWindow", this, &Window::gdbOpen, "tools"); m_platformActions.insert(mPLATFORM_GBA, gdbWindow); #endif #endif +#ifdef ENABLE_SCRIPTING + m_actions.addAction(tr("Scripting..."), "scripting", this, &Window::scriptingOpen, "tools"); +#endif +#if defined(USE_DEBUGGERS) || defined(ENABLE_SCRIPTING) m_actions.addSeparator("tools"); +#endif addGameAction(tr("View &palette..."), "paletteWindow", openControllerTView(), "tools"); addGameAction(tr("View &sprites..."), "spriteWindow", openControllerTView(), "tools"); @@ -2099,6 +2118,12 @@ void Window::setController(CoreController* controller, const QString& fname) { m_pendingPatch = QString(); } +#ifdef ENABLE_SCRIPTING + if (m_scripting) { + m_scripting->setController(m_controller); + } +#endif + attachDisplay(); m_controller->loadConfig(m_config); m_config->updateOption("showOSD"); diff --git a/src/platform/qt/Window.h b/src/platform/qt/Window.h index 6cc68150a..667cf77ca 100644 --- a/src/platform/qt/Window.h +++ b/src/platform/qt/Window.h @@ -23,6 +23,9 @@ #include "LoadSaveState.h" #include "LogController.h" #include "SettingsView.h" +#ifdef ENABLE_SCRIPTING +#include "ScriptingController.h" +#endif namespace QGBA { @@ -113,6 +116,10 @@ public slots: void gdbOpen(); #endif +#ifdef ENABLE_SCRIPTING + void scriptingOpen(); +#endif + protected: virtual void keyPressEvent(QKeyEvent* event) override; virtual void keyReleaseEvent(QKeyEvent* event) override; @@ -252,6 +259,10 @@ private: #ifdef USE_SQLITE3 LibraryController* m_libraryView; #endif + +#ifdef ENABLE_SCRIPTING + std::unique_ptr m_scripting; +#endif }; class WindowBackground : public QWidget {