From 85f3164e21014a7697f29ddb91e32307f7c96cb6 Mon Sep 17 00:00:00 2001 From: lampysprites Date: Wed, 29 Sep 2021 22:39:01 +0700 Subject: [PATCH 01/23] Expose DocObserver to the scripts --- src/app/script/sprite_class.cpp | 72 +++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/src/app/script/sprite_class.cpp b/src/app/script/sprite_class.cpp index ee9c16390..4c7ec610a 100644 --- a/src/app/script/sprite_class.cpp +++ b/src/app/script/sprite_class.cpp @@ -27,11 +27,14 @@ #include "app/color_spaces.h" #include "app/commands/commands.h" #include "app/commands/params.h" +#include "app/console.h" #include "app/context.h" #include "app/doc.h" #include "app/doc_access.h" #include "app/doc_api.h" #include "app/doc_range.h" +#include "app/doc_undo.h" +#include "app/doc_undo_observer.h" #include "app/file/palette_file.h" #include "app/script/docobj.h" #include "app/script/engine.h" @@ -57,6 +60,43 @@ namespace script { namespace { +class ScriptDocObserver : public DocUndoObserver { +public: + ScriptDocObserver(int callbackRef) + : DocUndoObserver(), + m_callbackRef(callbackRef) + { } + + void onAddUndoState(DocUndo* history) { callback(); } + void onDeleteUndoState(DocUndo* history, undo::UndoState* state) { } + void onCurrentUndoStateChange(DocUndo* history) { callback(); } + void onClearRedo(DocUndo* history) { } + void onTotalUndoSizeChange(DocUndo* history) { } + +private: + void callback() { + script::Engine* engine = App::instance()->scriptEngine(); + lua_State* L = engine->luaState(); + + lua_rawgeti(L, LUA_REGISTRYINDEX, m_callbackRef); + if (lua_pcall(L, 0, 0, 0)) { + if (const char* s = lua_tostring(L, -1)) { + Console().printf("Error: %s", s); + } + } + } + + int m_callbackRef; +}; + +const luaL_Reg ScriptDocObserver_methods[] = { + { nullptr, nullptr } +}; + +const Property ScriptDocObserver_properties[] = { + { nullptr, nullptr, nullptr } +}; + int Sprite_new(lua_State* L) { std::unique_ptr doc; @@ -589,6 +629,32 @@ int Sprite_deleteSlice(lua_State* L) } } +int Sprite_watch(lua_State* L) +{ + auto sprite = get_docobj(L, 1); + auto doc = static_cast(sprite->document()); + ScriptDocObserver* obs; + + if (lua_isfunction(L, 2)) { + int callbackRef = luaL_ref(L, LUA_REGISTRYINDEX); + obs = push_new(L, callbackRef); + doc->undoHistory()->add_observer(obs); + return 1; + } + return 0; +} + +int Sprite_unwatch(lua_State* L) +{ + if (auto obs = may_get_obj(L, 2)) { + auto sprite = get_docobj(L, 1); + auto doc = static_cast(sprite->document()); + + doc->undoHistory()->remove_observer(obs); + } + return 0; +} + int Sprite_get_filename(lua_State* L) { auto sprite = get_docobj(L, 1); @@ -821,6 +887,9 @@ const luaL_Reg Sprite_methods[] = { // Slices { "newSlice", Sprite_newSlice }, { "deleteSlice", Sprite_deleteSlice }, + // Observer + { "watch", Sprite_watch }, + { "unwatch", Sprite_unwatch }, { nullptr, nullptr } }; @@ -849,6 +918,7 @@ const Property Sprite_properties[] = { } // anonymous namespace DEF_MTNAME(doc::Sprite); +DEF_MTNAME(ScriptDocObserver); void register_sprite_class(lua_State* L) { @@ -856,6 +926,8 @@ void register_sprite_class(lua_State* L) REG_CLASS(L, Sprite); REG_CLASS_NEW(L, Sprite); REG_CLASS_PROPERTIES(L, Sprite); + + REG_CLASS(L, ScriptDocObserver); } } // namespace script From e6aeee64009e5c65b897e7e64d8e8b469e0634f8 Mon Sep 17 00:00:00 2001 From: lampysprites Date: Thu, 30 Sep 2021 13:13:43 +0700 Subject: [PATCH 02/23] Add IXWEbSocket library --- .gitmodules | 3 +++ src/app/CMakeLists.txt | 2 +- third_party/CMakeLists.txt | 4 ++++ third_party/IXWebSocket | 1 + 4 files changed, 9 insertions(+), 1 deletion(-) create mode 160000 third_party/IXWebSocket diff --git a/.gitmodules b/.gitmodules index 780aaf1c3..3609f710a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -66,3 +66,6 @@ [submodule "src/tga"] path = src/tga url = https://github.com/aseprite/tga.git +[submodule "third_party/IXWebSocket"] + path = third_party/IXWebSocket + url = https://github.com/machinezone/IXWebSocket diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index ecda68224..64598f68e 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -673,7 +673,7 @@ if(REQUIRE_CURL) endif() if(ENABLE_SCRIPTING) - target_link_libraries(app-lib lua lauxlib lualib) + target_link_libraries(app-lib lua lauxlib lualib ixwebsocket) endif() if(ENABLE_UPDATER) diff --git a/third_party/CMakeLists.txt b/third_party/CMakeLists.txt index dad788916..4592f5518 100644 --- a/third_party/CMakeLists.txt +++ b/third_party/CMakeLists.txt @@ -159,4 +159,8 @@ if(ENABLE_SCRIPTING) target_include_directories(lauxlib PUBLIC lua) target_include_directories(lualib PUBLIC lua) target_link_libraries(lauxlib lua) + +# ixwebsocket +add_subdirectory(ixwebsocket) +target_include_directories(ixwebsocket PUBLIC) endif() diff --git a/third_party/IXWebSocket b/third_party/IXWebSocket new file mode 160000 index 000000000..9bbd1f1b3 --- /dev/null +++ b/third_party/IXWebSocket @@ -0,0 +1 @@ +Subproject commit 9bbd1f1b30bfa6fb52bf1388ab336ef39d0faead From b0f10ee276403e3ab2966946b28009e9c716077d Mon Sep 17 00:00:00 2001 From: lampysprites Date: Thu, 30 Sep 2021 22:13:16 +0700 Subject: [PATCH 03/23] Add Websocket client API --- src/app/CMakeLists.txt | 1 + src/app/script/engine.cpp | 2 + src/app/script/websocket_class.cpp | 180 +++++++++++++++++++++++++++++ 3 files changed, 183 insertions(+) create mode 100644 src/app/script/websocket_class.cpp diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 64598f68e..de55dae70 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -189,6 +189,7 @@ if(ENABLE_SCRIPTING) script/tool_class.cpp script/values.cpp script/version_class.cpp + script/websocket_class.cpp shell.cpp ${scripting_files_ui}) endif() diff --git a/src/app/script/engine.cpp b/src/app/script/engine.cpp index ce1cfc655..5417bb435 100644 --- a/src/app/script/engine.cpp +++ b/src/app/script/engine.cpp @@ -180,6 +180,7 @@ void register_tag_class(lua_State* L); void register_tags_class(lua_State* L); void register_tool_class(lua_State* L); void register_version_class(lua_State* L); +void register_websocket_class(lua_State* L); void set_app_params(lua_State* L, const Params& params); @@ -411,6 +412,7 @@ Engine::Engine() register_tags_class(L); register_tool_class(L); register_version_class(L); + register_websocket_class(L); // Check that we have a clean start (without dirty in the stack) ASSERT(lua_gettop(L) == top); diff --git a/src/app/script/websocket_class.cpp b/src/app/script/websocket_class.cpp new file mode 100644 index 000000000..118970c5a --- /dev/null +++ b/src/app/script/websocket_class.cpp @@ -0,0 +1,180 @@ +// Aseprite +// Copyright (C) 2018-2020 Igara Studio S.A. +// Copyright (C) 2018 David Capello +// +// This program is distributed under the terms of +// the End-User License Agreement for Aseprite. + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "app/app.h" +#include "app/console.h" +#include "app/script/docobj.h" +#include "app/script/engine.h" +#include "app/script/luacpp.h" + +#include +#include +#include + +namespace app { +namespace script { + +namespace { +// additional "enum" value to make message callback simpler +#define MESSAGE_TYPE_BINARY (int)ix::WebSocketMessageType::Fragment + 10 + +int WebSocket_new(lua_State* L) +{ + static std::once_flag f; + std::call_once(f, &ix::initNetSystem); + + auto ws = new ix::WebSocket(); + + push_ptr(L, ws); + + if (lua_istable(L, 1)) { + lua_getfield(L, 1, "url"); + if (const char* s = lua_tostring(L, -1)) { + ws->setUrl(s); + } + lua_pop(L, 1); + + int type = lua_getfield(L, 1, "onreceive"); + if (type == LUA_TFUNCTION) { + int onreceiveRef = luaL_ref(L, LUA_REGISTRYINDEX); + + ws->setOnMessageCallback([L, ws, onreceiveRef](const ix::WebSocketMessagePtr& msg) { + lua_rawgeti(L, LUA_REGISTRYINDEX, onreceiveRef); + lua_pushnumber(L, (msg->binary ? MESSAGE_TYPE_BINARY : static_cast(msg->type))); + lua_pushlstring(L, msg->str.c_str(), msg->str.length()); + + if (lua_pcall(L, 2, 0, 0)) { + if (const char* s = lua_tostring(L, -1)) { + App::instance()->scriptEngine()->consolePrint(s); + ws->disableAutomaticReconnection(); + ws->close(); + } + } + }); + } + else { + lua_pop(L, 1); + } + } + + return 1; +} + +int WebSocket_gc(lua_State* L) +{ + auto ws = get_ptr(L, 1); + ws->stop(); + delete ws; + return 0; +} + +int WebSocket_sendText(lua_State* L) +{ + auto ws = get_ptr(L, 1); + std::stringstream data; + int argc = lua_gettop(L); + + for (int i = 2; i <= argc; i++) { + size_t bufLen; + const char* buf = lua_tolstring(L, i, &bufLen); + data.write(buf, bufLen); + } + lua_pop(L, argc); + + ws->sendText(data.str()); + return 0; +} + +int WebSocket_sendBinary(lua_State* L) +{ + auto ws = get_ptr(L, 1); + std::stringstream data; + int argc = lua_gettop(L); + + for (int i = 2; i <= argc; i++) { + size_t bufLen; + const char* buf = lua_tolstring(L, i, &bufLen); + data.write(buf, bufLen); + } + lua_pop(L, argc); + + ws->sendBinary(data.str()); + return 0; +} + +int WebSocket_connect(lua_State* L) +{ + auto ws = get_ptr(L, 1); + lua_pop(L, 1); + ws->enableAutomaticReconnection(); + ws->start(); + return 0; +} + +int WebSocket_close(lua_State* L) +{ + auto ws = get_ptr(L, 1); + lua_pop(L, 1); + ws->disableAutomaticReconnection(); + ws->close(); + return 0; +} + +int WebSocket_get_url(lua_State* L) +{ + auto ws = get_ptr(L, 1); + lua_pop(L, 1); + lua_pushstring(L, ws->getUrl().c_str()); + return 1; +} + +const luaL_Reg WebSocket_methods[] = { + { "__gc", WebSocket_gc }, + { "close", WebSocket_close }, + { "connect", WebSocket_connect }, + { "sendText", WebSocket_sendText }, + { "sendBinary", WebSocket_sendBinary }, + { nullptr, nullptr } +}; + +const Property WebSocket_properties[] = { + { "url", WebSocket_get_url, nullptr }, + { nullptr, nullptr, nullptr } +}; + +} // namespace { } + +using WebSocket = ix::WebSocket; +DEF_MTNAME(WebSocket); + +void register_websocket_class(lua_State* L) +{ + REG_CLASS(L, WebSocket); + REG_CLASS_NEW(L, WebSocket); + REG_CLASS_PROPERTIES(L, WebSocket); + + // message type enum + lua_newtable(L); + lua_pushvalue(L, -1); + lua_setglobal(L, "MessageType"); + setfield_integer(L, "Text", (int)ix::WebSocketMessageType::Message); + setfield_integer(L, "Binary", MESSAGE_TYPE_BINARY); + setfield_integer(L, "Open", (int)ix::WebSocketMessageType::Open); + setfield_integer(L, "Close", (int)ix::WebSocketMessageType::Close); + setfield_integer(L, "Error", (int)ix::WebSocketMessageType::Error); + setfield_integer(L, "Ping", (int)ix::WebSocketMessageType::Ping); + setfield_integer(L, "Pong", (int)ix::WebSocketMessageType::Pong); + setfield_integer(L, "Fragment", (int)ix::WebSocketMessageType::Fragment); + lua_pop(L, 1); +} + +} // namespace script +} // namespace app \ No newline at end of file From f8ef06a86ebf6b0ae731086c81b2ab9ae4439995 Mon Sep 17 00:00:00 2001 From: lampysprites Date: Sat, 2 Oct 2021 14:05:13 +0700 Subject: [PATCH 04/23] Lua API Image:bytes - convert lua Image to data string and back --- src/app/script/image_class.cpp | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/app/script/image_class.cpp b/src/app/script/image_class.cpp index a8ca8e416..8bdf4e4e9 100644 --- a/src/app/script/image_class.cpp +++ b/src/app/script/image_class.cpp @@ -483,6 +483,30 @@ int Image_resize(lua_State* L) return 0; } +int Image_get_bytes(lua_State* L) +{ + const auto img = get_obj(L, 1)->image(L); + lua_pushlstring(L, (const char*)img->getPixelAddress(0, 0), img->getRowStrideSize() * img->height()); + return 1; +} + +int Image_set_bytes(lua_State* L) +{ + const auto img = get_obj(L, 1)->image(L); + size_t bytes_size, bytes_needed = img->getRowStrideSize() * img->height(); + const char* bytes = lua_tolstring(L, 2, &bytes_size); + + if (bytes_size == bytes_needed) { + memcpy_s(img->getPixelAddress(0, 0), bytes_needed, bytes, bytes_size); + } + else { + lua_pushfstring(L, "Data size does not match: given %d, needed %d.", bytes_size, bytes_needed); + lua_error(L); + } + + return 0; +} + int Image_get_width(lua_State* L) { const auto obj = get_obj(L, 1); @@ -537,6 +561,7 @@ const luaL_Reg Image_methods[] = { }; const Property Image_properties[] = { + { "bytes", Image_get_bytes, Image_set_bytes }, { "width", Image_get_width, nullptr }, { "height", Image_get_height, nullptr }, { "colorMode", Image_get_colorMode, nullptr }, From 22e3e6244a83dbc2ba8bf74cee1e700c176a46a3 Mon Sep 17 00:00:00 2001 From: lampysprites Date: Sat, 2 Oct 2021 16:22:31 +0700 Subject: [PATCH 05/23] Simplify user API for Sprite observers --- src/app/script/sprite_class.cpp | 69 ++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/src/app/script/sprite_class.cpp b/src/app/script/sprite_class.cpp index 4c7ec610a..373664fcd 100644 --- a/src/app/script/sprite_class.cpp +++ b/src/app/script/sprite_class.cpp @@ -59,7 +59,7 @@ namespace app { namespace script { namespace { - +// in lua, observers become `Sprite.onChange` class ScriptDocObserver : public DocUndoObserver { public: ScriptDocObserver(int callbackRef) @@ -73,6 +73,9 @@ public: void onClearRedo(DocUndo* history) { } void onTotalUndoSizeChange(DocUndo* history) { } + int callbackRef() { return m_callbackRef; } + void setCallbackRef(int callbackRef) { m_callbackRef = callbackRef; } + private: void callback() { script::Engine* engine = App::instance()->scriptEngine(); @@ -89,13 +92,8 @@ private: int m_callbackRef; }; -const luaL_Reg ScriptDocObserver_methods[] = { - { nullptr, nullptr } -}; - -const Property ScriptDocObserver_properties[] = { - { nullptr, nullptr, nullptr } -}; +// used to maintain one-to-one relation between sprites and observers +std::map script_observers; int Sprite_new(lua_State* L) { @@ -629,28 +627,42 @@ int Sprite_deleteSlice(lua_State* L) } } -int Sprite_watch(lua_State* L) +int Sprite_get_onChange(lua_State* L) +{ + auto sprite = get_docobj(L, 1); + ScriptDocObserver* obs = script_observers[sprite->id()]; + + if (obs) { + lua_rawgeti(L, LUA_REGISTRYINDEX, obs->callbackRef()); + } + else { + lua_pushnil(L); + } + return 1; +} + +int Sprite_set_onChange(lua_State* L) { auto sprite = get_docobj(L, 1); auto doc = static_cast(sprite->document()); - ScriptDocObserver* obs; - - if (lua_isfunction(L, 2)) { - int callbackRef = luaL_ref(L, LUA_REGISTRYINDEX); - obs = push_new(L, callbackRef); - doc->undoHistory()->add_observer(obs); - return 1; - } - return 0; -} - -int Sprite_unwatch(lua_State* L) -{ - if (auto obs = may_get_obj(L, 2)) { - auto sprite = get_docobj(L, 1); - auto doc = static_cast(sprite->document()); + ScriptDocObserver* obs = script_observers[sprite->id()]; + if (lua_isnil(L, 2) && obs) { doc->undoHistory()->remove_observer(obs); + script_observers.erase(sprite->id()); + luaL_unref(L, LUA_REGISTRYINDEX, obs->callbackRef()); + delete obs; + } + else if (lua_isfunction(L, 2)) { + int callbackRef = luaL_ref(L, LUA_REGISTRYINDEX); + if (obs) { + obs->setCallbackRef(callbackRef); + } + else { + obs = new ScriptDocObserver(callbackRef); + doc->undoHistory()->add_observer(obs); + script_observers[sprite->id()] = obs; + } } return 0; } @@ -887,9 +899,6 @@ const luaL_Reg Sprite_methods[] = { // Slices { "newSlice", Sprite_newSlice }, { "deleteSlice", Sprite_deleteSlice }, - // Observer - { "watch", Sprite_watch }, - { "unwatch", Sprite_unwatch }, { nullptr, nullptr } }; @@ -912,13 +921,13 @@ const Property Sprite_properties[] = { { "bounds", Sprite_get_bounds, nullptr }, { "gridBounds", Sprite_get_gridBounds, Sprite_set_gridBounds }, { "pixelRatio", Sprite_get_pixelRatio, Sprite_set_pixelRatio }, + { "onChange", Sprite_get_onChange, Sprite_set_onChange }, { nullptr, nullptr, nullptr } }; } // anonymous namespace DEF_MTNAME(doc::Sprite); -DEF_MTNAME(ScriptDocObserver); void register_sprite_class(lua_State* L) { @@ -926,8 +935,6 @@ void register_sprite_class(lua_State* L) REG_CLASS(L, Sprite); REG_CLASS_NEW(L, Sprite); REG_CLASS_PROPERTIES(L, Sprite); - - REG_CLASS(L, ScriptDocObserver); } } // namespace script From 2aa3fdbd9530b28d2cf92c69a3fe6c76cd704ec7 Mon Sep 17 00:00:00 2001 From: lampysprites Date: Sat, 2 Oct 2021 18:34:17 +0700 Subject: [PATCH 06/23] Execute websocket callbacks in the ui thread --- src/app/script/websocket_class.cpp | 36 +++++++++++++++++------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/src/app/script/websocket_class.cpp b/src/app/script/websocket_class.cpp index 118970c5a..1869fd40b 100644 --- a/src/app/script/websocket_class.cpp +++ b/src/app/script/websocket_class.cpp @@ -11,9 +11,9 @@ #include "app/app.h" #include "app/console.h" -#include "app/script/docobj.h" #include "app/script/engine.h" #include "app/script/luacpp.h" +#include "ui/system.h" #include #include @@ -46,19 +46,25 @@ int WebSocket_new(lua_State* L) if (type == LUA_TFUNCTION) { int onreceiveRef = luaL_ref(L, LUA_REGISTRYINDEX); - ws->setOnMessageCallback([L, ws, onreceiveRef](const ix::WebSocketMessagePtr& msg) { - lua_rawgeti(L, LUA_REGISTRYINDEX, onreceiveRef); - lua_pushnumber(L, (msg->binary ? MESSAGE_TYPE_BINARY : static_cast(msg->type))); - lua_pushlstring(L, msg->str.c_str(), msg->str.length()); + ws->setOnMessageCallback( + [L, ws, onreceiveRef](const ix::WebSocketMessagePtr& msg) { + int msgType = + (msg->binary ? MESSAGE_TYPE_BINARY : static_cast(msg->type)); + std::string msgData = msg->str; - if (lua_pcall(L, 2, 0, 0)) { - if (const char* s = lua_tostring(L, -1)) { - App::instance()->scriptEngine()->consolePrint(s); - ws->disableAutomaticReconnection(); - ws->close(); - } - } - }); + ui::execute_from_ui_thread([=]() { + lua_rawgeti(L, LUA_REGISTRYINDEX, onreceiveRef); + lua_pushinteger(L, msgType); + lua_pushlstring(L, msgData.c_str(), msgData.length()); + + if (lua_pcall(L, 2, 0, 0)) { + if (const char* s = lua_tostring(L, -1)) { + App::instance()->scriptEngine()->consolePrint(s); + ws->stop(); + } + } + }); + }); } else { lua_pop(L, 1); @@ -114,7 +120,6 @@ int WebSocket_connect(lua_State* L) { auto ws = get_ptr(L, 1); lua_pop(L, 1); - ws->enableAutomaticReconnection(); ws->start(); return 0; } @@ -123,8 +128,7 @@ int WebSocket_close(lua_State* L) { auto ws = get_ptr(L, 1); lua_pop(L, 1); - ws->disableAutomaticReconnection(); - ws->close(); + ws->stop(); return 0; } From 83c03a31584a475077be4c8f97911dc03e8bb843 Mon Sep 17 00:00:00 2001 From: lampysprites Date: Sat, 2 Oct 2021 20:09:35 +0700 Subject: [PATCH 07/23] Expose site observer to lua API --- src/app/script/site_class.cpp | 73 +++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/src/app/script/site_class.cpp b/src/app/script/site_class.cpp index 985d3c990..4a73fe7bc 100644 --- a/src/app/script/site_class.cpp +++ b/src/app/script/site_class.cpp @@ -9,6 +9,10 @@ #include "config.h" #endif +#include "app/app.h" +#include "app/console.h" +#include "app/context.h" +#include "app/context_observer.h" #include "app/script/docobj.h" #include "app/script/engine.h" #include "app/script/luacpp.h" @@ -22,6 +26,47 @@ namespace script { namespace { +class ScriptSiteObserver : public ContextObserver { +public: + static ScriptSiteObserver* instance() { + static ScriptSiteObserver instance; + return &instance; + } + + void onActiveSiteChange(const Site& site) { + if (m_onchangeRef == 0) + return; + + script::Engine* engine = App::instance()->scriptEngine(); + lua_State* L = engine->luaState(); + + lua_rawgeti(L, LUA_REGISTRYINDEX, m_onchangeRef); + if (lua_pcall(L, 0, 0, 0)) { + if (const char* s = lua_tostring(L, -1)) { + Console().printf("Error: %s", s); + } + } + } + + int callbackRef() { + return m_onchangeRef; + } + + void setCallbackRef(int onchangeRef) { + m_onchangeRef = onchangeRef; + } + +private: + ScriptSiteObserver() + : ContextObserver(), + m_onchangeRef(0) + { } + + ~ScriptSiteObserver() { } + + int m_onchangeRef; +}; + int Site_get_sprite(lua_State* L) { auto site = get_obj(L, 1); @@ -79,6 +124,33 @@ int Site_get_image(lua_State* L) return 1; } +int Site_get_onChange(lua_State* L) +{ + lua_rawgeti(L, LUA_REGISTRYINDEX, ScriptSiteObserver::instance()->callbackRef()); + return 1; +} + +int Site_set_onChange(lua_State* L) +{ + auto site = get_obj(L, 1); + auto obs = ScriptSiteObserver::instance(); + + if (lua_isnil(L, 2) && obs->callbackRef()) { + luaL_unref(L, LUA_REGISTRYINDEX, obs->callbackRef()); + obs->setCallbackRef(0); + App::instance()->context()->remove_observer(obs); + } + else if (lua_isfunction(L, 2)) { + int onchangeRef = luaL_ref(L, LUA_REGISTRYINDEX); + if (!obs->callbackRef()) { + App::instance()->context()->add_observer(obs); + } + obs->setCallbackRef(onchangeRef); + } + + return 0; +} + const luaL_Reg Site_methods[] = { { nullptr, nullptr } }; @@ -90,6 +162,7 @@ const Property Site_properties[] = { { "frame", Site_get_frame, nullptr }, { "frameNumber", Site_get_frameNumber, nullptr }, { "image", Site_get_image, nullptr }, + { "onChange", Site_get_onChange, Site_set_onChange }, { nullptr, nullptr, nullptr } }; From 28595a410d20bfc83a739562cafafd63af14f095 Mon Sep 17 00:00:00 2001 From: lampysprites Date: Sat, 2 Oct 2021 22:10:25 +0700 Subject: [PATCH 08/23] Clean up sprite observers on lua GC --- src/app/script/sprite_class.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/app/script/sprite_class.cpp b/src/app/script/sprite_class.cpp index 373664fcd..ad54e89ba 100644 --- a/src/app/script/sprite_class.cpp +++ b/src/app/script/sprite_class.cpp @@ -172,6 +172,18 @@ int Sprite_new(lua_State* L) return 1; } +int Sprite_gc(lua_State* L) +{ + auto sprite = get_docobj(L, 1); + auto doc = static_cast(sprite->document()); + + if (ScriptDocObserver* obs = script_observers[sprite->id()]) { + doc->undoHistory()->remove_observer(obs); + script_observers.erase(sprite->id()); + } + return 0; +} + int Sprite_eq(lua_State* L) { const auto a = get_docobj(L, 1); @@ -871,6 +883,7 @@ int Sprite_set_pixelRatio(lua_State* L) } const luaL_Reg Sprite_methods[] = { + { "__gc", Sprite_gc }, { "__eq", Sprite_eq }, { "resize", Sprite_resize }, { "crop", Sprite_crop }, From 711741a35880bbf6ba7ff066a2d9cf2cbcbce4b0 Mon Sep 17 00:00:00 2001 From: lampysprites Date: Sun, 3 Oct 2021 02:22:15 +0700 Subject: [PATCH 09/23] Remove unneeded lua stack pops --- src/app/script/websocket_class.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/app/script/websocket_class.cpp b/src/app/script/websocket_class.cpp index 1869fd40b..c7743916c 100644 --- a/src/app/script/websocket_class.cpp +++ b/src/app/script/websocket_class.cpp @@ -93,7 +93,6 @@ int WebSocket_sendText(lua_State* L) const char* buf = lua_tolstring(L, i, &bufLen); data.write(buf, bufLen); } - lua_pop(L, argc); ws->sendText(data.str()); return 0; @@ -110,7 +109,6 @@ int WebSocket_sendBinary(lua_State* L) const char* buf = lua_tolstring(L, i, &bufLen); data.write(buf, bufLen); } - lua_pop(L, argc); ws->sendBinary(data.str()); return 0; @@ -119,7 +117,6 @@ int WebSocket_sendBinary(lua_State* L) int WebSocket_connect(lua_State* L) { auto ws = get_ptr(L, 1); - lua_pop(L, 1); ws->start(); return 0; } @@ -127,7 +124,6 @@ int WebSocket_connect(lua_State* L) int WebSocket_close(lua_State* L) { auto ws = get_ptr(L, 1); - lua_pop(L, 1); ws->stop(); return 0; } @@ -135,7 +131,6 @@ int WebSocket_close(lua_State* L) int WebSocket_get_url(lua_State* L) { auto ws = get_ptr(L, 1); - lua_pop(L, 1); lua_pushstring(L, ws->getUrl().c_str()); return 1; } From 4354be1d7a3f1c3edc9807a745cc7316a9070a9e Mon Sep 17 00:00:00 2001 From: lampysprites Date: Sun, 3 Oct 2021 12:40:29 +0700 Subject: [PATCH 10/23] Add CMake option to disable websockets --- CMakeLists.txt | 1 + src/CMakeLists.txt | 5 +++++ src/app/CMakeLists.txt | 12 ++++++++++-- src/app/script/engine.cpp | 2 ++ third_party/CMakeLists.txt | 9 ++++++--- 5 files changed, 24 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3af0bd074..5ce626045 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -70,6 +70,7 @@ option(ENABLE_MEMLEAK "Enable memory-leaks detector (only for developers)" option(ENABLE_NEWS "Enable the news in Home tab" on) option(ENABLE_UPDATER "Enable automatic check for updates" on) option(ENABLE_SCRIPTING "Compile with scripting support" on) +option(ENABLE_WEBSOCKET "Compile with websocket support" on) option(ENABLE_TESTS "Compile unit tests" off) option(ENABLE_BENCHMARKS "Compile benchmarks" off) option(ENABLE_TRIAL_MODE "Compile the trial version" off) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index dfc2fefbd..cb25aeac6 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -74,6 +74,11 @@ if(ENABLE_SCRIPTING) add_definitions(-DENABLE_SCRIPTING) endif() +if(ENABLE_WEBSOCKET) + # Needed for "app" and "main" + add_definitions(-DENABLE_WEBSOCKET) +endif() + ###################################################################### # Aseprite Libraries (in preferred order to be built) diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index de55dae70..9f2e07549 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -147,6 +147,10 @@ if(ENABLE_SCRIPTING) commands/cmd_open_script_folder.cpp ui/devconsole_view.cpp) endif() + if(ENABLE_WEBSOCKET) + set(scripting_files_ws + script/websocket_class.cpp) + endif() set(scripting_files commands/cmd_run_script.cpp script/app_command_object.cpp @@ -189,8 +193,8 @@ if(ENABLE_SCRIPTING) script/tool_class.cpp script/values.cpp script/version_class.cpp - script/websocket_class.cpp shell.cpp + ${scripting_files_ws} ${scripting_files_ui}) endif() @@ -674,7 +678,11 @@ if(REQUIRE_CURL) endif() if(ENABLE_SCRIPTING) - target_link_libraries(app-lib lua lauxlib lualib ixwebsocket) + target_link_libraries(app-lib lua lauxlib lualib) + + if(ENABLE_WEBSOCKET) + target_link_libraries(app-lib ixwebsocket) + endif() endif() if(ENABLE_UPDATER) diff --git a/src/app/script/engine.cpp b/src/app/script/engine.cpp index 5417bb435..cf6c7f7b5 100644 --- a/src/app/script/engine.cpp +++ b/src/app/script/engine.cpp @@ -412,7 +412,9 @@ Engine::Engine() register_tags_class(L); register_tool_class(L); register_version_class(L); +#if ENABLE_WEBSOCKET register_websocket_class(L); + #endif // Check that we have a clean start (without dirty in the stack) ASSERT(lua_gettop(L) == top); diff --git a/third_party/CMakeLists.txt b/third_party/CMakeLists.txt index 4592f5518..8e5428f1c 100644 --- a/third_party/CMakeLists.txt +++ b/third_party/CMakeLists.txt @@ -160,7 +160,10 @@ if(ENABLE_SCRIPTING) target_include_directories(lualib PUBLIC lua) target_link_libraries(lauxlib lua) -# ixwebsocket -add_subdirectory(ixwebsocket) -target_include_directories(ixwebsocket PUBLIC) + # ixwebsocket + if(ENABLE_WEBSOCKET) + add_subdirectory(IXWebSocket) + target_include_directories(ixwebsocket PUBLIC) + endif() + endif() From a90853f7653287894287f23402e5f445780457ba Mon Sep 17 00:00:00 2001 From: lampysprites Date: Sun, 3 Oct 2021 12:52:26 +0700 Subject: [PATCH 11/23] Extend WebSocket API --- src/app/script/websocket_class.cpp | 34 ++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/src/app/script/websocket_class.cpp b/src/app/script/websocket_class.cpp index c7743916c..e743ffd5e 100644 --- a/src/app/script/websocket_class.cpp +++ b/src/app/script/websocket_class.cpp @@ -42,6 +42,15 @@ int WebSocket_new(lua_State* L) } lua_pop(L, 1); + lua_getfield(L, 1, "deflate"); + if (lua_toboolean(L, -1)) { + ws->enablePerMessageDeflate(); + } + else { + ws->disablePerMessageDeflate(); + } + lua_pop(L, 1); + int type = lua_getfield(L, 1, "onreceive"); if (type == LUA_TFUNCTION) { int onreceiveRef = luaL_ref(L, LUA_REGISTRYINDEX); @@ -94,7 +103,10 @@ int WebSocket_sendText(lua_State* L) data.write(buf, bufLen); } - ws->sendText(data.str()); + if (!ws->sendText(data.str()).success) { + lua_pushstring(L, "Websocket error"); + lua_error(L); + } return 0; } @@ -110,7 +122,24 @@ int WebSocket_sendBinary(lua_State* L) data.write(buf, bufLen); } - ws->sendBinary(data.str()); + if (!ws->sendBinary(data.str()).success) { + lua_pushstring(L, "Websocket error"); + lua_error(L); + } + return 0; +} + +int WebSocket_sendPing(lua_State* L) +{ + auto ws = get_ptr(L, 1); + size_t bufLen; + const char* buf = lua_tolstring(L, 2, &bufLen); + std::string data(buf, bufLen); + + if (!ws->ping(data).success) { + lua_pushstring(L, "Websocket error"); + lua_error(L); + } return 0; } @@ -141,6 +170,7 @@ const luaL_Reg WebSocket_methods[] = { { "connect", WebSocket_connect }, { "sendText", WebSocket_sendText }, { "sendBinary", WebSocket_sendBinary }, + { "sendPing", WebSocket_sendPing }, { nullptr, nullptr } }; From bf53fa26cd9891d6915e9f3dcceace127e0b74fe Mon Sep 17 00:00:00 2001 From: lampysprites Date: Mon, 4 Oct 2021 12:17:25 +0700 Subject: [PATCH 12/23] Make onChange callbacks more predictable --- src/app/script/site_class.cpp | 12 ++++++------ src/app/script/sprite_class.cpp | 20 ++++++++------------ 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/src/app/script/site_class.cpp b/src/app/script/site_class.cpp index 4a73fe7bc..bed17f91f 100644 --- a/src/app/script/site_class.cpp +++ b/src/app/script/site_class.cpp @@ -135,18 +135,18 @@ int Site_set_onChange(lua_State* L) auto site = get_obj(L, 1); auto obs = ScriptSiteObserver::instance(); - if (lua_isnil(L, 2) && obs->callbackRef()) { - luaL_unref(L, LUA_REGISTRYINDEX, obs->callbackRef()); - obs->setCallbackRef(0); - App::instance()->context()->remove_observer(obs); - } - else if (lua_isfunction(L, 2)) { + if (lua_isfunction(L, 2)) { int onchangeRef = luaL_ref(L, LUA_REGISTRYINDEX); if (!obs->callbackRef()) { App::instance()->context()->add_observer(obs); } obs->setCallbackRef(onchangeRef); } + else if (obs->callbackRef()) { + luaL_unref(L, LUA_REGISTRYINDEX, obs->callbackRef()); + obs->setCallbackRef(0); + App::instance()->context()->remove_observer(obs); + } return 0; } diff --git a/src/app/script/sprite_class.cpp b/src/app/script/sprite_class.cpp index ad54e89ba..4c7a46bd6 100644 --- a/src/app/script/sprite_class.cpp +++ b/src/app/script/sprite_class.cpp @@ -176,11 +176,6 @@ int Sprite_gc(lua_State* L) { auto sprite = get_docobj(L, 1); auto doc = static_cast(sprite->document()); - - if (ScriptDocObserver* obs = script_observers[sprite->id()]) { - doc->undoHistory()->remove_observer(obs); - script_observers.erase(sprite->id()); - } return 0; } @@ -659,13 +654,7 @@ int Sprite_set_onChange(lua_State* L) auto doc = static_cast(sprite->document()); ScriptDocObserver* obs = script_observers[sprite->id()]; - if (lua_isnil(L, 2) && obs) { - doc->undoHistory()->remove_observer(obs); - script_observers.erase(sprite->id()); - luaL_unref(L, LUA_REGISTRYINDEX, obs->callbackRef()); - delete obs; - } - else if (lua_isfunction(L, 2)) { + if (lua_isfunction(L, 2)) { int callbackRef = luaL_ref(L, LUA_REGISTRYINDEX); if (obs) { obs->setCallbackRef(callbackRef); @@ -676,6 +665,13 @@ int Sprite_set_onChange(lua_State* L) script_observers[sprite->id()] = obs; } } + else if (obs) { + doc->undoHistory()->remove_observer(obs); + script_observers.erase(sprite->id()); + luaL_unref(L, LUA_REGISTRYINDEX, obs->callbackRef()); + delete obs; + } + return 0; } From 2628afdfceb50e052d3e1be347a62269c01cdf60 Mon Sep 17 00:00:00 2001 From: lampysprites Date: Mon, 4 Oct 2021 23:15:55 +0700 Subject: [PATCH 13/23] Remove unneeded Sprite.__gc --- src/app/script/sprite_class.cpp | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/app/script/sprite_class.cpp b/src/app/script/sprite_class.cpp index 4c7a46bd6..cfcdbdd5f 100644 --- a/src/app/script/sprite_class.cpp +++ b/src/app/script/sprite_class.cpp @@ -172,13 +172,6 @@ int Sprite_new(lua_State* L) return 1; } -int Sprite_gc(lua_State* L) -{ - auto sprite = get_docobj(L, 1); - auto doc = static_cast(sprite->document()); - return 0; -} - int Sprite_eq(lua_State* L) { const auto a = get_docobj(L, 1); @@ -879,7 +872,6 @@ int Sprite_set_pixelRatio(lua_State* L) } const luaL_Reg Sprite_methods[] = { - { "__gc", Sprite_gc }, { "__eq", Sprite_eq }, { "resize", Sprite_resize }, { "crop", Sprite_crop }, From f8dabfa294b147afbb245588f964ce94d12ea7cd Mon Sep 17 00:00:00 2001 From: David Capello Date: Mon, 4 Oct 2021 18:06:40 -0300 Subject: [PATCH 14/23] Use portable memcpy instead of memcpy_s --- src/app/script/image_class.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/script/image_class.cpp b/src/app/script/image_class.cpp index 8bdf4e4e9..cd694194a 100644 --- a/src/app/script/image_class.cpp +++ b/src/app/script/image_class.cpp @@ -33,6 +33,7 @@ #include "render/render.h" #include +#include #include namespace app { @@ -497,7 +498,7 @@ int Image_set_bytes(lua_State* L) const char* bytes = lua_tolstring(L, 2, &bytes_size); if (bytes_size == bytes_needed) { - memcpy_s(img->getPixelAddress(0, 0), bytes_needed, bytes, bytes_size); + std::memcpy(img->getPixelAddress(0, 0), bytes, std::min(bytes_needed, bytes_size)); } else { lua_pushfstring(L, "Data size does not match: given %d, needed %d.", bytes_size, bytes_needed); From b6d7c1e2614fa81717e2246743634dd4187388ce Mon Sep 17 00:00:00 2001 From: David Capello Date: Mon, 4 Oct 2021 18:07:28 -0300 Subject: [PATCH 15/23] Avoid error running cmake when entering IXWebSocket subdirectory I don't know why it fails, but it looks like the commands specified to install the IXWebSocket library are problematic. (And anyway we don't want to install the library after the compilation process.) --- third_party/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/third_party/CMakeLists.txt b/third_party/CMakeLists.txt index 8e5428f1c..e4457eb24 100644 --- a/third_party/CMakeLists.txt +++ b/third_party/CMakeLists.txt @@ -162,6 +162,7 @@ if(ENABLE_SCRIPTING) # ixwebsocket if(ENABLE_WEBSOCKET) + set(IXWEBSOCKET_INSTALL OFF CACHE BOOL "Install IXWebSocket") add_subdirectory(IXWebSocket) target_include_directories(ixwebsocket PUBLIC) endif() From c117b1b01ffef5d70ad57b69b06d29ff8c745139 Mon Sep 17 00:00:00 2001 From: David Capello Date: Wed, 6 Oct 2021 19:15:42 -0300 Subject: [PATCH 16/23] Simplify Image_set_bytes(): as bytes_size == bytes_needed we don't need to call std::min() --- src/app/script/image_class.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/script/image_class.cpp b/src/app/script/image_class.cpp index cd694194a..857e44e95 100644 --- a/src/app/script/image_class.cpp +++ b/src/app/script/image_class.cpp @@ -498,7 +498,7 @@ int Image_set_bytes(lua_State* L) const char* bytes = lua_tolstring(L, 2, &bytes_size); if (bytes_size == bytes_needed) { - std::memcpy(img->getPixelAddress(0, 0), bytes, std::min(bytes_needed, bytes_size)); + std::memcpy(img->getPixelAddress(0, 0), bytes, bytes_size); } else { lua_pushfstring(L, "Data size does not match: given %d, needed %d.", bytes_size, bytes_needed); From 0ca411690bac831e012cea495a18772ef2c66d21 Mon Sep 17 00:00:00 2001 From: David Capello Date: Wed, 6 Oct 2021 19:17:10 -0300 Subject: [PATCH 17/23] Remove trailing whitespaces --- src/app/script/site_class.cpp | 10 +++++----- src/app/script/websocket_class.cpp | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/app/script/site_class.cpp b/src/app/script/site_class.cpp index bed17f91f..11d7d624e 100644 --- a/src/app/script/site_class.cpp +++ b/src/app/script/site_class.cpp @@ -28,7 +28,7 @@ namespace { class ScriptSiteObserver : public ContextObserver { public: - static ScriptSiteObserver* instance() { + static ScriptSiteObserver* instance() { static ScriptSiteObserver instance; return &instance; } @@ -48,11 +48,11 @@ public: } } - int callbackRef() { - return m_onchangeRef; + int callbackRef() { + return m_onchangeRef; } - void setCallbackRef(int onchangeRef) { + void setCallbackRef(int onchangeRef) { m_onchangeRef = onchangeRef; } @@ -147,7 +147,7 @@ int Site_set_onChange(lua_State* L) obs->setCallbackRef(0); App::instance()->context()->remove_observer(obs); } - + return 0; } diff --git a/src/app/script/websocket_class.cpp b/src/app/script/websocket_class.cpp index e743ffd5e..d57290b98 100644 --- a/src/app/script/websocket_class.cpp +++ b/src/app/script/websocket_class.cpp @@ -34,7 +34,7 @@ int WebSocket_new(lua_State* L) auto ws = new ix::WebSocket(); push_ptr(L, ws); - + if (lua_istable(L, 1)) { lua_getfield(L, 1, "url"); if (const char* s = lua_tostring(L, -1)) { @@ -164,7 +164,7 @@ int WebSocket_get_url(lua_State* L) return 1; } -const luaL_Reg WebSocket_methods[] = { +const luaL_Reg WebSocket_methods[] = { { "__gc", WebSocket_gc }, { "close", WebSocket_close }, { "connect", WebSocket_connect }, @@ -174,7 +174,7 @@ const luaL_Reg WebSocket_methods[] = { { nullptr, nullptr } }; -const Property WebSocket_properties[] = { +const Property WebSocket_properties[] = { { "url", WebSocket_get_url, nullptr }, { nullptr, nullptr, nullptr } }; @@ -206,4 +206,4 @@ void register_websocket_class(lua_State* L) } } // namespace script -} // namespace app \ No newline at end of file +} // namespace app From 327b38a05dd9a215b02f456f070bddf20a687e28 Mon Sep 17 00:00:00 2001 From: David Capello Date: Wed, 6 Oct 2021 19:29:19 -0300 Subject: [PATCH 18/23] Wrap MESSAGE_TYPE_BINARY with parenthesis (...) --- src/app/script/websocket_class.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/app/script/websocket_class.cpp b/src/app/script/websocket_class.cpp index d57290b98..a4b5f8851 100644 --- a/src/app/script/websocket_class.cpp +++ b/src/app/script/websocket_class.cpp @@ -23,8 +23,9 @@ namespace app { namespace script { namespace { -// additional "enum" value to make message callback simpler -#define MESSAGE_TYPE_BINARY (int)ix::WebSocketMessageType::Fragment + 10 + +// Additional "enum" value to make message callback simpler +#define MESSAGE_TYPE_BINARY ((int)ix::WebSocketMessageType::Fragment + 10) int WebSocket_new(lua_State* L) { From 0249275f8c5229a044233d5b848afb447e3c688a Mon Sep 17 00:00:00 2001 From: David Capello Date: Wed, 6 Oct 2021 19:32:56 -0300 Subject: [PATCH 19/23] [lua] Change MessageType to WebSocketMessageType Just in case to avoid collision or confusion with some kind of future MessageType (e.g. ui::MessageType). There are no plans for this, but we prefer to use WebSocket* prefix. --- src/app/script/websocket_class.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/script/websocket_class.cpp b/src/app/script/websocket_class.cpp index a4b5f8851..32be7d365 100644 --- a/src/app/script/websocket_class.cpp +++ b/src/app/script/websocket_class.cpp @@ -194,7 +194,7 @@ void register_websocket_class(lua_State* L) // message type enum lua_newtable(L); lua_pushvalue(L, -1); - lua_setglobal(L, "MessageType"); + lua_setglobal(L, "WebSocketMessageType"); setfield_integer(L, "Text", (int)ix::WebSocketMessageType::Message); setfield_integer(L, "Binary", MESSAGE_TYPE_BINARY); setfield_integer(L, "Open", (int)ix::WebSocketMessageType::Open); From cd342f5630151d9756ee92e8bdbc4ac6741b6529 Mon Sep 17 00:00:00 2001 From: David Capello Date: Thu, 7 Oct 2021 18:56:39 -0300 Subject: [PATCH 20/23] [lua] Add events handling with Sprite.events & App.events Added a new Events object with :on() and :off() methods to start or stop listening to a specific event respectively. This also allows to add several callbacks for the same event. Replaced the temporal Site.onChange & Sprite.onChange implementations. Related to several issues (enable more possibilities for): #138, #1403, #1949, #2965, #2980 --- laf | 2 +- src/app/CMakeLists.txt | 1 + src/app/active_site_handler.cpp | 11 +- src/app/context.cpp | 29 ++- src/app/context.h | 8 +- src/app/doc.cpp | 8 + src/app/doc_observer.h | 3 +- src/app/doc_undo_observer.h | 11 +- src/app/script/app_object.cpp | 7 + src/app/script/engine.cpp | 2 + src/app/script/engine.h | 4 +- src/app/script/events_class.cpp | 343 ++++++++++++++++++++++++++++++++ src/app/script/site_class.cpp | 69 ------- src/app/script/sprite_class.cpp | 75 +------ src/app/ui_context.cpp | 6 +- src/app/ui_context.h | 4 +- 16 files changed, 412 insertions(+), 171 deletions(-) create mode 100644 src/app/script/events_class.cpp diff --git a/laf b/laf index f58ece824..c9bb18ba9 160000 --- a/laf +++ b/laf @@ -1 +1 @@ -Subproject commit f58ece8248e2ebf6124b28ec49044be913f57292 +Subproject commit c9bb18ba92915f0d9735da25360e4725f767b447 diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 9f2e07549..aa81cb727 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -163,6 +163,7 @@ if(ENABLE_SCRIPTING) script/color_space_class.cpp script/dialog_class.cpp script/engine.cpp + script/events_class.cpp script/frame_class.cpp script/frames_class.cpp script/image_class.cpp diff --git a/src/app/active_site_handler.cpp b/src/app/active_site_handler.cpp index 45320b47e..d29e1841f 100644 --- a/src/app/active_site_handler.cpp +++ b/src/app/active_site_handler.cpp @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2019-2020 Igara Studio S.A. +// Copyright (C) 2019-2021 Igara Studio S.A. // // This program is distributed under the terms of // the End-User License Agreement for Aseprite. @@ -28,10 +28,11 @@ ActiveSiteHandler::~ActiveSiteHandler() void ActiveSiteHandler::addDoc(Doc* doc) { Data data; - if (doc::Layer* layer = doc->sprite()->root()->firstLayer()) - data.layer = layer->id(); - else - data.layer = doc::NullId; + data.layer = doc::NullId; + if (doc->sprite()) { // The sprite can be nullptr in some tests + if (doc::Layer* layer = doc->sprite()->root()->firstLayer()) + data.layer = layer->id(); + } data.frame = 0; m_data.insert(std::make_pair(doc, data)); doc->add_observer(this); diff --git a/src/app/context.cpp b/src/app/context.cpp index 078dd9126..1e5bb74a1 100644 --- a/src/app/context.cpp +++ b/src/app/context.cpp @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2018-2020 Igara Studio S.A. +// Copyright (C) 2018-2021 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -81,7 +81,7 @@ Doc* Context::activeDocument() const void Context::setActiveDocument(Doc* document) { - onSetActiveDocument(document); + onSetActiveDocument(document, true); } void Context::setActiveLayer(doc::Layer* layer) @@ -206,15 +206,19 @@ void Context::onAddDocument(Doc* doc) if (m_activeSiteHandler) m_activeSiteHandler->addDoc(doc); + + notifyActiveSiteChanged(); } void Context::onRemoveDocument(Doc* doc) { - if (doc == m_lastSelectedDoc) - m_lastSelectedDoc = nullptr; - if (m_activeSiteHandler) m_activeSiteHandler->removeDoc(doc); + + if (doc == m_lastSelectedDoc) { + m_lastSelectedDoc = nullptr; + notifyActiveSiteChanged(); + } } void Context::onGetActiveSite(Site* site) const @@ -224,24 +228,33 @@ void Context::onGetActiveSite(Site* site) const activeSiteHandler()->getActiveSiteForDoc(doc, site); } -void Context::onSetActiveDocument(Doc* doc) +void Context::onSetActiveDocument(Doc* doc, bool notify) { m_lastSelectedDoc = doc; + if (notify) + notifyActiveSiteChanged(); } void Context::onSetActiveLayer(doc::Layer* layer) { Doc* newDoc = (layer ? static_cast(layer->sprite()->document()): nullptr); + if (!newDoc) + return; + + activeSiteHandler()->setActiveLayerInDoc(newDoc, layer); + if (newDoc != m_lastSelectedDoc) setActiveDocument(newDoc); - if (newDoc) - activeSiteHandler()->setActiveLayerInDoc(newDoc, layer); + else + notifyActiveSiteChanged(); } void Context::onSetActiveFrame(const doc::frame_t frame) { if (m_lastSelectedDoc) activeSiteHandler()->setActiveFrameInDoc(m_lastSelectedDoc, frame); + + notifyActiveSiteChanged(); } void Context::onSetRange(const DocRange& range) diff --git a/src/app/context.h b/src/app/context.h index e9fe64e48..14cbc6e39 100644 --- a/src/app/context.h +++ b/src/app/context.h @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2018-2020 Igara Studio S.A. +// Copyright (C) 2018-2021 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -110,7 +110,7 @@ namespace app { void onRemoveDocument(Doc* doc) override; virtual void onGetActiveSite(Site* site) const; - virtual void onSetActiveDocument(Doc* doc); + virtual void onSetActiveDocument(Doc* doc, bool notify); virtual void onSetActiveLayer(doc::Layer* layer); virtual void onSetActiveFrame(const doc::frame_t frame); virtual void onSetRange(const DocRange& range); @@ -122,10 +122,12 @@ namespace app { private: ActiveSiteHandler* activeSiteHandler() const; + // This must be defined before m_docs because ActiveSiteHandler + // will be an observer of all documents. + mutable std::unique_ptr m_activeSiteHandler; mutable Docs m_docs; ContextFlags m_flags; // Last updated flags. Doc* m_lastSelectedDoc; - mutable std::unique_ptr m_activeSiteHandler; mutable std::unique_ptr m_preferences; DISABLE_COPYING(Context); diff --git a/src/app/doc.cpp b/src/app/doc.cpp index c6168aa35..ee23ff21b 100644 --- a/src/app/doc.cpp +++ b/src/app/doc.cpp @@ -70,6 +70,14 @@ Doc::Doc(Sprite* sprite) Doc::~Doc() { DOC_TRACE("DOC: Deleting", this); + + try { + notify_observers(&DocObserver::onDestroy, this); + } + catch (...) { + LOG(ERROR, "DOC: Exception on DocObserver::onDestroy()\n"); + } + removeFromContext(); } diff --git a/src/app/doc_observer.h b/src/app/doc_observer.h index 7c958c120..052d829fd 100644 --- a/src/app/doc_observer.h +++ b/src/app/doc_observer.h @@ -17,6 +17,7 @@ namespace app { public: virtual ~DocObserver() { } + virtual void onDestroy(Doc* doc) { } virtual void onFileNameChanged(Doc* doc) { } // General update. If an observer receives this event, it's because @@ -80,8 +81,6 @@ namespace app { // Slices virtual void onSliceNameChange(DocEvent& ev) { } - // Called to destroy the observable. (Here you could call "delete this".) - virtual void dispose() { } }; } // namespace app diff --git a/src/app/doc_undo_observer.h b/src/app/doc_undo_observer.h index 9808b55b5..aa7feb836 100644 --- a/src/app/doc_undo_observer.h +++ b/src/app/doc_undo_observer.h @@ -1,4 +1,5 @@ // Aseprite +// Copyright (C) 2021 Igara Studio S.A. // Copyright (C) 2015-2018 David Capello // // This program is distributed under the terms of @@ -19,12 +20,12 @@ namespace app { class DocUndoObserver { public: virtual ~DocUndoObserver() { } - virtual void onAddUndoState(DocUndo* history) = 0; + virtual void onAddUndoState(DocUndo* history) { } virtual void onDeleteUndoState(DocUndo* history, - undo::UndoState* state) = 0; - virtual void onCurrentUndoStateChange(DocUndo* history) = 0; - virtual void onClearRedo(DocUndo* history) = 0; - virtual void onTotalUndoSizeChange(DocUndo* history) = 0; + undo::UndoState* state) { } + virtual void onCurrentUndoStateChange(DocUndo* history) { } + virtual void onClearRedo(DocUndo* history) { } + virtual void onTotalUndoSizeChange(DocUndo* history) { } }; } // namespace app diff --git a/src/app/script/app_object.cpp b/src/app/script/app_object.cpp index 700e452db..e1b5f6651 100644 --- a/src/app/script/app_object.cpp +++ b/src/app/script/app_object.cpp @@ -456,6 +456,12 @@ int App_useTool(lua_State* L) return 0; } +int App_get_events(lua_State* L) +{ + push_app_events(L); + return 1; +} + int App_get_activeSprite(lua_State* L) { app::Context* ctx = App::instance()->context(); @@ -736,6 +742,7 @@ const Property App_properties[] = { { "range", App_get_range, nullptr }, { "isUIAvailable", App_get_isUIAvailable, nullptr }, { "defaultPalette", App_get_defaultPalette, App_set_defaultPalette }, + { "events", App_get_events, nullptr }, { nullptr, nullptr, nullptr } }; diff --git a/src/app/script/engine.cpp b/src/app/script/engine.cpp index cf6c7f7b5..b9034037f 100644 --- a/src/app/script/engine.cpp +++ b/src/app/script/engine.cpp @@ -155,6 +155,7 @@ void register_color_space_class(lua_State* L); #ifdef ENABLE_UI void register_dialog_class(lua_State* L); #endif +void register_events_class(lua_State* L); void register_frame_class(lua_State* L); void register_frames_class(lua_State* L); void register_image_class(lua_State* L); @@ -387,6 +388,7 @@ Engine::Engine() #ifdef ENABLE_UI register_dialog_class(L); #endif + register_events_class(L); register_frame_class(L); register_frames_class(L); register_image_class(L); diff --git a/src/app/script/engine.h b/src/app/script/engine.h index 0c8717643..3c3c0b7f6 100644 --- a/src/app/script/engine.h +++ b/src/app/script/engine.h @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2018-2020 Igara Studio S.A. +// Copyright (C) 2018-2021 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -121,6 +121,7 @@ namespace app { EngineDelegate* m_oldDelegate; }; + void push_app_events(lua_State* L); int push_image_iterator_function(lua_State* L, const doc::Image* image, int extraArgIndex); void push_brush(lua_State* L, const doc::BrushRef& brush); void push_cel_image(lua_State* L, doc::Cel* cel); @@ -136,6 +137,7 @@ namespace app { void push_palette(lua_State* L, doc::Palette* palette); void push_plugin(lua_State* L, Extension* ext); void push_sprite_cel(lua_State* L, doc::Cel* cel); + void push_sprite_events(lua_State* L, doc::Sprite* sprite); void push_sprite_frame(lua_State* L, doc::Sprite* sprite, doc::frame_t frame); void push_sprite_frames(lua_State* L, doc::Sprite* sprite); void push_sprite_frames(lua_State* L, doc::Sprite* sprite, const std::vector& frames); diff --git a/src/app/script/events_class.cpp b/src/app/script/events_class.cpp new file mode 100644 index 000000000..1bf254bb2 --- /dev/null +++ b/src/app/script/events_class.cpp @@ -0,0 +1,343 @@ +// Aseprite +// Copyright (C) 2021 Igara Studio S.A. +// +// This program is distributed under the terms of +// the End-User License Agreement for Aseprite. + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "app/app.h" +#include "app/context.h" +#include "app/context_observer.h" +#include "app/doc.h" +#include "app/doc_undo.h" +#include "app/doc_undo_observer.h" +#include "app/script/docobj.h" +#include "app/script/engine.h" +#include "app/script/luacpp.h" +#include "doc/document.h" +#include "doc/sprite.h" + +#include +#include +#include + +namespace app { +namespace script { + +using namespace doc; + +namespace { + +using EventListener = int; + +class AppEvents; +class SpriteEvents; +static std::unique_ptr g_appEvents; +static std::map> g_spriteEvents; + +class Events { +public: + using EventType = int; + + Events() { } + virtual ~Events() { } + Events(const Events&) = delete; + Events& operator=(const Events&) = delete; + + virtual EventType eventType(const char* eventName) const = 0; + + bool hasListener(EventListener callbackRef) const { + for (auto& listeners : m_listeners) { + for (EventListener listener : listeners) { + if (listener == callbackRef) + return true; + } + } + return false; + } + + void add(EventType eventType, EventListener callbackRef) { + if (eventType >= m_listeners.size()) + m_listeners.resize(eventType+1); + + auto& listeners = m_listeners[eventType]; + listeners.push_back(callbackRef); + if (listeners.size() == 1) + onAddFirstListener(eventType); + } + + void remove(EventListener callbackRef) { + for (int i=0; i= m_listeners.size()) + return; + + script::Engine* engine = App::instance()->scriptEngine(); + lua_State* L = engine->luaState(); + + try { + for (EventListener callbackRef : m_listeners[eventType]) { + lua_rawgeti(L, LUA_REGISTRYINDEX, callbackRef); + if (lua_pcall(L, 0, 0, 0)) { + if (const char* s = lua_tostring(L, -1)) + engine->consolePrint(s); + } + } + } + catch (const std::exception& ex) { + engine->consolePrint(ex.what()); + } + } + +private: + virtual void onAddFirstListener(EventType eventType) = 0; + virtual void onRemoveLastListener(EventType eventType) = 0; + + using EventListeners = std::vector; + std::vector m_listeners; +}; + +class AppEvents : public Events + , private ContextObserver { +public: + enum : EventType { Unknown = -1, SiteChange }; + + AppEvents() { + } + + EventType eventType(const char* eventName) const { + if (std::strcmp(eventName, "sitechange") == 0) + return SiteChange; + else + return Unknown; + } + +private: + + void onAddFirstListener(EventType eventType) override { + switch (eventType) { + case SiteChange: { + App::instance()->context()->add_observer(this); + break; + } + } + } + + void onRemoveLastListener(EventType eventType) override { + switch (eventType) { + case SiteChange: { + App::instance()->context()->remove_observer(this); + break; + } + } + } + + // ContextObserver impl + void onActiveSiteChange(const Site& site) override { call(SiteChange); } +}; + +class SpriteEvents : public Events + , public DocUndoObserver + , public DocObserver { +public: + enum : EventType { Unknown = -1, Change }; + + SpriteEvents(const Sprite* sprite) + : m_spriteId(sprite->id()) { + doc()->add_observer(this); + } + + ~SpriteEvents() { + if (m_observingUndo) { + doc()->undoHistory()->remove_observer(this); + m_observingUndo = false; + } + doc()->remove_observer(this); + } + + EventType eventType(const char* eventName) const { + if (std::strcmp(eventName, "change") == 0) + return Change; + else + return Unknown; + } + + // DocObserver impl + void onDestroy(Doc* doc) override { + auto it = g_spriteEvents.find(m_spriteId); + ASSERT(it != g_spriteEvents.end()); + if (it != g_spriteEvents.end()) + g_spriteEvents.erase(it); + } + + // DocUndoObserver impl + void onAddUndoState(DocUndo* history) override { call(Change); } + void onCurrentUndoStateChange(DocUndo* history) override { call(Change); } + +private: + + void onAddFirstListener(EventType eventType) override { + switch (eventType) { + case Change: + ASSERT(!m_observingUndo); + doc()->undoHistory()->add_observer(this); + m_observingUndo = true; + break; + } + } + + void onRemoveLastListener(EventType eventType) override { + switch (eventType) { + case Change: { + if (m_observingUndo) { + doc()->undoHistory()->remove_observer(this); + m_observingUndo = false; + } + break; + } + } + } + + Doc* doc() { + Sprite* sprite = doc::get(m_spriteId); + if (sprite) + return static_cast(sprite->document()); + else + return nullptr; + } + + ObjectId m_spriteId; + bool m_observingUndo = false; +}; + +int Events_on(lua_State* L) +{ + auto evs = get_ptr(L, 1); + const char* eventName = lua_tostring(L, 2); + if (!eventName) + return 0; + + const int type = evs->eventType(eventName); + if (type < 0) + return luaL_error(L, "invalid event name to listen"); + + if (!lua_isfunction(L, 3)) + return luaL_error(L, "second argument must be a function"); + + // Copy the callback function to add it to the global registry + lua_pushvalue(L, 3); + int callbackRef = luaL_ref(L, LUA_REGISTRYINDEX); + evs->add(type, callbackRef); + + // Return the callback ref (this is an EventListener easier to use + // in Events_off()) + lua_pushinteger(L, callbackRef); + return 1; +} + +int Events_off(lua_State* L) +{ + auto evs = get_ptr(L, 1); + int callbackRef = LUA_REFNIL; + + // Remove by listener value + if (lua_isinteger(L, 2)) { + callbackRef = lua_tointeger(L, 2); + } + // Remove by function reference + else if (lua_isfunction(L, 2)) { + lua_pushnil(L); + while (lua_next(L, LUA_REGISTRYINDEX) != 0) { + if (lua_isnumber(L, -2) && + lua_isfunction(L, -1)) { + int i = lua_tointeger(L, -2); + if (// Compare value=function in 2nd argument + lua_compare(L, -1, 2, LUA_OPEQ) && + // Check that this Events contain this reference + evs->hasListener(i)) { + callbackRef = i; + lua_pop(L, 2); // Pop value and key + break; + } + } + lua_pop(L, 1); // Pop the value, leave the key for next lua_next() + } + } + else { + return luaL_error(L, "first argument must be a function or a EventListener"); + } + + if (callbackRef != LUA_REFNIL) { + evs->remove(callbackRef); + luaL_unref(L, LUA_REGISTRYINDEX, callbackRef); + } + return 0; +} + +// We don't need a __gc (to call ~Events()), because Events instances +// will be deleted when the Sprite is deleted or on App Exit +const luaL_Reg Events_methods[] = { + { "on", Events_on }, + { "off", Events_off }, + { nullptr, nullptr } +}; + +} // anonymous namespace + +DEF_MTNAME(Events); + +void register_events_class(lua_State* L) +{ + REG_CLASS(L, Events); +} + +void push_app_events(lua_State* L) +{ + if (!g_appEvents) { + App::instance()->Exit.connect([]{ g_appEvents.reset(); }); + g_appEvents.reset(new AppEvents); + } + push_ptr(L, g_appEvents.get()); +} + +void push_sprite_events(lua_State* L, Sprite* sprite) +{ + ASSERT(sprite); + + SpriteEvents* spriteEvents; + + auto it = g_spriteEvents.find(sprite->id()); + if (it != g_spriteEvents.end()) + spriteEvents = it->second.get(); + else { + spriteEvents = new SpriteEvents(sprite); + g_spriteEvents[sprite->id()].reset(spriteEvents); + } + + push_ptr(L, spriteEvents); +} + +} // namespace script +} // namespace app diff --git a/src/app/script/site_class.cpp b/src/app/script/site_class.cpp index 11d7d624e..466ae8ec6 100644 --- a/src/app/script/site_class.cpp +++ b/src/app/script/site_class.cpp @@ -26,47 +26,6 @@ namespace script { namespace { -class ScriptSiteObserver : public ContextObserver { -public: - static ScriptSiteObserver* instance() { - static ScriptSiteObserver instance; - return &instance; - } - - void onActiveSiteChange(const Site& site) { - if (m_onchangeRef == 0) - return; - - script::Engine* engine = App::instance()->scriptEngine(); - lua_State* L = engine->luaState(); - - lua_rawgeti(L, LUA_REGISTRYINDEX, m_onchangeRef); - if (lua_pcall(L, 0, 0, 0)) { - if (const char* s = lua_tostring(L, -1)) { - Console().printf("Error: %s", s); - } - } - } - - int callbackRef() { - return m_onchangeRef; - } - - void setCallbackRef(int onchangeRef) { - m_onchangeRef = onchangeRef; - } - -private: - ScriptSiteObserver() - : ContextObserver(), - m_onchangeRef(0) - { } - - ~ScriptSiteObserver() { } - - int m_onchangeRef; -}; - int Site_get_sprite(lua_State* L) { auto site = get_obj(L, 1); @@ -124,33 +83,6 @@ int Site_get_image(lua_State* L) return 1; } -int Site_get_onChange(lua_State* L) -{ - lua_rawgeti(L, LUA_REGISTRYINDEX, ScriptSiteObserver::instance()->callbackRef()); - return 1; -} - -int Site_set_onChange(lua_State* L) -{ - auto site = get_obj(L, 1); - auto obs = ScriptSiteObserver::instance(); - - if (lua_isfunction(L, 2)) { - int onchangeRef = luaL_ref(L, LUA_REGISTRYINDEX); - if (!obs->callbackRef()) { - App::instance()->context()->add_observer(obs); - } - obs->setCallbackRef(onchangeRef); - } - else if (obs->callbackRef()) { - luaL_unref(L, LUA_REGISTRYINDEX, obs->callbackRef()); - obs->setCallbackRef(0); - App::instance()->context()->remove_observer(obs); - } - - return 0; -} - const luaL_Reg Site_methods[] = { { nullptr, nullptr } }; @@ -162,7 +94,6 @@ const Property Site_properties[] = { { "frame", Site_get_frame, nullptr }, { "frameNumber", Site_get_frameNumber, nullptr }, { "image", Site_get_image, nullptr }, - { "onChange", Site_get_onChange, Site_set_onChange }, { nullptr, nullptr, nullptr } }; diff --git a/src/app/script/sprite_class.cpp b/src/app/script/sprite_class.cpp index cfcdbdd5f..e2db05306 100644 --- a/src/app/script/sprite_class.cpp +++ b/src/app/script/sprite_class.cpp @@ -59,41 +59,6 @@ namespace app { namespace script { namespace { -// in lua, observers become `Sprite.onChange` -class ScriptDocObserver : public DocUndoObserver { -public: - ScriptDocObserver(int callbackRef) - : DocUndoObserver(), - m_callbackRef(callbackRef) - { } - - void onAddUndoState(DocUndo* history) { callback(); } - void onDeleteUndoState(DocUndo* history, undo::UndoState* state) { } - void onCurrentUndoStateChange(DocUndo* history) { callback(); } - void onClearRedo(DocUndo* history) { } - void onTotalUndoSizeChange(DocUndo* history) { } - - int callbackRef() { return m_callbackRef; } - void setCallbackRef(int callbackRef) { m_callbackRef = callbackRef; } - -private: - void callback() { - script::Engine* engine = App::instance()->scriptEngine(); - lua_State* L = engine->luaState(); - - lua_rawgeti(L, LUA_REGISTRYINDEX, m_callbackRef); - if (lua_pcall(L, 0, 0, 0)) { - if (const char* s = lua_tostring(L, -1)) { - Console().printf("Error: %s", s); - } - } - } - - int m_callbackRef; -}; - -// used to maintain one-to-one relation between sprites and observers -std::map script_observers; int Sprite_new(lua_State* L) { @@ -627,47 +592,13 @@ int Sprite_deleteSlice(lua_State* L) } } -int Sprite_get_onChange(lua_State* L) +int Sprite_get_events(lua_State* L) { auto sprite = get_docobj(L, 1); - ScriptDocObserver* obs = script_observers[sprite->id()]; - - if (obs) { - lua_rawgeti(L, LUA_REGISTRYINDEX, obs->callbackRef()); - } - else { - lua_pushnil(L); - } + push_sprite_events(L, sprite); return 1; } -int Sprite_set_onChange(lua_State* L) -{ - auto sprite = get_docobj(L, 1); - auto doc = static_cast(sprite->document()); - ScriptDocObserver* obs = script_observers[sprite->id()]; - - if (lua_isfunction(L, 2)) { - int callbackRef = luaL_ref(L, LUA_REGISTRYINDEX); - if (obs) { - obs->setCallbackRef(callbackRef); - } - else { - obs = new ScriptDocObserver(callbackRef); - doc->undoHistory()->add_observer(obs); - script_observers[sprite->id()] = obs; - } - } - else if (obs) { - doc->undoHistory()->remove_observer(obs); - script_observers.erase(sprite->id()); - luaL_unref(L, LUA_REGISTRYINDEX, obs->callbackRef()); - delete obs; - } - - return 0; -} - int Sprite_get_filename(lua_State* L) { auto sprite = get_docobj(L, 1); @@ -922,7 +853,7 @@ const Property Sprite_properties[] = { { "bounds", Sprite_get_bounds, nullptr }, { "gridBounds", Sprite_get_gridBounds, Sprite_set_gridBounds }, { "pixelRatio", Sprite_get_pixelRatio, Sprite_set_pixelRatio }, - { "onChange", Sprite_get_onChange, Sprite_set_onChange }, + { "events", Sprite_get_events, nullptr }, { nullptr, nullptr, nullptr } }; diff --git a/src/app/ui_context.cpp b/src/app/ui_context.cpp index e2f1a5438..3270414bc 100644 --- a/src/app/ui_context.cpp +++ b/src/app/ui_context.cpp @@ -134,10 +134,10 @@ void UIContext::setActiveView(DocView* docView) notifyActiveSiteChanged(); } -void UIContext::onSetActiveDocument(Doc* document) +void UIContext::onSetActiveDocument(Doc* document, bool notify) { - bool notify = (lastSelectedDoc() != document); - app::Context::onSetActiveDocument(document); + notify = (notify && lastSelectedDoc() != document); + app::Context::onSetActiveDocument(document, false); DocView* docView = getFirstDocView(document); if (docView) { // The view can be null if we are in --batch mode diff --git a/src/app/ui_context.h b/src/app/ui_context.h index 35472d908..aa4d5eb81 100644 --- a/src/app/ui_context.h +++ b/src/app/ui_context.h @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2019-2020 Igara Studio S.A. +// Copyright (C) 2019-2021 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -53,7 +53,7 @@ namespace app { void onAddDocument(Doc* doc) override; void onRemoveDocument(Doc* doc) override; void onGetActiveSite(Site* site) const override; - void onSetActiveDocument(Doc* doc) override; + void onSetActiveDocument(Doc* doc, bool notify) override; void onSetActiveLayer(doc::Layer* layer) override; void onSetActiveFrame(const doc::frame_t frame) override; void onSetRange(const DocRange& range) override; From 4cd137a6398292ed7b02a4339060eaf72f9c6118 Mon Sep 17 00:00:00 2001 From: David Capello Date: Thu, 7 Oct 2021 19:10:27 -0300 Subject: [PATCH 21/23] [lua] Add Image.rowStride property now that Image.bytes is available We need to know the specific rowStride to be able to set a proper Image.bytes (bytes size = Image.height * Image.rowStride). --- src/app/script/image_class.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/app/script/image_class.cpp b/src/app/script/image_class.cpp index 857e44e95..785237852 100644 --- a/src/app/script/image_class.cpp +++ b/src/app/script/image_class.cpp @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2018-2020 Igara Studio S.A. +// Copyright (C) 2018-2021 Igara Studio S.A. // Copyright (C) 2015-2018 David Capello // // This program is distributed under the terms of @@ -484,6 +484,13 @@ int Image_resize(lua_State* L) return 0; } +int Image_get_rowStride(lua_State* L) +{ + const auto obj = get_obj(L, 1); + lua_pushinteger(L, obj->image(L)->getRowStrideSize()); + return 1; +} + int Image_get_bytes(lua_State* L) { const auto img = get_obj(L, 1)->image(L); @@ -562,6 +569,7 @@ const luaL_Reg Image_methods[] = { }; const Property Image_properties[] = { + { "rowStride", Image_get_rowStride, nullptr }, { "bytes", Image_get_bytes, Image_set_bytes }, { "width", Image_get_width, nullptr }, { "height", Image_get_height, nullptr }, From b9241e6d91e53a303690ba657e3236252f5f375f Mon Sep 17 00:00:00 2001 From: David Capello Date: Thu, 7 Oct 2021 19:34:40 -0300 Subject: [PATCH 22/23] Refactor ask_access() with ResourceType argument --- src/app/script/app_fs_object.cpp | 6 +++--- src/app/script/app_object.cpp | 2 +- src/app/script/engine.h | 7 ------- src/app/script/image_class.cpp | 2 +- src/app/script/palette_class.cpp | 6 +++--- src/app/script/security.cpp | 24 +++++++++++++++--------- src/app/script/security.h | 15 ++++++++++++++- src/app/script/sprite_class.cpp | 4 ++-- 8 files changed, 39 insertions(+), 27 deletions(-) diff --git a/src/app/script/app_fs_object.cpp b/src/app/script/app_fs_object.cpp index 1f872377c..0250f4b97 100644 --- a/src/app/script/app_fs_object.cpp +++ b/src/app/script/app_fs_object.cpp @@ -126,7 +126,7 @@ int AppFS_makeDirectory(lua_State* L) return 1; } - if (!ask_access(L, path, FileAccessMode::Write, true)) + if (!ask_access(L, path, FileAccessMode::Full, ResourceType::File)) return luaL_error(L, "the script doesn't have access to create the directory '%s'", path); try { @@ -148,7 +148,7 @@ int AppFS_makeAllDirectories(lua_State* L) return 1; } - if (!ask_access(L, path, FileAccessMode::Write, true)) + if (!ask_access(L, path, FileAccessMode::Write, ResourceType::File)) return luaL_error(L, "the script doesn't have access to create all directories '%s'", path); try { @@ -170,7 +170,7 @@ int AppFS_removeDirectory(lua_State* L) return 1; } - if (!ask_access(L, path, FileAccessMode::Write, true)) + if (!ask_access(L, path, FileAccessMode::Write, ResourceType::File)) return luaL_error(L, "the script doesn't have access to remove the directory '%s'", path); try { diff --git a/src/app/script/app_object.cpp b/src/app/script/app_object.cpp index e1b5f6651..2792cee76 100644 --- a/src/app/script/app_object.cpp +++ b/src/app/script/app_object.cpp @@ -58,7 +58,7 @@ int load_sprite_from_file(lua_State* L, const char* filename, const LoadSpriteFromFileParam param) { std::string absFn = base::get_absolute_path(filename); - if (!ask_access(L, absFn.c_str(), FileAccessMode::Read, true)) + if (!ask_access(L, absFn.c_str(), FileAccessMode::Read, ResourceType::File)) return luaL_error(L, "script doesn't have access to open file %s", absFn.c_str()); diff --git a/src/app/script/engine.h b/src/app/script/engine.h index 3c3c0b7f6..6db3fce94 100644 --- a/src/app/script/engine.h +++ b/src/app/script/engine.h @@ -58,13 +58,6 @@ namespace app { namespace script { - enum class FileAccessMode { - Execute = 1, - Write = 2, - Read = 4, - Full = 7 - }; - class EngineDelegate { public: virtual ~EngineDelegate() { } diff --git a/src/app/script/image_class.cpp b/src/app/script/image_class.cpp index 785237852..cc69bc04b 100644 --- a/src/app/script/image_class.cpp +++ b/src/app/script/image_class.cpp @@ -367,7 +367,7 @@ int Image_saveAs(lua_State* L) return luaL_error(L, "missing filename in Image:saveAs()"); std::string absFn = base::get_absolute_path(fn); - if (!ask_access(L, absFn.c_str(), FileAccessMode::Write, true)) + if (!ask_access(L, absFn.c_str(), FileAccessMode::Write, ResourceType::File)) return luaL_error(L, "script doesn't have access to write file %s", absFn.c_str()); diff --git a/src/app/script/palette_class.cpp b/src/app/script/palette_class.cpp index 05cf8ec8b..d926b2c6f 100644 --- a/src/app/script/palette_class.cpp +++ b/src/app/script/palette_class.cpp @@ -76,7 +76,7 @@ int Palette_new(lua_State* L) std::string absFn = base::get_absolute_path(fromFile); lua_pop(L, 1); - if (!ask_access(L, absFn.c_str(), FileAccessMode::Read, true)) + if (!ask_access(L, absFn.c_str(), FileAccessMode::Read, ResourceType::File)) return luaL_error(L, "script doesn't have access to open file %s", absFn.c_str()); @@ -105,7 +105,7 @@ int Palette_new(lua_State* L) if (!idAndPaths[id].empty()) { std::string absFn = base::get_absolute_path(idAndPaths[id]); - if (!ask_access(L, absFn.c_str(), FileAccessMode::Read, true)) + if (!ask_access(L, absFn.c_str(), FileAccessMode::Read, ResourceType::File)) return luaL_error(L, "script doesn't have access to open file %s", absFn.c_str()); @@ -240,7 +240,7 @@ int Palette_saveAs(lua_State* L) const char* fn = luaL_checkstring(L, 2); if (fn) { std::string absFn = base::get_absolute_path(fn); - if (!ask_access(L, absFn.c_str(), FileAccessMode::Write, true)) + if (!ask_access(L, absFn.c_str(), FileAccessMode::Write, ResourceType::File)) return luaL_error(L, "script doesn't have access to write file %s", absFn.c_str()); save_palette(absFn.c_str(), pal, pal->size()); diff --git a/src/app/script/security.cpp b/src/app/script/security.cpp index d9f077173..ae9170869 100644 --- a/src/app/script/security.cpp +++ b/src/app/script/security.cpp @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2019 Igara Studio S.A. +// Copyright (C) 2019-2021 Igara Studio S.A. // Copyright (C) 2018 David Capello // // This program is distributed under the terms of @@ -81,7 +81,7 @@ int secure_io_open(lua_State* L) mode = FileAccessMode::Write; } - if (!ask_access(L, absFilename.c_str(), mode, true)) { + if (!ask_access(L, absFilename.c_str(), mode, ResourceType::File)) { return luaL_error(L, "the script doesn't have access to file '%s'", absFilename.c_str()); } @@ -101,7 +101,7 @@ int secure_os_execute(lua_State* L) return 0; const char* cmd = lua_tostring(L, 1); - if (!ask_access(L, cmd, FileAccessMode::Execute, false)) { + if (!ask_access(L, cmd, FileAccessMode::Execute, ResourceType::Command)) { // Stop script return luaL_error(L, "the script doesn't have access to execute the command: '%s'", cmd); @@ -117,7 +117,7 @@ int secure_os_execute(lua_State* L) bool ask_access(lua_State* L, const char* filename, const FileAccessMode mode, - const bool canOpenFile) + const ResourceType resourceType) { #ifdef ENABLE_UI // Ask for permission to open the file @@ -144,10 +144,16 @@ bool ask_access(lua_State* L, app::gen::ScriptAccess dlg; dlg.script()->setText(script); - dlg.fileLabel()->setText( - canOpenFile ? - Strings::script_access_file_label(): - Strings::script_access_command_label()); + + { + std::string label; + switch (resourceType) { + case ResourceType::File: label = Strings::script_access_file_label(); break; + case ResourceType::Command: label = Strings::script_access_command_label(); break; + } + dlg.fileLabel()->setText(label); + } + dlg.file()->setText(filename); dlg.allow()->setText(allowButtonText); dlg.allow()->processMnemonicFromText(); @@ -174,7 +180,7 @@ bool ask_access(lua_State* L, } }); - if (canOpenFile) { + if (resourceType == ResourceType::File) { dlg.file()->Click.connect( [&dlg]{ std::string fn = dlg.file()->text(); diff --git a/src/app/script/security.h b/src/app/script/security.h index 4e7b1741b..9ee283471 100644 --- a/src/app/script/security.h +++ b/src/app/script/security.h @@ -1,4 +1,5 @@ // Aseprite +// Copyright (C) 2021 Igara Studio S.A. // Copyright (C) 2018 David Capello // // This program is distributed under the terms of @@ -17,13 +18,25 @@ namespace app { namespace script { + enum class FileAccessMode { + Execute = 1, + Write = 2, + Read = 4, + Full = 7, + }; + + enum class ResourceType { + File, + Command, + }; + int secure_io_open(lua_State* L); int secure_os_execute(lua_State* L); bool ask_access(lua_State* L, const char* filename, const FileAccessMode mode, - const bool canOpenFile); + const ResourceType resourceType); } // namespace script } // namespace app diff --git a/src/app/script/sprite_class.cpp b/src/app/script/sprite_class.cpp index e2db05306..6b60cbf41 100644 --- a/src/app/script/sprite_class.cpp +++ b/src/app/script/sprite_class.cpp @@ -208,7 +208,7 @@ int Sprite_saveAs_base(lua_State* L, std::string& absFn) appCtx->setActiveDocument(doc); absFn = base::get_absolute_path(fn); - if (!ask_access(L, absFn.c_str(), FileAccessMode::Write, true)) + if (!ask_access(L, absFn.c_str(), FileAccessMode::Write, ResourceType::File)) return luaL_error(L, "script doesn't have access to write file %s", absFn.c_str()); @@ -267,7 +267,7 @@ int Sprite_loadPalette(lua_State* L) const char* fn = luaL_checkstring(L, 2); if (fn && sprite) { std::string absFn = base::get_absolute_path(fn); - if (!ask_access(L, absFn.c_str(), FileAccessMode::Read, true)) + if (!ask_access(L, absFn.c_str(), FileAccessMode::Read, ResourceType::File)) return luaL_error(L, "script doesn't have access to open file %s", absFn.c_str()); From 6e84bb5443da89dc7ccb7c0823b15ef080cd9341 Mon Sep 17 00:00:00 2001 From: David Capello Date: Thu, 7 Oct 2021 19:37:10 -0300 Subject: [PATCH 23/23] [lua] Ask for permission to open a WebSocket from scripts --- data/strings/en.ini | 2 ++ src/app/script/security.cpp | 3 +++ src/app/script/security.h | 2 ++ src/app/script/websocket_class.cpp | 9 ++++++--- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/data/strings/en.ini b/data/strings/en.ini index fbfa87cda..310bd3397 100644 --- a/data/strings/en.ini +++ b/data/strings/en.ini @@ -1377,11 +1377,13 @@ title = Security script_label = The following script: file_label = wants to access to this file: command_label = wants to execute the following command: +websocket_label = wants to open a WebSocket connection to this URL: dont_show_for_this_access = Don't show this specific alert again for this script dont_show_for_this_script = Give full trust to this script allow_execute_access = &Allow Execute Access allow_write_access = &Allow Write Access allow_read_access = &Allow Read Access +allow_open_conn_access = &Allow to Open Connections give_full_access = Give Script Full &Access stop_script = &Stop Script diff --git a/src/app/script/security.cpp b/src/app/script/security.cpp index ae9170869..477b98860 100644 --- a/src/app/script/security.cpp +++ b/src/app/script/security.cpp @@ -136,6 +136,8 @@ bool ask_access(lua_State* L, return true; std::string allowButtonText = + mode == FileAccessMode::OpenSocket ? + Strings::script_access_allow_open_conn_access(): mode == FileAccessMode::Execute ? Strings::script_access_allow_execute_access(): mode == FileAccessMode::Write ? @@ -150,6 +152,7 @@ bool ask_access(lua_State* L, switch (resourceType) { case ResourceType::File: label = Strings::script_access_file_label(); break; case ResourceType::Command: label = Strings::script_access_command_label(); break; + case ResourceType::WebSocket: label = Strings::script_access_websocket_label(); break; } dlg.fileLabel()->setText(label); } diff --git a/src/app/script/security.h b/src/app/script/security.h index 9ee283471..ffaae2cb0 100644 --- a/src/app/script/security.h +++ b/src/app/script/security.h @@ -23,11 +23,13 @@ namespace script { Write = 2, Read = 4, Full = 7, + OpenSocket = 8, }; enum class ResourceType { File, Command, + WebSocket, }; int secure_io_open(lua_State* L); diff --git a/src/app/script/websocket_class.cpp b/src/app/script/websocket_class.cpp index 32be7d365..9aaab4519 100644 --- a/src/app/script/websocket_class.cpp +++ b/src/app/script/websocket_class.cpp @@ -1,6 +1,5 @@ // Aseprite -// Copyright (C) 2018-2020 Igara Studio S.A. -// Copyright (C) 2018 David Capello +// Copyright (C) 2021 Igara Studio S.A. // // This program is distributed under the terms of // the End-User License Agreement for Aseprite. @@ -13,6 +12,7 @@ #include "app/console.h" #include "app/script/engine.h" #include "app/script/luacpp.h" +#include "app/script/security.h" #include "ui/system.h" #include @@ -39,6 +39,9 @@ int WebSocket_new(lua_State* L) if (lua_istable(L, 1)) { lua_getfield(L, 1, "url"); if (const char* s = lua_tostring(L, -1)) { + if (!ask_access(L, s, FileAccessMode::OpenSocket, ResourceType::WebSocket)) + return luaL_error(L, "the script doesn't have access to create a WebSocket for '%s'", s); + ws->setUrl(s); } lua_pop(L, 1); @@ -180,7 +183,7 @@ const Property WebSocket_properties[] = { { nullptr, nullptr, nullptr } }; -} // namespace { } +} // anonymous namespace using WebSocket = ix::WebSocket; DEF_MTNAME(WebSocket);