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 | |
20 | namespace app { |
21 | namespace script { |
22 | |
23 | namespace { |
24 | |
25 | struct Plugin { |
26 | Extension* ext; |
27 | Plugin(Extension* ext) : ext(ext) { } |
28 | }; |
29 | |
30 | class PluginCommand : public Command { |
31 | public: |
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 | |
55 | protected: |
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 | |
101 | void 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 | |
111 | int Plugin_gc(lua_State* L) |
112 | { |
113 | get_obj<Plugin>(L, 1)->~Plugin(); |
114 | return 0; |
115 | } |
116 | |
117 | int 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::instance()) { |
170 | std::unique_ptr<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 | |
183 | int 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 | |
207 | int 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 | |
217 | int Plugin_set_preferences(lua_State* L) |
218 | { |
219 | lua_pushvalue(L, 2); |
220 | lua_setuservalue(L, 1); |
221 | return 0; |
222 | } |
223 | |
224 | const luaL_Reg Plugin_methods[] = { |
225 | { "__gc" , Plugin_gc }, |
226 | { "newCommand" , Plugin_newCommand }, |
227 | { "deleteCommand" , Plugin_deleteCommand }, |
228 | { nullptr, nullptr } |
229 | }; |
230 | |
231 | const Property Plugin_properties[] = { |
232 | { "preferences" , Plugin_get_preferences, Plugin_set_preferences }, |
233 | { nullptr, nullptr, nullptr } |
234 | }; |
235 | |
236 | } // anonymous namespace |
237 | |
238 | DEF_MTNAME(Plugin); |
239 | |
240 | void register_plugin_class(lua_State* L) |
241 | { |
242 | REG_CLASS(L, Plugin); |
243 | REG_CLASS_PROPERTIES(L, Plugin); |
244 | } |
245 | |
246 | void push_plugin(lua_State* L, Extension* ext) |
247 | { |
248 | push_new<Plugin>(L, ext); |
249 | } |
250 | |
251 | } // namespace script |
252 | } // namespace app |
253 | |