1// Aseprite
2// Copyright (C) 2020-2022 Igara Studio S.A.
3//
4// This program is distributed under the terms of
5// the End-User License Agreement for Aseprite.
6
7#ifdef HAVE_CONFIG_H
8#include "config.h"
9#endif
10
11#include "app/app.h"
12#include "app/app_menus.h"
13#include "app/commands/command.h"
14#include "app/commands/commands.h"
15#include "app/console.h"
16#include "app/script/engine.h"
17#include "app/script/luacpp.h"
18#include "app/ui/app_menuitem.h"
19
20namespace app {
21namespace script {
22
23namespace {
24
25struct Plugin {
26 Extension* ext;
27 Plugin(Extension* ext) : ext(ext) { }
28};
29
30class PluginCommand : public Command {
31public:
32 PluginCommand(const std::string& id,
33 const std::string& title,
34 int onclickRef,
35 int onenabledRef)
36 : Command(id.c_str(), CmdUIOnlyFlag)
37 , m_title(title)
38 , m_onclickRef(onclickRef)
39 , m_onenabledRef(onenabledRef)
40 {
41 }
42
43 ~PluginCommand() {
44 auto app = App::instance();
45 ASSERT(app);
46 if (!app)
47 return;
48
49 if (script::Engine* engine = app->scriptEngine()) {
50 lua_State* L = engine->luaState();
51 luaL_unref(L, LUA_REGISTRYINDEX, m_onclickRef);
52 }
53 }
54
55protected:
56 std::string onGetFriendlyName() const override {
57 return m_title;
58 }
59
60 void onExecute(Context* context) override {
61 script::Engine* engine = App::instance()->scriptEngine();
62 lua_State* L = engine->luaState();
63
64 lua_rawgeti(L, LUA_REGISTRYINDEX, m_onclickRef);
65 if (lua_pcall(L, 0, 1, 0)) {
66 if (const char* s = lua_tostring(L, -1)) {
67 Console().printf("Error: %s", s);
68 }
69 }
70 else {
71 lua_pop(L, 1);
72 }
73 }
74
75 bool onEnabled(Context* context) override {
76 if (m_onenabledRef) {
77 script::Engine* engine = App::instance()->scriptEngine();
78 lua_State* L = engine->luaState();
79
80 lua_rawgeti(L, LUA_REGISTRYINDEX, m_onenabledRef);
81 if (lua_pcall(L, 0, 1, 0)) {
82 if (const char* s = lua_tostring(L, -1)) {
83 Console().printf("Error: %s", s);
84 return false;
85 }
86 }
87 else {
88 bool ret = lua_toboolean(L, -1);
89 lua_pop(L, 1);
90 return ret;
91 }
92 }
93 return true;
94 }
95
96 std::string m_title;
97 int m_onclickRef;
98 int m_onenabledRef;
99};
100
101void deleteCommandIfExistent(Extension* ext, const std::string& id)
102{
103 auto cmd = Commands::instance()->byId(id.c_str());
104 if (cmd) {
105 Commands::instance()->remove(cmd);
106 ext->removeCommand(id);
107 delete cmd;
108 }
109}
110
111int Plugin_gc(lua_State* L)
112{
113 get_obj<Plugin>(L, 1)->~Plugin();
114 return 0;
115}
116
117int Plugin_newCommand(lua_State* L)
118{
119 auto plugin = get_obj<Plugin>(L, 1);
120 if (lua_istable(L, 2)) {
121 std::string id, title, group;
122 int onenabledRef = 0;
123
124 lua_getfield(L, 2, "id");
125 if (const char* s = lua_tostring(L, -1)) {
126 id = s;
127 }
128 lua_pop(L, 1);
129
130 if (id.empty())
131 return luaL_error(L, "Empty id field in plugin:newCommand{ id=... }");
132
133 lua_getfield(L, 2, "title");
134 if (const char* s = lua_tostring(L, -1)) {
135 title = s;
136 }
137 lua_pop(L, 1);
138
139 lua_getfield(L, 2, "group");
140 if (const char* s = lua_tostring(L, -1)) {
141 group = s;
142 }
143 lua_pop(L, 1);
144
145 int type = lua_getfield(L, 2, "onenabled");
146 if (type == LUA_TFUNCTION) {
147 onenabledRef = luaL_ref(L, LUA_REGISTRYINDEX); // does a pop
148 }
149 else {
150 lua_pop(L, 1);
151 }
152
153 type = lua_getfield(L, 2, "onclick");
154 if (type == LUA_TFUNCTION) {
155 int onclickRef = luaL_ref(L, LUA_REGISTRYINDEX);
156
157 // Delete the command if it already exist (e.g. we are
158 // overwriting a previous registered command)
159 deleteCommandIfExistent(plugin->ext, id);
160
161 auto cmd = new PluginCommand(id, title, onclickRef, onenabledRef);
162 Commands::instance()->add(cmd);
163 plugin->ext->addCommand(id);
164
165#ifdef ENABLE_UI
166 // Add a new menu option if the "group" is defined
167 if (!group.empty() &&
168 App::instance()->isGui()) { // On CLI menus do not make sense
169 if (auto appMenus = AppMenus::instance()) {
170 std::unique_ptr<MenuItem> menuItem(new AppMenuItem(title, id));
171 appMenus->addMenuItemIntoGroup(group, std::move(menuItem));
172 }
173 }
174#endif // ENABLE_UI
175 }
176 else {
177 lua_pop(L, 1);
178 }
179 }
180 return 0;
181}
182
183int Plugin_deleteCommand(lua_State* L)
184{
185 std::string id;
186
187 auto plugin = get_obj<Plugin>(L, 1);
188 if (lua_istable(L, 2)) {
189 lua_getfield(L, 2, "id");
190 if (const char* s = lua_tostring(L, -1)) {
191 id = s;
192 }
193 lua_pop(L, 1);
194 }
195 else if (const char* s = lua_tostring(L, 2)) {
196 id = s;
197 }
198
199 if (id.empty())
200 return luaL_error(L, "No command id specified in plugin:deleteCommand()");
201
202 // TODO this can crash if we delete the command from the same command
203 deleteCommandIfExistent(plugin->ext, id);
204 return 0;
205}
206
207int Plugin_get_preferences(lua_State* L)
208{
209 if (!lua_getuservalue(L, 1)) {
210 lua_newtable(L);
211 lua_pushvalue(L, -1);
212 lua_setuservalue(L, 1);
213 }
214 return 1;
215}
216
217int Plugin_set_preferences(lua_State* L)
218{
219 lua_pushvalue(L, 2);
220 lua_setuservalue(L, 1);
221 return 0;
222}
223
224const luaL_Reg Plugin_methods[] = {
225 { "__gc", Plugin_gc },
226 { "newCommand", Plugin_newCommand },
227 { "deleteCommand", Plugin_deleteCommand },
228 { nullptr, nullptr }
229};
230
231const Property Plugin_properties[] = {
232 { "preferences", Plugin_get_preferences, Plugin_set_preferences },
233 { nullptr, nullptr, nullptr }
234};
235
236} // anonymous namespace
237
238DEF_MTNAME(Plugin);
239
240void register_plugin_class(lua_State* L)
241{
242 REG_CLASS(L, Plugin);
243 REG_CLASS_PROPERTIES(L, Plugin);
244}
245
246void push_plugin(lua_State* L, Extension* ext)
247{
248 push_new<Plugin>(L, ext);
249}
250
251} // namespace script
252} // namespace app
253