Qt: Add basic scripting view

This commit is contained in:
Vicki Pfau 2022-05-13 00:42:50 -07:00
parent 48f49b74b1
commit fa847b1e63
10 changed files with 453 additions and 5 deletions

View File

@ -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)

View File

@ -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());
}

View File

@ -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;
};
}

View File

@ -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);
}

View File

@ -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;
};
}

View File

@ -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(";;");
}

View File

@ -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;
};
}

View File

@ -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>

View File

@ -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");

View File

@ -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 {