mirror of https://github.com/mgba-emu/mgba.git
Qt: Add basic scripting view
This commit is contained in:
parent
48f49b74b1
commit
fa847b1e63
|
@ -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)
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -5,16 +5,36 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
#pragma once
|
||||
|
||||
#include <QTextEdit>
|
||||
#include <QPlainTextEdit>
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -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<Logger*>(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<ScriptingController*>(context);
|
||||
self->m_engines[QString::fromUtf8(key)] = static_cast<mScriptEngineContext*>(engine);
|
||||
}, this);
|
||||
|
||||
if (m_engines.count() == 1) {
|
||||
m_activeEngine = *m_engines.begin();
|
||||
}
|
||||
}
|
||||
|
||||
ScriptingController::~ScriptingController() {
|
||||
clearController();
|
||||
mScriptContextDeinit(&m_scriptContext);
|
||||
}
|
||||
|
||||
void ScriptingController::setController(std::shared_ptr<CoreController> 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);
|
||||
}
|
|
@ -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 <QHash>
|
||||
#include <QObject>
|
||||
|
||||
#include <mgba/script/context.h>
|
||||
#include <mgba/core/scripting.h>
|
||||
|
||||
#include "VFileDevice.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace QGBA {
|
||||
|
||||
class CoreController;
|
||||
class ScriptingController : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ScriptingController(QObject* parent = nullptr);
|
||||
~ScriptingController();
|
||||
|
||||
void setController(std::shared_ptr<CoreController> 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<QString, mScriptEngineContext*> m_engines;
|
||||
|
||||
std::shared_ptr<CoreController> m_controller;
|
||||
};
|
||||
|
||||
}
|
|
@ -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(";;");
|
||||
}
|
|
@ -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;
|
||||
};
|
||||
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>ScriptingView</class>
|
||||
<widget class="QMainWindow" name="ScriptingView">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>800</width>
|
||||
<height>600</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Scripting</string>
|
||||
</property>
|
||||
<widget class="QWidget" name="centralwidget">
|
||||
<layout class="QGridLayout" name="gridLayout" columnstretch="0,1,0">
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="prompt"/>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QPushButton" name="runButton">
|
||||
<property name="text">
|
||||
<string>Run</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1" colspan="2">
|
||||
<widget class="QGBA::LogWidget" name="log">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0" rowspan="2">
|
||||
<widget class="QListWidget" name="loadedScripts">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>180</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Console</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QMenuBar" name="menubar">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>800</width>
|
||||
<height>29</height>
|
||||
</rect>
|
||||
</property>
|
||||
<widget class="QMenu" name="menuFile">
|
||||
<property name="title">
|
||||
<string>File</string>
|
||||
</property>
|
||||
<addaction name="load"/>
|
||||
</widget>
|
||||
<addaction name="menuFile"/>
|
||||
</widget>
|
||||
<widget class="QStatusBar" name="statusbar"/>
|
||||
<action name="load">
|
||||
<property name="text">
|
||||
<string>Load script...</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>QGBA::LogWidget</class>
|
||||
<extends>QPlainTextEdit</extends>
|
||||
<header>LogWidget.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
|
@ -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<ScriptingController>();
|
||||
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<PaletteView>(), "tools");
|
||||
addGameAction(tr("View &sprites..."), "spriteWindow", openControllerTView<ObjView>(), "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");
|
||||
|
|
|
@ -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<ScriptingController> m_scripting;
|
||||
#endif
|
||||
};
|
||||
|
||||
class WindowBackground : public QWidget {
|
||||
|
|
Loading…
Reference in New Issue