Merge 67b2b85d32dd1190abaf49996f021f1492e28eea into be6d2251aad2e0a609f63ce8c7e46fefa858ded1

This commit is contained in:
Martín Capello 2025-02-27 15:06:56 -03:00 committed by GitHub
commit 112a7b9d42
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 610 additions and 116 deletions

2
laf

@ -1 +1 @@
Subproject commit 65829107c838817987f3cf6374cc68c583e5d538
Subproject commit 23c11583b626b36b125dbe74a2d1cde880780623

View File

@ -52,6 +52,7 @@ protected:
docPref.grid.snap(newValue);
StatusBar::instance()->showSnapToGridWarning(newValue);
StatusBar::instance()->showRunningScriptsWindow(newValue);
}
};

View File

@ -73,7 +73,7 @@ void RunScriptCommand::onExecute(Context* context)
return;
}
App::instance()->scriptEngine()->evalUserFile(m_filename, m_params);
App::instance()->scriptEngine()->evalUserFileInTask(m_filename, m_params);
if (context->isUIAvailable())
ui::Manager::getDefault()->invalidate();

View File

@ -32,6 +32,7 @@
#include "base/fs.h"
#include "base/paths.h"
#include "base/remove_from_container.h"
#include "ui/await.h"
#include "ui/box.h"
#include "ui/button.h"
#include "ui/combobox.h"
@ -125,6 +126,7 @@ struct Dialog {
// Reference used to keep the dialog alive (so it's not garbage
// collected) when it's visible.
int showRef = LUA_REFNIL;
int dlgStateRef = LUA_REFNIL;
lua_State* L = nullptr;
Dialog(const ui::Window::Type windowType, const std::string& title)
@ -152,6 +154,11 @@ struct Dialog {
this->L = L;
lua_pushvalue(L, 1);
showRef = luaL_ref(L, LUA_REGISTRYINDEX);
// Keep a reference of this thread state while the dialog is still alive
// because it will be reused when calling the event handlers to create
// new lua threads.
lua_pushthread(L);
dlgStateRef = luaL_ref(L, LUA_REGISTRYINDEX);
}
}
@ -163,8 +170,14 @@ struct Dialog {
void unrefShow()
{
if (showRef != LUA_REFNIL) {
int status = lua_status(this->L);
TRACE("status: %d %d\n", showRef, status);
// this->L
luaL_unref(this->L, LUA_REGISTRYINDEX, showRef);
luaL_unref(this->L, LUA_REGISTRYINDEX, dlgStateRef);
showRef = LUA_REFNIL;
dlgStateRef = LUA_REFNIL;
L = nullptr;
}
}
@ -301,6 +314,7 @@ struct Dialog {
template<typename... Args, typename Callback>
void Dialog_connect_signal(lua_State* L,
int dlgIdx,
std::string signalName,
obs::signal<void(Args...)>& signal,
Callback callback)
{
@ -336,18 +350,7 @@ void Dialog_connect_signal(lua_State* L,
callback(L, std::forward<Args>(args)...);
if (lua_isfunction(L, -2)) {
try {
if (lua_pcall(L, 1, 0, 0)) {
if (const char* s = lua_tostring(L, -1))
App::instance()->scriptEngine()->consolePrint(s);
}
}
catch (const std::exception& ex) {
// This is used to catch unhandled exception or for
// example, std::runtime_error exceptions when a Tx() is
// created without an active sprite.
App::instance()->scriptEngine()->handleException(ex);
}
App::instance()->scriptEngine()->callInTask(L, 1, signalName);
}
else {
lua_pop(L, 1); // Pop the value which should have been a function
@ -358,63 +361,73 @@ void Dialog_connect_signal(lua_State* L,
int Dialog_new(lua_State* L)
{
// If we don't have UI, just return nil
if (!App::instance()->isGui())
try {
return ui::await<int>(
[L]() -> int {
// If we don't have UI, just return nil
if (!App::instance()->isGui())
return 0;
// Get the title and the type of window (with or without title bar)
ui::Window::Type windowType = ui::Window::WithTitleBar;
std::string title = "Script";
if (lua_isstring(L, 1)) {
title = lua_tostring(L, 1);
}
else if (lua_istable(L, 1)) {
int type = lua_getfield(L, 1, "title");
if (type != LUA_TNIL)
title = lua_tostring(L, -1);
lua_pop(L, 1);
type = lua_getfield(L, 1, "notitlebar");
if (type != LUA_TNIL && lua_toboolean(L, -1))
windowType = ui::Window::WithoutTitleBar;
lua_pop(L, 1);
}
auto dlg = push_new<Dialog>(L, windowType, title);
// The uservalue of the dialog userdata will contain a table that
// stores all the callbacks to handle events. As these callbacks can
// reference the dialog itself, it's important to store callbacks in
// this table that depends on the dialog lifetime itself
// (i.e. uservalue) and in the global registry, because in that case
// we could create a cyclic reference that would be not GC'd.
lua_newtable(L);
lua_setuservalue(L, -2);
if (lua_istable(L, 1)) {
int type = lua_getfield(L, 1, "parent");
if (type != LUA_TNIL) {
if (auto parentDlg = may_get_obj<Dialog>(L, -1))
dlg->window.setParentDisplay(parentDlg->window.display());
}
lua_pop(L, 1);
type = lua_getfield(L, 1, "onclose");
if (type == LUA_TFUNCTION) {
Dialog_connect_signal(L, -2, "onclose", dlg->window.Close, [](lua_State*, CloseEvent&) {
// Do nothing
});
}
lua_pop(L, 1);
}
// The showRef must be the last reference to the dialog to be
// unreferenced after the window is closed (that's why this is the
// last connection to ui::Window::Close)
dlg->unrefShowOnClose();
TRACE_DIALOG("Dialog_new", dlg);
return 1;
},
500);
}
catch (AwaitTimeoutException& ex) {
luaL_error(L, "Could not create Dialog");
return 0;
// Get the title and the type of window (with or without title bar)
ui::Window::Type windowType = ui::Window::WithTitleBar;
std::string title = "Script";
if (lua_isstring(L, 1)) {
title = lua_tostring(L, 1);
}
else if (lua_istable(L, 1)) {
int type = lua_getfield(L, 1, "title");
if (type != LUA_TNIL)
title = lua_tostring(L, -1);
lua_pop(L, 1);
type = lua_getfield(L, 1, "notitlebar");
if (type != LUA_TNIL && lua_toboolean(L, -1))
windowType = ui::Window::WithoutTitleBar;
lua_pop(L, 1);
}
auto dlg = push_new<Dialog>(L, windowType, title);
// The uservalue of the dialog userdata will contain a table that
// stores all the callbacks to handle events. As these callbacks can
// reference the dialog itself, it's important to store callbacks in
// this table that depends on the dialog lifetime itself
// (i.e. uservalue) and in the global registry, because in that case
// we could create a cyclic reference that would be not GC'd.
lua_newtable(L);
lua_setuservalue(L, -2);
if (lua_istable(L, 1)) {
int type = lua_getfield(L, 1, "parent");
if (type != LUA_TNIL) {
if (auto parentDlg = may_get_obj<Dialog>(L, -1))
dlg->window.setParentDisplay(parentDlg->window.display());
}
lua_pop(L, 1);
type = lua_getfield(L, 1, "onclose");
if (type == LUA_TFUNCTION) {
Dialog_connect_signal(L, -2, dlg->window.Close, [](lua_State*, CloseEvent&) {
// Do nothing
});
}
lua_pop(L, 1);
}
// The showRef must be the last reference to the dialog to be
// unreferenced after the window is closed (that's why this is the
// last connection to ui::Window::Close)
dlg->unrefShowOnClose();
TRACE_DIALOG("Dialog_new", dlg);
return 1;
}
int Dialog_gc(lua_State* L)
@ -764,7 +777,7 @@ int Dialog_button_base(lua_State* L, T** outputWidget = nullptr)
type = lua_getfield(L, 2, "onclick");
if (type == LUA_TFUNCTION) {
Dialog_connect_signal(L, 1, widget->Click, [](lua_State*) {
Dialog_connect_signal(L, 1, "onclick", widget->Click, [](lua_State*) {
// Do nothing
});
closeWindowByDefault = false;
@ -838,7 +851,7 @@ int Dialog_entry(lua_State* L)
if (lua_istable(L, 2)) {
int type = lua_getfield(L, 2, "onchange");
if (type == LUA_TFUNCTION) {
Dialog_connect_signal(L, 1, widget->Change, [](lua_State* L) {
Dialog_connect_signal(L, 1, "onchange", widget->Change, [](lua_State* L) {
// Do nothing
});
}
@ -868,7 +881,7 @@ int Dialog_number(lua_State* L)
type = lua_getfield(L, 2, "onchange");
if (type == LUA_TFUNCTION) {
Dialog_connect_signal(L, 1, widget->Change, [](lua_State* L) {
Dialog_connect_signal(L, 1, "onchange", widget->Change, [](lua_State* L) {
// Do nothing
});
}
@ -909,7 +922,7 @@ int Dialog_slider(lua_State* L)
if (lua_istable(L, 2)) {
int type = lua_getfield(L, 2, "onchange");
if (type == LUA_TFUNCTION) {
Dialog_connect_signal(L, 1, widget->Change, [](lua_State* L) {
Dialog_connect_signal(L, 1, "onchange", widget->Change, [](lua_State* L) {
// Do nothing
});
}
@ -917,7 +930,7 @@ int Dialog_slider(lua_State* L)
type = lua_getfield(L, 2, "onrelease");
if (type == LUA_TFUNCTION) {
Dialog_connect_signal(L, 1, widget->SliderReleased, [](lua_State* L) {
Dialog_connect_signal(L, 1, "onrelease", widget->SliderReleased, [](lua_State* L) {
// Do nothing
});
}
@ -955,7 +968,7 @@ int Dialog_combobox(lua_State* L)
type = lua_getfield(L, 2, "onchange");
if (type == LUA_TFUNCTION) {
Dialog_connect_signal(L, 1, widget->Change, [](lua_State* L) {
Dialog_connect_signal(L, 1, "onchange", widget->Change, [](lua_State* L) {
// Do nothing
});
}
@ -979,10 +992,14 @@ int Dialog_color(lua_State* L)
if (lua_istable(L, 2)) {
int type = lua_getfield(L, 2, "onchange");
if (type == LUA_TFUNCTION) {
Dialog_connect_signal(L, 1, widget->Change, [](lua_State* L, const app::Color& color) {
push_obj<app::Color>(L, color);
lua_setfield(L, -2, "color");
});
Dialog_connect_signal(L,
1,
"onchange",
widget->Change,
[](lua_State* L, const app::Color& color) {
push_obj<app::Color>(L, color);
lua_setfield(L, -2, "color");
});
}
}
@ -1026,6 +1043,7 @@ int Dialog_shades(lua_State* L)
if (type == LUA_TFUNCTION) {
Dialog_connect_signal(L,
1,
"onclick",
widget->Click,
[widget](lua_State* L, ColorShades::ClickEvent& ev) {
lua_pushinteger(L, (int)ev.button());
@ -1101,7 +1119,7 @@ int Dialog_file(lua_State* L)
if (lua_istable(L, 2)) {
int type = lua_getfield(L, 2, "onchange");
if (type == LUA_TFUNCTION) {
Dialog_connect_signal(L, 1, widget->Change, [](lua_State* L) {
Dialog_connect_signal(L, 1, "onchange", widget->Change, [](lua_State* L) {
// Do nothing
});
}
@ -1276,7 +1294,7 @@ int Dialog_canvas(lua_State* L)
if (lua_istable(L, 2)) {
int type = lua_getfield(L, 2, "onpaint");
if (type == LUA_TFUNCTION) {
Dialog_connect_signal(L, 1, widget->Paint, [](lua_State* L, GraphicsContext& gc) {
Dialog_connect_signal(L, 1, "onpaint", widget->Paint, [](lua_State* L, GraphicsContext& gc) {
push_new<GraphicsContext>(L, std::move(gc));
lua_setfield(L, -2, "context");
});
@ -1285,51 +1303,55 @@ int Dialog_canvas(lua_State* L)
type = lua_getfield(L, 2, "onkeydown");
if (type == LUA_TFUNCTION) {
Dialog_connect_signal(L, 1, widget->KeyDown, fill_keymessage_values);
Dialog_connect_signal(L, 1, "onkeydown", widget->KeyDown, fill_keymessage_values);
handleKeyEvents = true;
}
lua_pop(L, 1);
type = lua_getfield(L, 2, "onkeyup");
if (type == LUA_TFUNCTION) {
Dialog_connect_signal(L, 1, widget->KeyUp, fill_keymessage_values);
Dialog_connect_signal(L, 1, "onkeyup", widget->KeyUp, fill_keymessage_values);
handleKeyEvents = true;
}
lua_pop(L, 1);
type = lua_getfield(L, 2, "onmousemove");
if (type == LUA_TFUNCTION) {
Dialog_connect_signal(L, 1, widget->MouseMove, fill_mousemessage_values);
Dialog_connect_signal(L, 1, "onmousemove", widget->MouseMove, fill_mousemessage_values);
}
lua_pop(L, 1);
type = lua_getfield(L, 2, "onmousedown");
if (type == LUA_TFUNCTION) {
Dialog_connect_signal(L, 1, widget->MouseDown, fill_mousemessage_values);
Dialog_connect_signal(L, 1, "onmousedown", widget->MouseDown, fill_mousemessage_values);
}
lua_pop(L, 1);
type = lua_getfield(L, 2, "onmouseup");
if (type == LUA_TFUNCTION) {
Dialog_connect_signal(L, 1, widget->MouseUp, fill_mousemessage_values);
Dialog_connect_signal(L, 1, "onmouseup", widget->MouseUp, fill_mousemessage_values);
}
lua_pop(L, 1);
type = lua_getfield(L, 2, "ondblclick");
if (type == LUA_TFUNCTION) {
Dialog_connect_signal(L, 1, widget->DoubleClick, fill_mousemessage_values);
Dialog_connect_signal(L, 1, "ondblclick", widget->DoubleClick, fill_mousemessage_values);
}
lua_pop(L, 1);
type = lua_getfield(L, 2, "onwheel");
if (type == LUA_TFUNCTION) {
Dialog_connect_signal(L, 1, widget->Wheel, fill_mousemessage_values);
Dialog_connect_signal(L, 1, "onwheel", widget->Wheel, fill_mousemessage_values);
}
lua_pop(L, 1);
type = lua_getfield(L, 2, "ontouchmagnify");
if (type == LUA_TFUNCTION) {
Dialog_connect_signal(L, 1, widget->TouchMagnify, fill_touchmessage_values);
Dialog_connect_signal(L,
1,
"ontouchmagnify",
widget->TouchMagnify,
fill_touchmessage_values);
}
lua_pop(L, 1);
}
@ -1382,7 +1404,7 @@ int Dialog_tab(lua_State* L)
if (lua_istable(L, 2)) {
int type = lua_getfield(L, 2, "onclick");
if (type == LUA_TFUNCTION) {
Dialog_connect_signal(L, 1, tab->Click, [id](lua_State* L) {
Dialog_connect_signal(L, 1, "onclick", tab->Click, [id](lua_State* L) {
lua_pushstring(L, id.c_str());
lua_setfield(L, -2, "tab");
});
@ -1436,7 +1458,7 @@ int Dialog_endtabs(lua_State* L)
type = lua_getfield(L, 2, "onchange");
if (type == LUA_TFUNCTION) {
auto tab = dlg->wipTab;
Dialog_connect_signal(L, 1, dlg->wipTab->TabChanged, [tab](lua_State* L) {
Dialog_connect_signal(L, 1, "onchange", dlg->wipTab->TabChanged, [tab](lua_State* L) {
lua_pushstring(L, tab->tabId(tab->selectedTab()).c_str());
lua_setfield(L, -2, "tab");
});
@ -1879,31 +1901,42 @@ int Dialog_set_bounds(lua_State* L)
return 0;
}
#define wrap(func) \
[](lua_State* L) -> int { \
try { \
return ui::await<int>([L]() -> int { return func(L); }, 500); \
} \
catch (AwaitTimeoutException & ex) { \
luaL_error(L, "Could not execute %s", #func); \
return 0; \
} \
}
const luaL_Reg Dialog_methods[] = {
{ "__gc", Dialog_gc },
{ "show", Dialog_show },
{ "showMenu", Dialog_showMenu },
{ "close", Dialog_close },
{ "newrow", Dialog_newrow },
{ "separator", Dialog_separator },
{ "label", Dialog_label },
{ "button", Dialog_button },
{ "check", Dialog_check },
{ "radio", Dialog_radio },
{ "menuItem", Dialog_menuItem },
{ "entry", Dialog_entry },
{ "number", Dialog_number },
{ "slider", Dialog_slider },
{ "combobox", Dialog_combobox },
{ "color", Dialog_color },
{ "shades", Dialog_shades },
{ "file", Dialog_file },
{ "canvas", Dialog_canvas },
{ "tab", Dialog_tab },
{ "endtabs", Dialog_endtabs },
{ "modify", Dialog_modify },
{ "repaint", Dialog_repaint },
{ nullptr, nullptr }
{ "__gc", wrap(Dialog_gc) },
{ "show", wrap(Dialog_show) },
{ "showMenu", Dialog_showMenu },
{ "close", Dialog_close },
{ "newrow", Dialog_newrow },
{ "separator", Dialog_separator },
{ "label", Dialog_label },
{ "button", wrap(Dialog_button) },
{ "check", Dialog_check },
{ "radio", Dialog_radio },
{ "menuItem", Dialog_menuItem },
{ "entry", Dialog_entry },
{ "number", Dialog_number },
{ "slider", Dialog_slider },
{ "combobox", Dialog_combobox },
{ "color", Dialog_color },
{ "shades", Dialog_shades },
{ "file", Dialog_file },
{ "canvas", Dialog_canvas },
{ "tab", Dialog_tab },
{ "endtabs", Dialog_endtabs },
{ "modify", Dialog_modify },
{ "repaint", Dialog_repaint },
{ nullptr, nullptr }
};
const Property Dialog_properties[] = {

View File

@ -35,7 +35,10 @@
#include "fmt/format.h"
#include "ui/base.h"
#include "ui/cursor_type.h"
#include "ui/manager.h"
#include "ui/message_loop.h"
#include "ui/mouse_button.h"
#include "ui/system.h"
#include <fstream>
#include <sstream>
@ -213,7 +216,100 @@ void register_websocket_class(lua_State* L);
void set_app_params(lua_State* L, const Params& params);
Engine::Engine() : L(luaL_newstate()), m_delegate(nullptr), m_printLastResult(false)
#define RUNNING_TASKS "RunningTasks"
std::mutex runningTasksMutex;
RunScriptTask::RunScriptTask(lua_State* L, int nelems, const std::string& description, Func&& func)
: m_LRef(LUA_REFNIL)
, m_description(description)
{
m_L = lua_newthread(L);
// Move the thread object in the parent thread stack to the new thread stack.
lua_xmove(L, m_L, 1);
// Save a reference to the lua thread in the registry to avoid garbage collection
// before it gets executed.
m_LRef = luaL_ref(m_L, LUA_REGISTRYINDEX);
// Move nelems stack elements from main thread to the child lua thread.
lua_xmove(L, m_L, nelems);
// We use a global table to have access to the state of all the currently
// running scripts
{
std::lock_guard<std::mutex> lock(runningTasksMutex);
int type = lua_getglobal(m_L, RUNNING_TASKS);
if (type == LUA_TNIL) {
lua_newtable(m_L);
lua_pushvalue(m_L, -1);
lua_setglobal(m_L, RUNNING_TASKS);
}
lua_pushlightuserdata(m_L, this);
lua_rawsetp(m_L, -2, m_L);
lua_pop(m_L, 1);
}
lua_sethook(
m_L,
[](lua_State* L, lua_Debug* ar) {
if (ar->event == LUA_HOOKCOUNT) {
std::lock_guard<std::mutex> lock(runningTasksMutex);
int type = lua_getglobal(L, RUNNING_TASKS);
if (type == LUA_TTABLE) {
lua_rawgetp(L, -1, L);
RunScriptTask* task = (RunScriptTask*)lua_topointer(L, -1);
lua_pop(L, 2);
if (task->wantsToStop()) {
luaL_where(L, 0);
const char* where = lua_tostring(L, -1);
luaL_traceback(L, L, "Script stopped", 0);
const char* traceback = lua_tostring(L, -1);
std::string msg(fmt::format("{}{}", where, traceback));
lua_pop(L, 2);
lua_pushstring(L, msg.c_str());
lua_error(L);
}
}
}
},
LUA_MASKCOUNT,
10);
// TODO: use the token for allowing the script to report progress somehow.
m_task.on_execute([this, func](base::task_token& token) { func(m_L); });
}
RunScriptTask::~RunScriptTask()
{
std::lock_guard<std::mutex> lock(runningTasksMutex);
int type = lua_getglobal(m_L, RUNNING_TASKS);
if (type == LUA_TTABLE) {
lua_pushlightuserdata(m_L, nullptr);
lua_rawsetp(m_L, -2, m_L);
}
lua_pop(m_L, 1);
luaL_unref(m_L, LUA_REGISTRYINDEX, m_LRef);
// Collect script garbage.
lua_gc(m_L, LUA_GCCOLLECT);
}
void RunScriptTask::execute(base::thread_pool& pool)
{
m_task.start(pool);
}
void RunScriptTask::stop()
{
m_wantsToStop = true;
}
Engine::Engine()
: L(luaL_newstate())
, m_delegate(nullptr)
, m_threadPool(3)
, m_printLastResult(false)
{
#if _DEBUG
int top = lua_gettop(L);
@ -525,6 +621,15 @@ Engine::~Engine()
void Engine::destroy()
{
// Stop all running tasks.
{
std::lock_guard<std::mutex> lock(m_mutex);
for (auto& task : m_tasks) {
task->stop();
}
}
m_threadPool.wait_all();
close_all_dialogs();
lua_close(L);
L = nullptr;
@ -594,6 +699,11 @@ bool Engine::evalCode(const std::string& code, const std::string& filename)
}
void Engine::handleException(const std::exception& ex)
{
handleException(L, ex);
}
void Engine::handleException(lua_State* L, const std::exception& ex)
{
luaL_where(L, 1);
const char* where = lua_tostring(L, -1);
@ -631,6 +741,122 @@ bool Engine::evalFile(const std::string& filename, const Params& params)
return result;
}
void Engine::callInTask(lua_State* parentL, int nargs, const std::string& description)
{
executeTask(parentL, nargs + 1, description, [this, nargs](lua_State* L) {
try {
if (lua_pcall(L, nargs, 0, 0)) {
if (const char* s = lua_tostring(L, -1)) {
std::string error = std::string(s);
ui::execute_from_ui_thread([this, error]() { consolePrint(error.c_str()); });
}
}
}
catch (const std::exception& ex) {
// This is used to catch unhandled exception or for
// example, std::runtime_error exceptions when a Tx() is
// created without an active sprite.
handleException(L, ex);
}
});
}
void Engine::executeTask(lua_State* parentL,
int nelems,
const std::string& description,
RunScriptTask::Func&& func)
{
auto task = std::make_unique<RunScriptTask>(parentL, nelems, description, std::move(func));
auto* taskPtr = task.get();
task->onDone([this, taskPtr](base::task_token&) { onTaskDone(taskPtr); });
{
std::lock_guard<std::mutex> lock(m_mutex);
m_tasks.push_back(std::move(task));
}
TaskStart(taskPtr);
taskPtr->execute(m_threadPool);
}
void Engine::onTaskDone(const RunScriptTask* task)
{
std::lock_guard<std::mutex> lock(m_mutex);
TaskDone(task);
for (auto it = m_tasks.begin(); it != m_tasks.end(); ++it) {
if ((*it).get() == task) {
m_tasks.erase(it);
break;
}
}
}
bool Engine::evalUserFileInTask(const std::string& filename, const Params& params)
{
executeTask(L, 0, filename, [filename, params, this](lua_State* L) {
std::string absFilename = base::get_absolute_path(filename);
// Set the _SCRIPT_PATH global so require() can find .lua files from
// the script path.
std::string path = base::get_file_path(absFilename);
SetScriptForRequire setScript(L, path.c_str());
bool ok = true;
std::stringstream buf;
{
std::ifstream s(FSTREAM_PATH(filename));
// Returns false if we cannot open the file
if (!s) {
ok = false;
return;
}
buf << s.rdbuf();
}
AddScriptFilename addScript(absFilename);
set_app_params(L, params);
const std::string& code = buf.str();
const std::string& atFilename = "@" + absFilename;
int returnCode;
try {
if (luaL_loadbuffer(L, code.c_str(), code.size(), atFilename.c_str()) ||
lua_pcall(L, 0, 1, 0)) {
const char* s = lua_tostring(L, -1);
if (s) {
std::string error = std::string(s);
ui::execute_from_ui_thread([this, error]() { onConsoleError(error.c_str()); });
}
ok = false;
returnCode = -1;
}
else {
// Return code
if (lua_isinteger(L, -1))
returnCode = lua_tointeger(L, -1);
else
returnCode = 0;
// Code was executed correctly
if (m_printLastResult) {
if (!lua_isnone(L, -1)) {
const char* result = lua_tostring(L, -1);
if (result)
TRACE("Result: %s\n", result);
// onConsolePrint(result);
}
}
}
lua_pop(L, 1);
}
catch (const std::exception& ex) {
// handleException(ex);
TRACE("Exception: %s\n", ex.what());
ok = false;
returnCode = -1;
}
});
return true;
}
bool Engine::evalUserFile(const std::string& filename, const Params& params)
{
// Set the _SCRIPT_PATH global so require() can find .lua files from
@ -660,6 +886,16 @@ void Engine::stopDebugger()
lua_sethook(L, nullptr, 0, 0);
}
void Engine::stopTask(const RunScriptTask* task)
{
std::lock_guard<std::mutex> lock(m_mutex);
for (const auto& t : m_tasks) {
if (t.get() == task) {
t->stop();
}
}
}
void Engine::onConsoleError(const char* text)
{
if (text && m_delegate)

View File

@ -16,6 +16,8 @@
#include "app/color.h"
#include "app/commands/params.h"
#include "app/extensions.h"
#include "base/task.h"
#include "base/thread_pool.h"
#include "base/uuid.h"
#include "doc/brush.h"
#include "doc/frame.h"
@ -84,8 +86,36 @@ public:
virtual void endFile(const std::string& file) = 0;
};
class RunScriptTask {
public:
typedef std::function<void(lua_State* L)> Func;
RunScriptTask(lua_State* L, int nelems, const std::string& description, Func&& func);
~RunScriptTask();
void onDone(base::task::func_t&& funcDone) { m_task.on_done(std::move(funcDone)); }
void execute(base::thread_pool& pool);
void stop();
bool wantsToStop() const { return m_wantsToStop; }
int ref() const { return m_LRef; }
const std::string& description() const { return m_description; }
bool isRunning() const { return m_task.running(); }
bool isEnqueued() const { return m_task.enqueued(); }
private:
// Lua's thread state.
lua_State* m_L;
int m_LRef;
base::task m_task;
bool m_wantsToStop = false;
std::string m_description;
};
class Engine {
public:
typedef std::vector<std::unique_ptr<RunScriptTask>> Tasks;
Engine();
~Engine();
@ -101,8 +131,22 @@ public:
bool evalCode(const std::string& code, const std::string& filename = std::string());
bool evalFile(const std::string& filename, const Params& params = Params());
bool evalUserFile(const std::string& filename, const Params& params = Params());
bool evalUserFileInTask(const std::string& filename, const Params& params = Params());
// Calls the function in the stack with the number of arguments specified by nargs in
// a new RunScriptTask.
void callInTask(lua_State* L, int nargs, const std::string& description);
std::vector<const RunScriptTask*> tasks() const
{
std::lock_guard<std::mutex> lock(m_mutex);
std::vector<const RunScriptTask*> tasks;
tasks.reserve(m_tasks.size());
for (const auto& task : m_tasks)
tasks.push_back(task.get());
return tasks;
}
void handleException(const std::exception& ex);
void handleException(lua_State* L, const std::exception& ex);
void consolePrint(const char* text) { onConsolePrint(text); }
@ -112,13 +156,29 @@ public:
void startDebugger(DebuggerDelegate* debuggerDelegate);
void stopDebugger();
// Stops the specified task
void stopTask(const RunScriptTask* task);
obs::signal<void(const RunScriptTask*)> TaskStart;
obs::signal<void(const RunScriptTask*)> TaskDone;
private:
void onConsoleError(const char* text);
void onConsolePrint(const char* text);
// Creates a new RunScriptTask based on parentL and moving nelems from parentL's stack
// to the child lua thread stack
void executeTask(lua_State* parentL,
int nelems,
const std::string& description,
RunScriptTask::Func&& func);
void onTaskDone(const RunScriptTask* task);
static void checkProgress(lua_State* L, lua_Debug* ar);
lua_State* L;
EngineDelegate* m_delegate;
base::thread_pool m_threadPool;
mutable std::mutex m_mutex;
Tasks m_tasks;
bool m_printLastResult;
int m_returnCode;
};

View File

@ -22,6 +22,7 @@
#include "app/modules/gui.h"
#include "app/modules/palettes.h"
#include "app/pref/preferences.h"
#include "app/script/engine.h"
#include "app/tools/active_tool.h"
#include "app/tools/tool.h"
#include "app/ui/button_set.h"
@ -44,8 +45,16 @@
#include "fmt/format.h"
#include "gfx/size.h"
#include "os/surface.h"
#include "os/system.h"
#include "text/font.h"
#include "ui/box.h"
#include "ui/button.h"
#include "ui/label.h"
#include "ui/listbox.h"
#include "ui/listitem.h"
#include "ui/popup_window.h"
#include "ui/ui.h"
#include "ui/view.h"
#include "ver/info.h"
#include <algorithm>
@ -607,6 +616,70 @@ private:
ui::Button m_button;
};
class StatusBar::RunningScriptsWindow : public ui::Window {
public:
// TODO: Replace the title by a string
RunningScriptsWindow() : ui::Window(Type::WithTitleBar, "Running scripts")
{
setMinSize({ 500, 200 });
m_runningScripts.setExpansive(true);
m_view.setExpansive(true);
m_view.attachToView(&m_runningScripts);
addChild(&m_view);
initTheme();
refreshList();
App::instance()->scriptEngine()->TaskStart.connect([this](const script::RunScriptTask*) {
ui::execute_from_ui_thread([this] { refreshList(); });
});
App::instance()->scriptEngine()->TaskDone.connect([this](const script::RunScriptTask*) {
ui::execute_from_ui_thread([this] { refreshList(); });
});
}
void refreshList()
{
m_runningScripts.removeAllChildren();
for (const auto* task : App::instance()->scriptEngine()->tasks()) {
auto* item = new TaskItem(task);
m_runningScripts.addChild(item);
}
m_runningScripts.layout();
flushRedraw();
}
private:
class TaskItem : public ListItem {
public:
TaskItem(const script::RunScriptTask* task) : m_label(""), m_stop("Stop")
{
m_task = task;
m_label.setText(
fmt::format("{} ({})", task->description(), (task->isEnqueued() ? "enqueued" : "running")));
m_label.setExpansive(true);
m_row.setExpansive(true);
m_row.addChild(&m_label);
if (!task->isEnqueued())
m_row.addChild(&m_stop);
addChild(&m_row);
m_stop.Click.connect([this]() { App::instance()->scriptEngine()->stopTask(m_task); });
}
private:
const script::RunScriptTask* m_task;
ui::HBox m_row;
ui::Label m_label;
ui::Button m_stop;
};
ui::View m_view;
ui::ListBox m_runningScripts;
};
// This widget is used to show the current frame.
class GotoFrameEntry : public Entry {
public:
@ -886,6 +959,25 @@ void StatusBar::showSnapToGridWarning(bool state)
}
}
void StatusBar::showRunningScriptsWindow(bool state)
{
if (state) {
if (!m_runningScriptsWindow)
m_runningScriptsWindow = new RunningScriptsWindow;
m_runningScriptsWindow->setDisplay(display(), false);
if (!m_runningScriptsWindow->isVisible()) {
m_runningScriptsWindow->openWindow();
m_runningScriptsWindow->remapWindow();
// updateRunningScriptsWindowPosition();
}
}
else if (m_runningScriptsWindow) {
m_runningScriptsWindow->closeWindow(nullptr);
}
}
//////////////////////////////////////////////////////////////////////
// StatusBar message handler

View File

@ -66,6 +66,7 @@ public:
void showTile(int msecs, doc::tile_t tile, const std::string& text = std::string());
void showTool(int msecs, tools::Tool* tool);
void showSnapToGridWarning(bool state);
void showRunningScriptsWindow(bool state);
// Used by AppEditor to update the zoom level in the status bar.
void updateFromEditor(Editor* editor);
@ -119,6 +120,9 @@ private:
// Snap to grid window
class SnapToGridWindow;
SnapToGridWindow* m_snapToGridWindow;
class RunningScriptsWindow;
RunningScriptsWindow* m_runningScriptsWindow;
};
} // namespace app

68
src/ui/await.h Normal file
View File

@ -0,0 +1,68 @@
// Aseprite UI Library
// Copyright (C) 2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#ifndef UI_AWAIT_H_INCLUDED
#define UI_AWAIT_H_INCLUDED
#pragma once
#include "os/event.h"
#include "os/event_queue.h"
#include "ui/system.h"
#include <functional>
#include <future>
namespace ui {
class AwaitTimeoutException : std::runtime_error {
public:
AwaitTimeoutException() : std::runtime_error("Await timed out") {}
};
// Awaits for the future result of the function passed.
// It is meant to be used from background threads to ask something to the main
// thread and wait for its result.
// If await is called from the main thread it just executes the function
// and returns the result.
template<typename T>
T await(std::function<T()>&& func, int timeout = 0)
{
if (ui::is_ui_thread())
return func();
std::promise<T> promise;
std::future<T> future = promise.get_future();
// Queue the event
os::Event ev;
ev.setType(os::Event::Callback);
ev.setCallback([func, &promise]() {
try {
if constexpr (std::is_void<T>())
func();
else
promise.set_value(func());
}
catch (...) {
promise.set_exception(std::current_exception());
}
});
os::queue_event(ev);
// Wait for the future
if (timeout > 0) {
auto status = future.wait_for(std::chrono::milliseconds(timeout));
if (status != std::future_status::ready) {
throw AwaitTimeoutException();
}
}
return future.get();
}
} // namespace ui
#endif