From 76a398b16205f2d003a0bee3f4fd4a168c46585d Mon Sep 17 00:00:00 2001 From: David Capello Date: Fri, 30 Dec 2022 14:51:43 -0300 Subject: [PATCH 01/11] [lua] Add access to user data properties in Sprite object (aseprite/api#88) This is a basic implementation where we can only access basic properties (not maps or vectors yet). --- src/app/CMakeLists.txt | 1 + src/app/script/engine.cpp | 2 + src/app/script/engine.h | 2 +- src/app/script/properties_class.cpp | 128 ++++++++++++++++++++++++++++ src/app/script/sprite_class.cpp | 1 + src/app/script/userdata.h | 7 ++ src/app/script/values.cpp | 47 +++++++++- tests/scripts/userdata.lua | 24 ++++++ 8 files changed, 208 insertions(+), 4 deletions(-) create mode 100644 src/app/script/properties_class.cpp create mode 100644 tests/scripts/userdata.lua diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 77777f573..67deee7ee 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -191,6 +191,7 @@ if(ENABLE_SCRIPTING) script/plugin_class.cpp script/point_class.cpp script/preferences_object.cpp + script/properties_class.cpp script/range_class.cpp script/rectangle_class.cpp script/security.cpp diff --git a/src/app/script/engine.cpp b/src/app/script/engine.cpp index c1c7e7f7c..6703456d0 100644 --- a/src/app/script/engine.cpp +++ b/src/app/script/engine.cpp @@ -176,6 +176,7 @@ void register_palette_class(lua_State* L); void register_palettes_class(lua_State* L); void register_plugin_class(lua_State* L); void register_point_class(lua_State* L); +void register_properties_class(lua_State* L); void register_range_class(lua_State* L); void register_rect_class(lua_State* L); void register_selection_class(lua_State* L); @@ -454,6 +455,7 @@ Engine::Engine() register_palettes_class(L); register_plugin_class(L); register_point_class(L); + register_properties_class(L); register_range_class(L); register_rect_class(L); register_selection_class(L); diff --git a/src/app/script/engine.h b/src/app/script/engine.h index 0b5514d05..525bf7c19 100644 --- a/src/app/script/engine.h +++ b/src/app/script/engine.h @@ -150,6 +150,7 @@ namespace app { void push_layers(lua_State* L, const doc::ObjectIds& layers); void push_palette(lua_State* L, doc::Palette* palette); void push_plugin(lua_State* L, Extension* ext); + void push_properties(lua_State* L, doc::WithUserData* userData); 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); @@ -166,7 +167,6 @@ namespace app { void push_tileset_image(lua_State* L, doc::Tileset* tileset, doc::Image* image); void push_tilesets(lua_State* L, doc::Tilesets* tilesets); void push_tool(lua_State* L, app::tools::Tool* tool); - void push_userdata(lua_State* L, doc::WithUserData* userData); void push_version(lua_State* L, const base::Version& ver); gfx::Point convert_args_into_point(lua_State* L, int index); diff --git a/src/app/script/properties_class.cpp b/src/app/script/properties_class.cpp new file mode 100644 index 000000000..6b5d38bd0 --- /dev/null +++ b/src/app/script/properties_class.cpp @@ -0,0 +1,128 @@ +// Aseprite +// Copyright (C) 2022 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/script/docobj.h" +#include "app/script/engine.h" +#include "app/script/luacpp.h" +#include "app/script/values.h" +#include "doc/with_user_data.h" + +#include + +namespace app { +namespace script { + +namespace { + +struct Properties { + doc::ObjectId id = 0; + Properties(doc::ObjectId id) : id(id) { } +}; + +int Properties_index(lua_State* L) +{ + auto propObj = get_obj(L, 1); + const char* field = lua_tostring(L, 2); + if (!field) + return luaL_error(L, "field in 'properties.field' must be a string"); + + auto obj = static_cast(get_object(propObj->id)); + if (!obj) + return luaL_error(L, "the object with these properties was destroyed"); + + auto& properties = obj->userData().properties(); + auto it = properties.find(field); + if (it != properties.end()) { + push_value_to_lua(L, (*it).second); + } + else { + lua_pushnil(L); + } + return 1; +} + +int Properties_newindex(lua_State* L) +{ + auto propObj = get_obj(L, 1); + const char* field = lua_tostring(L, 2); + if (!field) + return luaL_error(L, "field in 'properties.field' must be a string"); + + auto obj = static_cast(get_object(propObj->id)); + if (!obj) + return luaL_error(L, "the object with these properties was destroyed"); + + auto& properties = obj->userData().properties(); + + // TODO add undo information + switch (lua_type(L, 3)) { + + case LUA_TNONE: + case LUA_TNIL: + default: { + // Just erase the property + auto it = properties.find(field); + if (it != properties.end()) + properties.erase(it); + break; + } + + case LUA_TBOOLEAN: + properties[field] = (lua_toboolean(L, 3) ? true: false); + break; + + case LUA_TNUMBER: + if (lua_isinteger(L, 3)) + properties[field] = lua_tointeger(L, 3); + else { + properties[field] = doc::UserData::Fixed{ + fixmath::ftofix(lua_tonumber(L, 3)) + }; + } + break; + + case LUA_TSTRING: + properties[field] = lua_tostring(L, 3); + break; + + case LUA_TTABLE: + // TODO convert a full table into properties recursively + break; + + case LUA_TUSERDATA: + // TODO convert table-like objects (Size, Point, Rectangle, etc.) + break; + + } + return 1; +} + +const luaL_Reg Properties_methods[] = { + { "__index", Properties_index }, + { "__newindex", Properties_newindex }, + { nullptr, nullptr } +}; + +} // anonymous namespace + +DEF_MTNAME(Properties); + +void register_properties_class(lua_State* L) +{ + REG_CLASS(L, Properties); +} + +void push_properties(lua_State* L, doc::WithUserData* userData) +{ + push_obj(L, userData->id()); +} + +} // namespace script +} // namespace app diff --git a/src/app/script/sprite_class.cpp b/src/app/script/sprite_class.cpp index 7b127e1f2..7965babe7 100644 --- a/src/app/script/sprite_class.cpp +++ b/src/app/script/sprite_class.cpp @@ -882,6 +882,7 @@ const Property Sprite_properties[] = { { "gridBounds", Sprite_get_gridBounds, Sprite_set_gridBounds }, { "color", UserData_get_color, UserData_set_color }, { "data", UserData_get_text, UserData_set_text }, + { "properties", UserData_get_properties, nullptr }, { "pixelRatio", Sprite_get_pixelRatio, Sprite_set_pixelRatio }, { "events", Sprite_get_events, nullptr }, { nullptr, nullptr, nullptr } diff --git a/src/app/script/userdata.h b/src/app/script/userdata.h index 65af696b0..038a92e66 100644 --- a/src/app/script/userdata.h +++ b/src/app/script/userdata.h @@ -51,6 +51,13 @@ int UserData_get_color(lua_State* L) { return 1; } +template +int UserData_get_properties(lua_State* L) { + auto obj = get_docobj(L, 1); + push_properties(L, get_WithUserData(obj)); + return 1; +} + template int UserData_set_text(lua_State* L) { auto obj = get_docobj(L, 1); diff --git a/src/app/script/values.cpp b/src/app/script/values.cpp index 47ca4a960..e043a07d3 100644 --- a/src/app/script/values.cpp +++ b/src/app/script/values.cpp @@ -16,6 +16,7 @@ #include "doc/remap.h" #include +#include namespace app { namespace script { @@ -39,9 +40,21 @@ bool get_value_from_lua(lua_State* L, int index) { // int template<> -void push_value_to_lua(lua_State* L, const int& value) { - lua_pushinteger(L, value); -} +void push_value_to_lua(lua_State* L, const int8_t& value) { lua_pushinteger(L, value); } +template<> +void push_value_to_lua(lua_State* L, const int16_t& value) { lua_pushinteger(L, value); } +template<> +void push_value_to_lua(lua_State* L, const int32_t& value) { lua_pushinteger(L, value); } +template<> +void push_value_to_lua(lua_State* L, const int64_t& value) { lua_pushinteger(L, value); } +template<> +void push_value_to_lua(lua_State* L, const uint8_t& value) { lua_pushinteger(L, value); } +template<> +void push_value_to_lua(lua_State* L, const uint16_t& value) { lua_pushinteger(L, value); } +template<> +void push_value_to_lua(lua_State* L, const uint32_t& value) { lua_pushinteger(L, value); } +template<> +void push_value_to_lua(lua_State* L, const uint64_t& value) { lua_pushinteger(L, value); } template<> int get_value_from_lua(lua_State* L, int index) { @@ -61,6 +74,14 @@ double get_value_from_lua(lua_State* L, int index) { return lua_tonumber(L, index); } +// ---------------------------------------------------------------------- +// fixed + +template<> +void push_value_to_lua(lua_State* L, const doc::UserData::Fixed& value) { + lua_pushnumber(L, fixmath::fixtof(value.value)); +} + // ---------------------------------------------------------------------- // std::string @@ -193,10 +214,12 @@ app::tools::InkType get_value_from_lua(lua_State* L, int index) { // ---------------------------------------------------------------------- // doc::tile_t +#if 0 // doc::tile_t matches uint32_t, and we have the uint32_t version already defined template<> void push_value_to_lua(lua_State* L, const doc::tile_t& value) { lua_pushinteger(L, value); } +#endif template<> doc::tile_t get_value_from_lua(lua_State* L, int index) { @@ -251,5 +274,23 @@ FOR_ENUM(filters::HueSaturationFilter::Mode) FOR_ENUM(filters::TiledMode) FOR_ENUM(render::OnionskinPosition) +// ---------------------------------------------------------------------- +// UserData::Properties / VariantStruct + +template<> +void push_value_to_lua(lua_State* L, const doc::UserData::Properties& value) { + // TODO convert a Properties map into a Lua table +} + +template<> +void push_value_to_lua(lua_State* L, const doc::UserData::Vector& value) { + // TODO convert a Vector into a Lua table +} + +template<> +void push_value_to_lua(lua_State* L, const doc::UserData::Variant& value) { + std::visit([L](auto&& v){ push_value_to_lua(L, v); }, value); +} + } // namespace script } // namespace app diff --git a/tests/scripts/userdata.lua b/tests/scripts/userdata.lua new file mode 100644 index 000000000..5b035fcda --- /dev/null +++ b/tests/scripts/userdata.lua @@ -0,0 +1,24 @@ +-- Copyright (C) 2022 Igara Studio S.A. +-- +-- This file is released under the terms of the MIT license. +-- Read LICENSE.txt for more information. + +do + local spr = Sprite(1, 1) + + spr.properties.a = true + spr.properties.b = 1 + spr.properties.c = "hi" + spr.properties.d = 2.3 + assert(spr.properties.a == true) + assert(spr.properties.b == 1) + assert(spr.properties.c == "hi") + -- TODO we don't have too much precision saving fixed points in properties + assert(math.abs(spr.properties.d - 2.3) < 0.00001) + + spr.properties.a = false + assert(spr.properties.a == false) + + spr.properties.b = nil + assert(spr.properties.b == nil) +end From 574f58375332bb80ce5572fdedb1028617786e45 Mon Sep 17 00:00:00 2001 From: David Capello Date: Fri, 30 Dec 2022 18:04:46 -0300 Subject: [PATCH 02/11] Add std::visit() alternative using switch/case to push_value_to_lua() --- src/app/script/values.cpp | 53 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/src/app/script/values.cpp b/src/app/script/values.cpp index e043a07d3..f497eb9d8 100644 --- a/src/app/script/values.cpp +++ b/src/app/script/values.cpp @@ -289,7 +289,60 @@ void push_value_to_lua(lua_State* L, const doc::UserData::Vector& value) { template<> void push_value_to_lua(lua_State* L, const doc::UserData::Variant& value) { +#if 1 // We are targetting macOS 10.9, so we don't have the std::visit() available + switch (value.type()) { + case USER_DATA_PROPERTY_TYPE_BOOL: + push_value_to_lua(L, *std::get_if(&value)); + break; + case USER_DATA_PROPERTY_TYPE_INT8: + push_value_to_lua(L, *std::get_if(&value)); + break; + case USER_DATA_PROPERTY_TYPE_UINT8: + push_value_to_lua(L, *std::get_if(&value)); + break; + case USER_DATA_PROPERTY_TYPE_INT16: + push_value_to_lua(L, *std::get_if(&value)); + break; + case USER_DATA_PROPERTY_TYPE_UINT16: + push_value_to_lua(L, *std::get_if(&value)); + break; + case USER_DATA_PROPERTY_TYPE_INT32: + push_value_to_lua(L, *std::get_if(&value)); + break; + case USER_DATA_PROPERTY_TYPE_UINT32: + push_value_to_lua(L, *std::get_if(&value)); + break; + case USER_DATA_PROPERTY_TYPE_INT64: + push_value_to_lua(L, *std::get_if(&value)); + break; + case USER_DATA_PROPERTY_TYPE_UINT64: + push_value_to_lua(L, *std::get_if(&value)); + break; + case USER_DATA_PROPERTY_TYPE_FIXED: + push_value_to_lua(L, *std::get_if(&value)); + break; + case USER_DATA_PROPERTY_TYPE_STRING: + push_value_to_lua(L, *std::get_if(&value)); + break; + case USER_DATA_PROPERTY_TYPE_POINT: + push_value_to_lua(L, *std::get_if(&value)); + break; + case USER_DATA_PROPERTY_TYPE_SIZE: + push_value_to_lua(L, *std::get_if(&value)); + break; + case USER_DATA_PROPERTY_TYPE_RECT: + push_value_to_lua(L, *std::get_if(&value)); + break; + case USER_DATA_PROPERTY_TYPE_VECTOR: + push_value_to_lua(L, *std::get_if(&value)); + break; + case USER_DATA_PROPERTY_TYPE_PROPERTIES: + push_value_to_lua(L, *std::get_if(&value)); + break; + } +#else // TODO enable this in the future std::visit([L](auto&& v){ push_value_to_lua(L, v); }, value); +#endif } } // namespace script From c77c95181a3ee5c8889a83d6e3e978da76059139 Mon Sep 17 00:00:00 2001 From: David Capello Date: Mon, 2 Jan 2023 11:30:33 -0300 Subject: [PATCH 03/11] Fix MSVC issue converting a string literal (const char*) to bool when assigning to a variant More information: https://twitter.com/davidcapello/status/1609918634075226121 --- src/app/script/properties_class.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/script/properties_class.cpp b/src/app/script/properties_class.cpp index 6b5d38bd0..a04aebb3c 100644 --- a/src/app/script/properties_class.cpp +++ b/src/app/script/properties_class.cpp @@ -89,7 +89,7 @@ int Properties_newindex(lua_State* L) break; case LUA_TSTRING: - properties[field] = lua_tostring(L, 3); + properties[field] = std::string(lua_tostring(L, 3)); break; case LUA_TTABLE: From ef0596fd01b4f8b6664eb792df737503841067d4 Mon Sep 17 00:00:00 2001 From: David Capello Date: Mon, 2 Jan 2023 12:17:11 -0300 Subject: [PATCH 04/11] [lua] Add __len operator to Properties --- src/app/script/properties_class.cpp | 13 +++++++++++++ tests/scripts/userdata.lua | 1 + 2 files changed, 14 insertions(+) diff --git a/src/app/script/properties_class.cpp b/src/app/script/properties_class.cpp index a04aebb3c..8d91c55d1 100644 --- a/src/app/script/properties_class.cpp +++ b/src/app/script/properties_class.cpp @@ -26,6 +26,18 @@ struct Properties { Properties(doc::ObjectId id) : id(id) { } }; +int Properties_len(lua_State* L) +{ + auto propObj = get_obj(L, 1); + auto obj = static_cast(get_object(propObj->id)); + if (!obj) + return luaL_error(L, "the object with these properties was destroyed"); + + auto& properties = obj->userData().properties(); + lua_pushinteger(L, properties.size()); + return 1; +} + int Properties_index(lua_State* L) { auto propObj = get_obj(L, 1); @@ -105,6 +117,7 @@ int Properties_newindex(lua_State* L) } const luaL_Reg Properties_methods[] = { + { "__len", Properties_len }, { "__index", Properties_index }, { "__newindex", Properties_newindex }, { nullptr, nullptr } diff --git a/tests/scripts/userdata.lua b/tests/scripts/userdata.lua index 5b035fcda..9cb766d07 100644 --- a/tests/scripts/userdata.lua +++ b/tests/scripts/userdata.lua @@ -15,6 +15,7 @@ do assert(spr.properties.c == "hi") -- TODO we don't have too much precision saving fixed points in properties assert(math.abs(spr.properties.d - 2.3) < 0.00001) + assert(#spr.properties == 4) spr.properties.a = false assert(spr.properties.a == false) From 9f0491ddb6bfb8dd8d1aa10bf67ba65385857524 Mon Sep 17 00:00:00 2001 From: David Capello Date: Mon, 2 Jan 2023 14:36:49 -0300 Subject: [PATCH 05/11] [lua] Add support to iterate properties using pairs() --- src/app/script/properties_class.cpp | 41 +++++++++++++++++++++++++++++ tests/scripts/userdata.lua | 10 +++++++ 2 files changed, 51 insertions(+) diff --git a/src/app/script/properties_class.cpp b/src/app/script/properties_class.cpp index 8d91c55d1..7934128c3 100644 --- a/src/app/script/properties_class.cpp +++ b/src/app/script/properties_class.cpp @@ -26,6 +26,8 @@ struct Properties { Properties(doc::ObjectId id) : id(id) { } }; +using PropertiesIterator = doc::UserData::Properties::iterator; + int Properties_len(lua_State* L) { auto propObj = get_obj(L, 1); @@ -116,20 +118,59 @@ int Properties_newindex(lua_State* L) return 1; } +int Properties_pairs_next(lua_State* L) +{ + auto propObj = get_obj(L, 1); + auto obj = static_cast(get_object(propObj->id)); + if (!obj) + return luaL_error(L, "the object with these properties was destroyed"); + + auto& properties = obj->userData().properties(); + auto& it = *get_obj(L, lua_upvalueindex(1)); + if (it == properties.end()) + return 0; + lua_pushstring(L, (*it).first.c_str()); + push_value_to_lua(L, (*it).second); + ++it; + return 2; +} + +int Properties_pairs(lua_State* L) +{ + auto propObj = get_obj(L, 1); + auto obj = static_cast(get_object(propObj->id)); + if (!obj) + return luaL_error(L, "the object with these properties was destroyed"); + + auto& properties = obj->userData().properties(); + + push_obj(L, properties.begin()); + lua_pushcclosure(L, Properties_pairs_next, 1); + lua_pushvalue(L, 1); // Copy the same propObj as the second return value + return 2; +} + const luaL_Reg Properties_methods[] = { { "__len", Properties_len }, { "__index", Properties_index }, { "__newindex", Properties_newindex }, + { "__pairs", Properties_pairs }, + { nullptr, nullptr } +}; + +const luaL_Reg PropertiesIterator_methods[] = { { nullptr, nullptr } }; } // anonymous namespace DEF_MTNAME(Properties); +DEF_MTNAME(PropertiesIterator); void register_properties_class(lua_State* L) { REG_CLASS(L, Properties); + REG_CLASS(L, PropertiesIterator); } void push_properties(lua_State* L, doc::WithUserData* userData) diff --git a/tests/scripts/userdata.lua b/tests/scripts/userdata.lua index 9cb766d07..401deab03 100644 --- a/tests/scripts/userdata.lua +++ b/tests/scripts/userdata.lua @@ -17,6 +17,16 @@ do assert(math.abs(spr.properties.d - 2.3) < 0.00001) assert(#spr.properties == 4) + -- Iterate all properties + local t = {} + for k,v in pairs(spr.properties) do + t[k] = v + end + assert(t.a == true) + assert(t.b == 1) + assert(t.c == "hi") + assert(math.abs(t.d - 2.3) < 0.00001) + spr.properties.a = false assert(spr.properties.a == false) From 11dbb22efa6cc421dc135ddf780c7e67f0770225 Mon Sep 17 00:00:00 2001 From: David Capello Date: Mon, 2 Jan 2023 17:05:08 -0300 Subject: [PATCH 06/11] [lua] Correctly call PropertiesIterator destructor using its __gc This was detected by MSVC with its _ITERATOR_DEBUG_LEVEL=2 where orphan iterators are detected when the map/container is destroyed. --- src/app/script/properties_class.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/app/script/properties_class.cpp b/src/app/script/properties_class.cpp index 7934128c3..5f8dc9ba4 100644 --- a/src/app/script/properties_class.cpp +++ b/src/app/script/properties_class.cpp @@ -150,6 +150,12 @@ int Properties_pairs(lua_State* L) return 2; } +int PropertiesIterator_gc(lua_State* L) +{ + get_obj(L, 1)->~PropertiesIterator(); + return 0; +} + const luaL_Reg Properties_methods[] = { { "__len", Properties_len }, { "__index", Properties_index }, @@ -159,6 +165,7 @@ const luaL_Reg Properties_methods[] = { }; const luaL_Reg PropertiesIterator_methods[] = { + { "__gc", PropertiesIterator_gc }, { nullptr, nullptr } }; From 9138592e98a0c2d6af00aeba87891b37fd679b28 Mon Sep 17 00:00:00 2001 From: David Capello Date: Mon, 2 Jan 2023 19:51:07 -0300 Subject: [PATCH 07/11] [lua] Add support to set/get tables (arrays/maps) into user properties --- src/app/script/properties_class.cpp | 32 +----- src/app/script/values.cpp | 161 ++++++++++++++++++++++++++-- tests/scripts/userdata.lua | 42 +++++++- 3 files changed, 197 insertions(+), 38 deletions(-) diff --git a/src/app/script/properties_class.cpp b/src/app/script/properties_class.cpp index 5f8dc9ba4..8b3d96ab8 100644 --- a/src/app/script/properties_class.cpp +++ b/src/app/script/properties_class.cpp @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2022 Igara Studio S.A. +// Copyright (C) 2022-2023 Igara Studio S.A. // // This program is distributed under the terms of // the End-User License Agreement for Aseprite. @@ -79,8 +79,7 @@ int Properties_newindex(lua_State* L) switch (lua_type(L, 3)) { case LUA_TNONE: - case LUA_TNIL: - default: { + case LUA_TNIL: { // Just erase the property auto it = properties.find(field); if (it != properties.end()) @@ -88,32 +87,9 @@ int Properties_newindex(lua_State* L) break; } - case LUA_TBOOLEAN: - properties[field] = (lua_toboolean(L, 3) ? true: false); + default: + properties[field] = get_value_from_lua(L, 3); break; - - case LUA_TNUMBER: - if (lua_isinteger(L, 3)) - properties[field] = lua_tointeger(L, 3); - else { - properties[field] = doc::UserData::Fixed{ - fixmath::ftofix(lua_tonumber(L, 3)) - }; - } - break; - - case LUA_TSTRING: - properties[field] = std::string(lua_tostring(L, 3)); - break; - - case LUA_TTABLE: - // TODO convert a full table into properties recursively - break; - - case LUA_TUSERDATA: - // TODO convert table-like objects (Size, Point, Rectangle, etc.) - break; - } return 1; } diff --git a/src/app/script/values.cpp b/src/app/script/values.cpp index f497eb9d8..82900b3c2 100644 --- a/src/app/script/values.cpp +++ b/src/app/script/values.cpp @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2019-2022 Igara Studio S.A. +// Copyright (C) 2019-2023 Igara Studio S.A. // // This program is distributed under the terms of // the End-User License Agreement for Aseprite. @@ -275,20 +275,21 @@ FOR_ENUM(filters::TiledMode) FOR_ENUM(render::OnionskinPosition) // ---------------------------------------------------------------------- -// UserData::Properties / VariantStruct +// UserData::Properties / Variant template<> -void push_value_to_lua(lua_State* L, const doc::UserData::Properties& value) { - // TODO convert a Properties map into a Lua table -} +void push_value_to_lua(lua_State* L, const doc::UserData::Properties& value); +template<> +void push_value_to_lua(lua_State* L, const doc::UserData::Vector& value); template<> -void push_value_to_lua(lua_State* L, const doc::UserData::Vector& value) { - // TODO convert a Vector into a Lua table -} +doc::UserData::Properties get_value_from_lua(lua_State* L, int index); +template<> +doc::UserData::Vector get_value_from_lua(lua_State* L, int index); template<> -void push_value_to_lua(lua_State* L, const doc::UserData::Variant& value) { +void push_value_to_lua(lua_State* L, const doc::UserData::Variant& value) +{ #if 1 // We are targetting macOS 10.9, so we don't have the std::visit() available switch (value.type()) { case USER_DATA_PROPERTY_TYPE_BOOL: @@ -345,5 +346,147 @@ void push_value_to_lua(lua_State* L, const doc::UserData::Variant& value) { #endif } +template<> +doc::UserData::Variant get_value_from_lua(lua_State* L, int index) +{ + doc::UserData::Variant v; + + switch (lua_type(L, index)) { + + case LUA_TNONE: + case LUA_TNIL: + // TODO should we add nullptr_t in Variant? + break; + + case LUA_TBOOLEAN: + v = (lua_toboolean(L, index) ? true: false); + break; + + case LUA_TNUMBER: + if (lua_isinteger(L, index)) + v = lua_tointeger(L, index); + else { + v = doc::UserData::Fixed{ + fixmath::ftofix(lua_tonumber(L, index)) + }; + } + break; + + case LUA_TSTRING: + v = std::string(lua_tostring(L, index)); + break; + + case LUA_TTABLE: { + int i = 0; + bool isArray = true; + if (index < 0) + --index; + lua_pushnil(L); + while (lua_next(L, index) != 0) { + if (lua_isinteger(L, -2)) { + // TODO we should check that all values are of the same type + // to create the vector + if (++i != lua_tointeger(L, -2)) { + isArray = false; + lua_pop(L, 2); // Pop value and key + break; + } + } + else { + isArray = false; + lua_pop(L, 2); + break; + } + lua_pop(L, 1); // Pop the value, leave the key for lua_next() + } + if (index < 0) + ++index; + if (isArray) { + v = get_value_from_lua(L, index); + } + else { + v = get_value_from_lua(L, index); + } + break; + } + + case LUA_TUSERDATA: { + if (auto rect = may_get_obj(L, index)) { + v = *rect; + } + else if (auto pt = may_get_obj(L, index)) { + v = *pt; + } + else if (auto sz = may_get_obj(L, index)) { + v = *sz; + } + break; + } + } + + return v; +} + +template<> +void push_value_to_lua(lua_State* L, const doc::UserData::Properties& value) +{ + lua_newtable(L); + for (const auto& kv : value) { + push_value_to_lua(L, kv.second); + lua_setfield(L, -2, kv.first.c_str()); + } +} + +template<> +void push_value_to_lua(lua_State* L, const doc::UserData::Vector& value) +{ + int i = 0; + lua_newtable(L); + for (const auto& kv : value) { + push_value_to_lua(L, kv); + lua_seti(L, -2, ++i); + } +} + +template<> +doc::UserData::Properties get_value_from_lua(lua_State* L, int index) +{ + doc::UserData::Properties m; + + if (index < 0) + --index; + lua_pushnil(L); + while (lua_next(L, index) != 0) { + if (auto k = lua_tostring(L, -2)) + m[k] = get_value_from_lua(L, -1); + lua_pop(L, 1); + } + + return m; +} + +template<> +doc::UserData::Vector get_value_from_lua(lua_State* L, int index) +{ + doc::UserData::Vector v; + + lua_len(L, index); + int len = lua_tointeger(L, -1); + lua_pop(L, 1); + if (len > 0) + v.reserve(len); + + if (index < 0) + --index; + lua_pushnil(L); + while (lua_next(L, index) != 0) { + // TODO we should check that all variants are of the same type + v.push_back(get_value_from_lua(L, -1)); + lua_pop(L, 1); + } + + return v; +} + } // namespace script } // namespace app diff --git a/tests/scripts/userdata.lua b/tests/scripts/userdata.lua index 401deab03..7fdbfb95f 100644 --- a/tests/scripts/userdata.lua +++ b/tests/scripts/userdata.lua @@ -1,4 +1,4 @@ --- Copyright (C) 2022 Igara Studio S.A. +-- Copyright (C) 2022-2023 Igara Studio S.A. -- -- This file is released under the terms of the MIT license. -- Read LICENSE.txt for more information. @@ -32,4 +32,44 @@ do spr.properties.b = nil assert(spr.properties.b == nil) + assert(#spr.properties == 3) + + spr.properties.v = { 10, 20, 30 } + assert(#spr.properties.v == 3) + assert(spr.properties.v[1] == 10) + assert(spr.properties.v[2] == 20) + assert(spr.properties.v[3] == 30) + + spr.properties.u = spr.properties.v -- Copy a property + assert(#spr.properties.u == 3) + assert(spr.properties.u[1] == 10) + assert(spr.properties.u[2] == 20) + assert(spr.properties.u[3] == 30) + + spr.properties.m = { a=10, + b="bye", + c={ "a", "b", "c" }, + d={ a=1, b="d", c={ x=2, y=0.5, z=0 } }, + e=Point(32, 20), + f=Size(40, 80), + g=Rectangle(2, 4, 6, 8) } + local m = spr.properties.m + assert(m.a == 10) + assert(m.b == "bye") + assert(m.c[1] == "a") + assert(m.c[2] == "b") + assert(m.c[3] == "c") + assert(m.d.a == 1) + assert(m.d.b == "d") + assert(m.d.c.x == 2) + assert(m.d.c.y == 0.5) + assert(m.d.c.z == 0) + assert(m.e.x == 32) + assert(m.e.y == 20) + assert(m.f.width == 40) + assert(m.f.height == 80) + assert(m.g.x == 2) + assert(m.g.y == 4) + assert(m.g.width == 6) + assert(m.g.height == 8) end From b9537dbbe05eb42af1fb38cb48650c63f36a8fd5 Mon Sep 17 00:00:00 2001 From: David Capello Date: Tue, 3 Jan 2023 08:34:14 -0300 Subject: [PATCH 08/11] [lua] Add object.properties("extID") syntax to get extension properties --- src/app/script/engine.h | 4 +-- src/app/script/properties_class.cpp | 49 ++++++++++++++++++++++++----- src/app/script/userdata.h | 4 +-- tests/scripts/userdata.lua | 20 ++++++++++++ 4 files changed, 65 insertions(+), 12 deletions(-) diff --git a/src/app/script/engine.h b/src/app/script/engine.h index 525bf7c19..adca790cb 100644 --- a/src/app/script/engine.h +++ b/src/app/script/engine.h @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2018-2022 Igara Studio S.A. +// Copyright (C) 2018-2023 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -150,7 +150,7 @@ namespace app { void push_layers(lua_State* L, const doc::ObjectIds& layers); void push_palette(lua_State* L, doc::Palette* palette); void push_plugin(lua_State* L, Extension* ext); - void push_properties(lua_State* L, doc::WithUserData* userData); + void push_properties(lua_State* L, doc::WithUserData* userData, const std::string& extID); 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); diff --git a/src/app/script/properties_class.cpp b/src/app/script/properties_class.cpp index 8b3d96ab8..d62009d63 100644 --- a/src/app/script/properties_class.cpp +++ b/src/app/script/properties_class.cpp @@ -23,7 +23,13 @@ namespace { struct Properties { doc::ObjectId id = 0; - Properties(doc::ObjectId id) : id(id) { } + std::string extID; + + Properties(doc::ObjectId id, + const std::string& extID) + : id(id) + , extID(extID) { + } }; using PropertiesIterator = doc::UserData::Properties::iterator; @@ -35,7 +41,7 @@ int Properties_len(lua_State* L) if (!obj) return luaL_error(L, "the object with these properties was destroyed"); - auto& properties = obj->userData().properties(); + auto& properties = obj->userData().properties(propObj->extID); lua_pushinteger(L, properties.size()); return 1; } @@ -51,7 +57,7 @@ int Properties_index(lua_State* L) if (!obj) return luaL_error(L, "the object with these properties was destroyed"); - auto& properties = obj->userData().properties(); + auto& properties = obj->userData().properties(propObj->extID); auto it = properties.find(field); if (it != properties.end()) { push_value_to_lua(L, (*it).second); @@ -73,7 +79,7 @@ int Properties_newindex(lua_State* L) if (!obj) return luaL_error(L, "the object with these properties was destroyed"); - auto& properties = obj->userData().properties(); + auto& properties = obj->userData().properties(propObj->extID); // TODO add undo information switch (lua_type(L, 3)) { @@ -91,6 +97,30 @@ int Properties_newindex(lua_State* L) properties[field] = get_value_from_lua(L, 3); break; } + return 0; +} + +int Properties_call(lua_State* L) +{ + auto propObj = get_obj(L, 1); + const char* extID = lua_tostring(L, 2); + if (!extID) + return luaL_error(L, "extensionID in 'properties(\"extensionID\")' must be a string"); + + // Special syntax to change the full extension properties using: + // + // object.property("extension", { ...}) + // + if (lua_istable(L, 3)) { + auto obj = static_cast(get_object(propObj->id)); + if (!obj) + return luaL_error(L, "the object with these properties was destroyed"); + + auto& properties = obj->userData().properties(extID); + properties = get_value_from_lua(L, 3); + } + + push_new(L, propObj->id, extID); return 1; } @@ -101,7 +131,7 @@ int Properties_pairs_next(lua_State* L) if (!obj) return luaL_error(L, "the object with these properties was destroyed"); - auto& properties = obj->userData().properties(); + auto& properties = obj->userData().properties(propObj->extID); auto& it = *get_obj(L, lua_upvalueindex(1)); if (it == properties.end()) return 0; @@ -118,7 +148,7 @@ int Properties_pairs(lua_State* L) if (!obj) return luaL_error(L, "the object with these properties was destroyed"); - auto& properties = obj->userData().properties(); + auto& properties = obj->userData().properties(propObj->extID); push_obj(L, properties.begin()); lua_pushcclosure(L, Properties_pairs_next, 1); @@ -134,6 +164,7 @@ int PropertiesIterator_gc(lua_State* L) const luaL_Reg Properties_methods[] = { { "__len", Properties_len }, + { "__call", Properties_call }, { "__index", Properties_index }, { "__newindex", Properties_newindex }, { "__pairs", Properties_pairs }, @@ -156,9 +187,11 @@ void register_properties_class(lua_State* L) REG_CLASS(L, PropertiesIterator); } -void push_properties(lua_State* L, doc::WithUserData* userData) +void push_properties(lua_State* L, + doc::WithUserData* userData, + const std::string& extID) { - push_obj(L, userData->id()); + push_new(L, userData->id(), extID); } } // namespace script diff --git a/src/app/script/userdata.h b/src/app/script/userdata.h index 038a92e66..feac60a4d 100644 --- a/src/app/script/userdata.h +++ b/src/app/script/userdata.h @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2018-2020 Igara Studio S.A. +// Copyright (C) 2018-2023 Igara Studio S.A. // Copyright (C) 2018 David Capello // // This program is distributed under the terms of @@ -54,7 +54,7 @@ int UserData_get_color(lua_State* L) { template int UserData_get_properties(lua_State* L) { auto obj = get_docobj(L, 1); - push_properties(L, get_WithUserData(obj)); + push_properties(L, get_WithUserData(obj), std::string()); return 1; } diff --git a/tests/scripts/userdata.lua b/tests/scripts/userdata.lua index 7fdbfb95f..0abfd5546 100644 --- a/tests/scripts/userdata.lua +++ b/tests/scripts/userdata.lua @@ -72,4 +72,24 @@ do assert(m.g.y == 4) assert(m.g.width == 6) assert(m.g.height == 8) + + -- Extension properties + spr.properties.a = 10 + spr.properties("ext1").a = 20 + assert(spr.properties.a == 10) + assert(spr.properties("ext1").a == 20) + + spr.properties("ext1", { a=30, b=35 }) + assert(spr.properties("ext1").a == 30) + assert(spr.properties("ext1").b == 35) + + local ext1 = spr.properties("ext1") + ext1.a = 40 + ext1.b = 45 + assert(spr.properties("ext1").a == 40) + assert(spr.properties("ext1").b == 45) + + spr.properties("", { a=50, b=60 }) -- Empty extension is the user properties + assert(spr.properties.a == 50) + assert(spr.properties.b == 60) end From 427efef257ca59c24f0c31f58a8ea382e5168dc1 Mon Sep 17 00:00:00 2001 From: David Capello Date: Tue, 3 Jan 2023 09:00:36 -0300 Subject: [PATCH 09/11] [lua] Add possibility to set the whole properties object --- src/app/script/userdata.h | 10 ++++++++++ tests/scripts/userdata.lua | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/src/app/script/userdata.h b/src/app/script/userdata.h index feac60a4d..75d789435 100644 --- a/src/app/script/userdata.h +++ b/src/app/script/userdata.h @@ -13,6 +13,7 @@ #include "app/color.h" #include "app/color_utils.h" #include "app/script/luacpp.h" +#include "app/script/values.h" #include "app/tx.h" #include "doc/cel.h" #include "doc/with_user_data.h" @@ -58,6 +59,15 @@ int UserData_get_properties(lua_State* L) { return 1; } +template +int UserData_set_properties(lua_State* L) { + auto obj = get_docobj(L, 1); + auto& properties = get_WithUserData(obj)->userData().properties(); + // TODO add undo information + properties = get_value_from_lua(L, 2); + return 0; +} + template int UserData_set_text(lua_State* L) { auto obj = get_docobj(L, 1); diff --git a/tests/scripts/userdata.lua b/tests/scripts/userdata.lua index 0abfd5546..b5ce39378 100644 --- a/tests/scripts/userdata.lua +++ b/tests/scripts/userdata.lua @@ -53,6 +53,7 @@ do e=Point(32, 20), f=Size(40, 80), g=Rectangle(2, 4, 6, 8) } + assert(#spr.properties == 6) local m = spr.properties.m assert(m.a == 10) assert(m.b == "bye") @@ -73,6 +74,11 @@ do assert(m.g.width == 6) assert(m.g.height == 8) + -- Set all properties + spr.properties = { a=1000 } + assert(spr.properties.a == 1000) + assert(#spr.properties == 1) + -- Extension properties spr.properties.a = 10 spr.properties("ext1").a = 20 From e2024c6edd6352554df8c8a8a00f4754649af2be Mon Sep 17 00:00:00 2001 From: David Capello Date: Tue, 3 Jan 2023 09:00:58 -0300 Subject: [PATCH 10/11] [lua] Add "properties" property to all objects with user data --- src/app/script/cel_class.cpp | 3 ++- src/app/script/layer_class.cpp | 3 ++- src/app/script/slice_class.cpp | 3 ++- src/app/script/sprite_class.cpp | 4 ++-- src/app/script/tag_class.cpp | 3 ++- src/app/script/tileset_class.cpp | 3 ++- 6 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/app/script/cel_class.cpp b/src/app/script/cel_class.cpp index 2215b6b5e..4e944828b 100644 --- a/src/app/script/cel_class.cpp +++ b/src/app/script/cel_class.cpp @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2018-2022 Igara Studio S.A. +// Copyright (C) 2018-2023 Igara Studio S.A. // Copyright (C) 2018 David Capello // // This program is distributed under the terms of @@ -160,6 +160,7 @@ const Property Cel_properties[] = { { "opacity", Cel_get_opacity, Cel_set_opacity }, { "color", UserData_get_color, UserData_set_color }, { "data", UserData_get_text, UserData_set_text }, + { "properties", UserData_get_properties, UserData_set_properties }, { nullptr, nullptr, nullptr } }; diff --git a/src/app/script/layer_class.cpp b/src/app/script/layer_class.cpp index b2c8b3eb6..9978e845b 100644 --- a/src/app/script/layer_class.cpp +++ b/src/app/script/layer_class.cpp @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2018-2022 Igara Studio S.A. +// Copyright (C) 2018-2023 Igara Studio S.A. // Copyright (C) 2018 David Capello // // This program is distributed under the terms of @@ -406,6 +406,7 @@ const Property Layer_properties[] = { { "cels", Layer_get_cels, nullptr }, { "color", UserData_get_color, UserData_set_color }, { "data", UserData_get_text, UserData_set_text }, + { "properties", UserData_get_properties, UserData_set_properties }, { "tileset", Layer_get_tileset, nullptr }, { nullptr, nullptr, nullptr } }; diff --git a/src/app/script/slice_class.cpp b/src/app/script/slice_class.cpp index 5aba49404..29aa88b46 100644 --- a/src/app/script/slice_class.cpp +++ b/src/app/script/slice_class.cpp @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2018-2022 Igara Studio S.A. +// Copyright (C) 2018-2023 Igara Studio S.A. // Copyright (C) 2018 David Capello // // This program is distributed under the terms of @@ -145,6 +145,7 @@ const Property Slice_properties[] = { { "pivot", Slice_get_pivot, Slice_set_pivot }, { "color", UserData_get_color, UserData_set_color }, { "data", UserData_get_text, UserData_set_text }, + { "properties", UserData_get_properties, UserData_set_properties }, { nullptr, nullptr, nullptr } }; diff --git a/src/app/script/sprite_class.cpp b/src/app/script/sprite_class.cpp index 7965babe7..69f236e84 100644 --- a/src/app/script/sprite_class.cpp +++ b/src/app/script/sprite_class.cpp @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2018-2022 Igara Studio S.A. +// Copyright (C) 2018-2023 Igara Studio S.A. // Copyright (C) 2015-2018 David Capello // // This program is distributed under the terms of @@ -882,7 +882,7 @@ const Property Sprite_properties[] = { { "gridBounds", Sprite_get_gridBounds, Sprite_set_gridBounds }, { "color", UserData_get_color, UserData_set_color }, { "data", UserData_get_text, UserData_set_text }, - { "properties", UserData_get_properties, nullptr }, + { "properties", UserData_get_properties, UserData_set_properties }, { "pixelRatio", Sprite_get_pixelRatio, Sprite_set_pixelRatio }, { "events", Sprite_get_events, nullptr }, { nullptr, nullptr, nullptr } diff --git a/src/app/script/tag_class.cpp b/src/app/script/tag_class.cpp index feed4ef40..d30768c96 100644 --- a/src/app/script/tag_class.cpp +++ b/src/app/script/tag_class.cpp @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2018-2022 Igara Studio S.A. +// Copyright (C) 2018-2023 Igara Studio S.A. // Copyright (C) 2018 David Capello // // This program is distributed under the terms of @@ -162,6 +162,7 @@ const Property Tag_properties[] = { { "repeats", Tag_get_repeats, Tag_set_repeats }, // Cannot be "repeat" because it's a Lua keyword { "color", UserData_get_color, UserData_set_color }, { "data", UserData_get_text, UserData_set_text }, + { "properties", UserData_get_properties, UserData_set_properties }, { nullptr, nullptr, nullptr } }; diff --git a/src/app/script/tileset_class.cpp b/src/app/script/tileset_class.cpp index 5a5b3b362..a5a93278e 100644 --- a/src/app/script/tileset_class.cpp +++ b/src/app/script/tileset_class.cpp @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2019-2022 Igara Studio S.A. +// Copyright (C) 2019-2023 Igara Studio S.A. // // This program is distributed under the terms of // the End-User License Agreement for Aseprite. @@ -100,6 +100,7 @@ const Property Tileset_properties[] = { { "baseIndex", Tileset_get_baseIndex, Tileset_set_baseIndex }, { "color", UserData_get_color, UserData_set_color }, { "data", UserData_get_text, UserData_set_text }, + { "properties", UserData_get_properties, UserData_set_properties }, { nullptr, nullptr, nullptr } }; From 645605305f3f2c453b6d1032edb3116a9a4acb4e Mon Sep 17 00:00:00 2001 From: David Capello Date: Tue, 3 Jan 2023 11:14:19 -0300 Subject: [PATCH 11/11] [lua] Add undo information when we modify user data properties --- src/app/CMakeLists.txt | 5 ++- src/app/cmd/remove_user_data_property.cpp | 53 ++++++++++++++++++++++ src/app/cmd/remove_user_data_property.h | 46 +++++++++++++++++++ src/app/cmd/set_user_data_properties.cpp | 48 ++++++++++++++++++++ src/app/cmd/set_user_data_properties.h | 46 +++++++++++++++++++ src/app/cmd/set_user_data_property.cpp | 50 +++++++++++++++++++++ src/app/cmd/set_user_data_property.h | 48 ++++++++++++++++++++ src/app/script/properties_class.cpp | 55 +++++++++++++++++++---- src/app/script/userdata.h | 29 ++++++++---- tests/scripts/userdata.lua | 30 +++++++++++++ 10 files changed, 392 insertions(+), 18 deletions(-) create mode 100644 src/app/cmd/remove_user_data_property.cpp create mode 100644 src/app/cmd/remove_user_data_property.h create mode 100644 src/app/cmd/set_user_data_properties.cpp create mode 100644 src/app/cmd/set_user_data_properties.h create mode 100644 src/app/cmd/set_user_data_property.cpp create mode 100644 src/app/cmd/set_user_data_property.h diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 67deee7ee..df8a27705 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -1,5 +1,5 @@ # Aseprite -# Copyright (C) 2018-2022 Igara Studio S.A. +# Copyright (C) 2018-2023 Igara Studio S.A. # Copyright (C) 2001-2018 David Capello # Generate a ui::Widget for each widget in a XML file @@ -495,6 +495,7 @@ add_library(app-lib cmd/remove_tag.cpp cmd/remove_tile.cpp cmd/remove_tileset.cpp + cmd/remove_user_data_property.cpp cmd/replace_image.cpp cmd/replace_tileset.cpp cmd/reselect_mask.cpp @@ -528,6 +529,8 @@ add_library(app-lib cmd/set_total_frames.cpp cmd/set_transparent_color.cpp cmd/set_user_data.cpp + cmd/set_user_data_properties.cpp + cmd/set_user_data_property.cpp cmd/shift_masked_cel.cpp cmd/trim_cel.cpp cmd/unlink_cel.cpp diff --git a/src/app/cmd/remove_user_data_property.cpp b/src/app/cmd/remove_user_data_property.cpp new file mode 100644 index 000000000..835430c64 --- /dev/null +++ b/src/app/cmd/remove_user_data_property.cpp @@ -0,0 +1,53 @@ +// Aseprite +// Copyright (C) 2023 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/cmd/remove_user_data_property.h" + +#include "app/cmd.h" +#include "app/cmd/with_document.h" +#include "doc/object_id.h" +#include "doc/user_data.h" +#include "doc/with_user_data.h" + +namespace app { +namespace cmd { + +RemoveUserDataProperty::RemoveUserDataProperty( + doc::WithUserData* obj, + const std::string& group, + const std::string& field) + : m_objId(obj->id()) + , m_group(group) + , m_field(field) + , m_oldVariant(obj->userData().properties(m_group)[m_field]) +{ +} + +void RemoveUserDataProperty::onExecute() +{ + auto obj = doc::get(m_objId); + auto& properties = obj->userData().properties(m_group); + auto it = properties.find(m_field); + ASSERT(it != properties.end()); + if (it != properties.end()) { + properties.erase(it); + obj->incrementVersion(); + } +} + +void RemoveUserDataProperty::onUndo() +{ + auto obj = doc::get(m_objId); + obj->userData().properties(m_group)[m_field] = m_oldVariant; + obj->incrementVersion(); +} + +} // namespace cmd +} // namespace app diff --git a/src/app/cmd/remove_user_data_property.h b/src/app/cmd/remove_user_data_property.h new file mode 100644 index 000000000..01fce682a --- /dev/null +++ b/src/app/cmd/remove_user_data_property.h @@ -0,0 +1,46 @@ +// Aseprite +// Copyright (C) 2023 Igara Studio S.A. +// +// This program is distributed under the terms of +// the End-User License Agreement for Aseprite. + +#ifndef APP_CMD_REMOVE_USER_DATA_PROPERTY_H_INCLUDED +#define APP_CMD_REMOVE_USER_DATA_PROPERTY_H_INCLUDED +#pragma once + +#include "app/cmd.h" +#include "doc/object_id.h" +#include "doc/user_data.h" + +namespace doc { + class WithUserData; +} + +namespace app { +namespace cmd { + + class RemoveUserDataProperty : public Cmd { + public: + RemoveUserDataProperty( + doc::WithUserData* obj, + const std::string& group, + const std::string& field); + + protected: + void onExecute() override; + void onUndo() override; + size_t onMemSize() const override { + return sizeof(*this); // TODO + variants size + } + + private: + doc::ObjectId m_objId; + std::string m_group; + std::string m_field; + doc::UserData::Variant m_oldVariant; + }; + +} // namespace cmd +} // namespace app + +#endif diff --git a/src/app/cmd/set_user_data_properties.cpp b/src/app/cmd/set_user_data_properties.cpp new file mode 100644 index 000000000..f36a7fd12 --- /dev/null +++ b/src/app/cmd/set_user_data_properties.cpp @@ -0,0 +1,48 @@ +// Aseprite +// Copyright (C) 2023 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/cmd/set_user_data_properties.h" + +#include "app/cmd.h" +#include "app/cmd/with_document.h" +#include "doc/object_id.h" +#include "doc/user_data.h" +#include "doc/with_user_data.h" + +namespace app { +namespace cmd { + +SetUserDataProperties::SetUserDataProperties( + doc::WithUserData* obj, + const std::string& group, + doc::UserData::Properties&& newProperties) + : m_objId(obj->id()) + , m_group(group) + , m_oldProperties(obj->userData().properties(group)) + , m_newProperties(std::move(newProperties)) +{ +} + +void SetUserDataProperties::onExecute() +{ + auto obj = doc::get(m_objId); + obj->userData().properties(m_group) = m_newProperties; + obj->incrementVersion(); +} + +void SetUserDataProperties::onUndo() +{ + auto obj = doc::get(m_objId); + obj->userData().properties(m_group) = m_oldProperties; + obj->incrementVersion(); +} + +} // namespace cmd +} // namespace app diff --git a/src/app/cmd/set_user_data_properties.h b/src/app/cmd/set_user_data_properties.h new file mode 100644 index 000000000..e123e71e1 --- /dev/null +++ b/src/app/cmd/set_user_data_properties.h @@ -0,0 +1,46 @@ +// Aseprite +// Copyright (C) 2023 Igara Studio S.A. +// +// This program is distributed under the terms of +// the End-User License Agreement for Aseprite. + +#ifndef APP_CMD_SET_USER_DATA_PROPERTIES_H_INCLUDED +#define APP_CMD_SET_USER_DATA_PROPERTIES_H_INCLUDED +#pragma once + +#include "app/cmd.h" +#include "doc/object_id.h" +#include "doc/user_data.h" + +namespace doc { + class WithUserData; +} + +namespace app { +namespace cmd { + + class SetUserDataProperties : public Cmd { + public: + SetUserDataProperties( + doc::WithUserData* obj, + const std::string& group, + doc::UserData::Properties&& newProperties); + + protected: + void onExecute() override; + void onUndo() override; + size_t onMemSize() const override { + return sizeof(*this); // TODO + properties size + } + + private: + doc::ObjectId m_objId; + std::string m_group; + doc::UserData::Properties m_oldProperties; + doc::UserData::Properties m_newProperties; + }; + +} // namespace cmd +} // namespace app + +#endif diff --git a/src/app/cmd/set_user_data_property.cpp b/src/app/cmd/set_user_data_property.cpp new file mode 100644 index 000000000..36ffe4d87 --- /dev/null +++ b/src/app/cmd/set_user_data_property.cpp @@ -0,0 +1,50 @@ +// Aseprite +// Copyright (C) 2023 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/cmd/set_user_data_property.h" + +#include "app/cmd.h" +#include "app/cmd/with_document.h" +#include "doc/object_id.h" +#include "doc/user_data.h" +#include "doc/with_user_data.h" + +namespace app { +namespace cmd { + +SetUserDataProperty::SetUserDataProperty( + doc::WithUserData* obj, + const std::string& group, + const std::string& field, + doc::UserData::Variant&& newValue) + : m_objId(obj->id()) + , m_group(group) + , m_field(field) + , m_oldValue(obj->userData().properties(group)[m_field]) + , m_newValue(std::move(newValue)) +{ +} + +void SetUserDataProperty::onExecute() +{ + auto obj = doc::get(m_objId); + obj->userData().properties(m_group)[m_field] = m_newValue; + obj->incrementVersion(); +} + +void SetUserDataProperty::onUndo() +{ + auto obj = doc::get(m_objId); + obj->userData().properties(m_group)[m_field] = m_oldValue; + obj->incrementVersion(); +} + +} // namespace cmd +} // namespace app diff --git a/src/app/cmd/set_user_data_property.h b/src/app/cmd/set_user_data_property.h new file mode 100644 index 000000000..16e7ed093 --- /dev/null +++ b/src/app/cmd/set_user_data_property.h @@ -0,0 +1,48 @@ +// Aseprite +// Copyright (C) 2023 Igara Studio S.A. +// +// This program is distributed under the terms of +// the End-User License Agreement for Aseprite. + +#ifndef APP_CMD_SET_USER_DATA_PROPERTY_H_INCLUDED +#define APP_CMD_SET_USER_DATA_PROPERTY_H_INCLUDED +#pragma once + +#include "app/cmd.h" +#include "doc/object_id.h" +#include "doc/user_data.h" + +namespace doc { + class WithUserData; +} + +namespace app { +namespace cmd { + + class SetUserDataProperty : public Cmd { + public: + SetUserDataProperty( + doc::WithUserData* obj, + const std::string& group, + const std::string& field, + doc::UserData::Variant&& newValue); + + protected: + void onExecute() override; + void onUndo() override; + size_t onMemSize() const override { + return sizeof(*this); // TODO + variant size + } + + private: + doc::ObjectId m_objId; + std::string m_group; + std::string m_field; + doc::UserData::Variant m_oldValue; + doc::UserData::Variant m_newValue; + }; + +} // namespace cmd +} // namespace app + +#endif diff --git a/src/app/script/properties_class.cpp b/src/app/script/properties_class.cpp index d62009d63..e164f5ff9 100644 --- a/src/app/script/properties_class.cpp +++ b/src/app/script/properties_class.cpp @@ -8,10 +8,14 @@ #include "config.h" #endif +#include "app/cmd/remove_user_data_property.h" +#include "app/cmd/set_user_data_properties.h" +#include "app/cmd/set_user_data_property.h" #include "app/script/docobj.h" #include "app/script/engine.h" #include "app/script/luacpp.h" #include "app/script/values.h" +#include "app/tx.h" #include "doc/with_user_data.h" #include @@ -81,21 +85,45 @@ int Properties_newindex(lua_State* L) auto& properties = obj->userData().properties(propObj->extID); - // TODO add undo information switch (lua_type(L, 3)) { case LUA_TNONE: case LUA_TNIL: { - // Just erase the property + // If we assign nil to a property, we just remove the property. + auto it = properties.find(field); - if (it != properties.end()) - properties.erase(it); + if (it != properties.end()) { + // TODO add Object::sprite() member function, and fix "Tx" object + // to use the sprite of this object instead of the activeDocument() + //if (obj->sprite()) { + if (App::instance()->context()->activeDocument()) { + Tx tx; + tx(new cmd::RemoveUserDataProperty(obj, propObj->extID, field)); + tx.commit(); + } + else { + properties.erase(it); + } + } break; } - default: - properties[field] = get_value_from_lua(L, 3); + default: { + auto newValue = get_value_from_lua(L, 3); + + // TODO add Object::sprite() member function + //if (obj->sprite()) { + if (App::instance()->context()->activeDocument()) { + Tx tx; + tx(new cmd::SetUserDataProperty(obj, propObj->extID, field, + std::move(newValue))); + tx.commit(); + } + else { + properties[field] = std::move(newValue); + } break; + } } return 0; } @@ -112,12 +140,23 @@ int Properties_call(lua_State* L) // object.property("extension", { ...}) // if (lua_istable(L, 3)) { + auto newProperties = get_value_from_lua(L, 3); + auto obj = static_cast(get_object(propObj->id)); if (!obj) return luaL_error(L, "the object with these properties was destroyed"); - auto& properties = obj->userData().properties(extID); - properties = get_value_from_lua(L, 3); + // TODO add Object::sprite() member function + //if (obj->sprite()) { + if (App::instance()->context()->activeDocument()) { + Tx tx; + tx(new cmd::SetUserDataProperties(obj, extID, std::move(newProperties))); + tx.commit(); + } + else { + auto& properties = obj->userData().properties(extID); + properties = std::move(newProperties); + } } push_new(L, propObj->id, extID); diff --git a/src/app/script/userdata.h b/src/app/script/userdata.h index 75d789435..b7ca94e2b 100644 --- a/src/app/script/userdata.h +++ b/src/app/script/userdata.h @@ -10,6 +10,7 @@ #pragma once #include "app/cmd/set_user_data.h" +#include "app/cmd/set_user_data_properties.h" #include "app/color.h" #include "app/color_utils.h" #include "app/script/luacpp.h" @@ -59,15 +60,6 @@ int UserData_get_properties(lua_State* L) { return 1; } -template -int UserData_set_properties(lua_State* L) { - auto obj = get_docobj(L, 1); - auto& properties = get_WithUserData(obj)->userData().properties(); - // TODO add undo information - properties = get_value_from_lua(L, 2); - return 0; -} - template int UserData_set_text(lua_State* L) { auto obj = get_docobj(L, 1); @@ -106,6 +98,25 @@ int UserData_set_color(lua_State* L) { return 0; } +template +int UserData_set_properties(lua_State* L) { + auto obj = get_docobj(L, 1); + auto wud = get_WithUserData(obj); + auto newProperties = get_value_from_lua(L, 2); + if (obj->sprite()) { + Tx tx; + tx(new cmd::SetUserDataProperties(wud, + std::string(), + std::move(newProperties))); + tx.commit(); + } + else { + auto& properties = wud->userData().properties(); + properties = std::move(newProperties); + } + return 0; +} + } // namespace script } // namespace app diff --git a/tests/scripts/userdata.lua b/tests/scripts/userdata.lua index b5ce39378..14fa943fd 100644 --- a/tests/scripts/userdata.lua +++ b/tests/scripts/userdata.lua @@ -78,6 +78,12 @@ do spr.properties = { a=1000 } assert(spr.properties.a == 1000) assert(#spr.properties == 1) + app.undo() -- Test undo/redo + assert(spr.properties.a == false) + assert(#spr.properties == 6) + app.redo() + assert(spr.properties.a == 1000) + assert(#spr.properties == 1) -- Extension properties spr.properties.a = 10 @@ -88,6 +94,10 @@ do spr.properties("ext1", { a=30, b=35 }) assert(spr.properties("ext1").a == 30) assert(spr.properties("ext1").b == 35) + app.undo() -- Test undo/redo + assert(spr.properties("ext1").a == 20) + assert(spr.properties("ext1").b == nil) + app.redo() local ext1 = spr.properties("ext1") ext1.a = 40 @@ -98,4 +108,24 @@ do spr.properties("", { a=50, b=60 }) -- Empty extension is the user properties assert(spr.properties.a == 50) assert(spr.properties.b == 60) + + -- Test undo/redo setting/removing one property at a time + spr.properties.a = "hi" + assert(spr.properties.a == "hi") + assert(spr.properties.b == 60) + app.undo() + assert(spr.properties.a == 50) + assert(spr.properties.b == 60) + app.redo() + assert(spr.properties.a == "hi") + assert(spr.properties.b == 60) + assert(#spr.properties == 2) + spr.properties.a = nil + assert(#spr.properties == 1) + assert(spr.properties.a == nil) + assert(spr.properties.b == 60) + app.undo() + assert(#spr.properties == 2) + assert(spr.properties.a == "hi") + assert(spr.properties.b == 60) end