1// Aseprite
2// Copyright (C) 2021-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/context.h"
13#include "app/context_observer.h"
14#include "app/doc.h"
15#include "app/doc_undo.h"
16#include "app/doc_undo_observer.h"
17#include "app/pref/preferences.h"
18#include "app/script/docobj.h"
19#include "app/script/engine.h"
20#include "app/script/luacpp.h"
21#include "app/script/values.h"
22#include "doc/document.h"
23#include "doc/sprite.h"
24#include "ui/app_state.h"
25
26#include <any>
27#include <cstring>
28#include <initializer_list>
29#include <map>
30#include <memory>
31
32namespace app {
33namespace script {
34
35using namespace doc;
36
37namespace {
38
39using EventListener = int;
40
41class AppEvents;
42class SpriteEvents;
43static std::unique_ptr<AppEvents> g_appEvents;
44static std::map<doc::ObjectId, std::unique_ptr<SpriteEvents>> g_spriteEvents;
45
46class Events {
47public:
48 using EventType = int;
49
50 Events() { }
51 virtual ~Events() { }
52 Events(const Events&) = delete;
53 Events& operator=(const Events&) = delete;
54
55 virtual EventType eventType(const char* eventName) const = 0;
56
57 bool hasListener(EventListener callbackRef) const {
58 for (auto& listeners : m_listeners) {
59 for (EventListener listener : listeners) {
60 if (listener == callbackRef)
61 return true;
62 }
63 }
64 return false;
65 }
66
67 void add(EventType eventType, EventListener callbackRef) {
68 if (eventType >= m_listeners.size())
69 m_listeners.resize(eventType+1);
70
71 auto& listeners = m_listeners[eventType];
72 listeners.push_back(callbackRef);
73 if (listeners.size() == 1)
74 onAddFirstListener(eventType);
75 }
76
77 void remove(EventListener callbackRef) {
78 for (int i=0; i<int(m_listeners.size()); ++i) {
79 EventListeners& listeners = m_listeners[i];
80 auto it = listeners.begin();
81 auto end = listeners.end();
82 bool removed = false;
83 for (; it != end; ) {
84 if (*it == callbackRef) {
85 removed = true;
86 it = listeners.erase(it);
87 end = listeners.end();
88 }
89 else
90 ++it;
91 }
92 if (removed && listeners.empty())
93 onRemoveLastListener(i);
94 }
95 }
96
97protected:
98 void call(EventType eventType,
99 const std::initializer_list<std::pair<const std::string, std::any>>& args = {}) {
100 if (eventType >= m_listeners.size())
101 return;
102
103 script::Engine* engine = App::instance()->scriptEngine();
104 lua_State* L = engine->luaState();
105
106 try {
107 for (EventListener callbackRef : m_listeners[eventType]) {
108 // Get user-defined callback function
109 lua_rawgeti(L, LUA_REGISTRYINDEX, callbackRef);
110
111 int callbackArgs = 0;
112 if (args.size() > 0) {
113 ++callbackArgs;
114 lua_newtable(L); // Create "ev" argument with fields about the event
115 for (const auto& kv : args) {
116 push_value_to_lua(L, kv.second);
117 lua_setfield(L, -2, kv.first.c_str());
118 }
119 }
120
121 if (lua_pcall(L, callbackArgs, 0, 0)) {
122 if (const char* s = lua_tostring(L, -1))
123 engine->consolePrint(s);
124 }
125 }
126 }
127 catch (const std::exception& ex) {
128 engine->consolePrint(ex.what());
129 }
130 }
131
132private:
133 virtual void onAddFirstListener(EventType eventType) = 0;
134 virtual void onRemoveLastListener(EventType eventType) = 0;
135
136 using EventListeners = std::vector<EventListener>;
137 std::vector<EventListeners> m_listeners;
138};
139
140class AppEvents : public Events
141 , private ContextObserver {
142public:
143 enum : EventType { Unknown = -1, SiteChange, FgColorChange, BgColorChange };
144
145 AppEvents() {
146 }
147
148 EventType eventType(const char* eventName) const override {
149 if (std::strcmp(eventName, "sitechange") == 0)
150 return SiteChange;
151 else if (std::strcmp(eventName, "fgcolorchange") == 0)
152 return FgColorChange;
153 else if (std::strcmp(eventName, "bgcolorchange") == 0)
154 return BgColorChange;
155 else
156 return Unknown;
157 }
158
159private:
160
161 void onAddFirstListener(EventType eventType) override {
162 switch (eventType) {
163 case SiteChange:
164 App::instance()->context()->add_observer(this);
165 break;
166 case FgColorChange:
167 m_fgConn = Preferences::instance().colorBar.fgColor
168 .AfterChange.connect([this]{ onFgColorChange(); });
169 break;
170 case BgColorChange:
171 m_bgConn = Preferences::instance().colorBar.bgColor
172 .AfterChange.connect([this]{ onBgColorChange(); });
173 break;
174 }
175 }
176
177 void onRemoveLastListener(EventType eventType) override {
178 switch (eventType) {
179 case SiteChange:
180 App::instance()->context()->remove_observer(this);
181 break;
182 case FgColorChange:
183 m_fgConn.disconnect();
184 break;
185 case BgColorChange:
186 m_bgConn.disconnect();
187 break;
188 }
189 }
190
191 void onFgColorChange() {
192 call(FgColorChange);
193 }
194
195 void onBgColorChange() {
196 call(BgColorChange);
197 }
198
199 // ContextObserver impl
200 void onActiveSiteChange(const Site& site) override {
201 call(SiteChange);
202 }
203
204 obs::scoped_connection m_fgConn;
205 obs::scoped_connection m_bgConn;
206};
207
208class SpriteEvents : public Events
209 , public DocUndoObserver
210 , public DocObserver {
211public:
212 enum : EventType { Unknown = -1, Change, FilenameChange };
213
214 SpriteEvents(const Sprite* sprite)
215 : m_spriteId(sprite->id()) {
216 doc()->add_observer(this);
217 }
218
219 ~SpriteEvents() {
220 auto doc = this->doc();
221 // The document can be nullptr in some cases like:
222 // - When closing the App with an exception
223 // (ui::get_app_state() == ui::AppState::kClosingWithException)
224 // - When Sprite.events property was accessed in a app
225 // "sitechange" event just when this same sprite was closed
226 // (so the SpriteEvents is created/destroyed for second time)
227 if (doc) {
228 disconnectFromUndoHistory(doc);
229 doc->remove_observer(this);
230 }
231 }
232
233 EventType eventType(const char* eventName) const override {
234 if (std::strcmp(eventName, "change") == 0)
235 return Change;
236 else if (std::strcmp(eventName, "filenamechange") == 0)
237 return FilenameChange;
238 else
239 return Unknown;
240 }
241
242 // DocObserver impl
243 void onCloseDocument(Doc* doc) override {
244 auto it = g_spriteEvents.find(m_spriteId);
245 ASSERT(it != g_spriteEvents.end());
246 if (it != g_spriteEvents.end()) {
247 // As this is an unique_ptr, here we are calling ~SpriteEvents()
248 g_spriteEvents.erase(it);
249 }
250 }
251
252 void onFileNameChanged(Doc* doc) override {
253 call(FilenameChange);
254 }
255
256 // DocUndoObserver impl
257 void onAddUndoState(DocUndo* history) override {
258 call(Change);
259 }
260 void onCurrentUndoStateChange(DocUndo* history) override {
261 call(Change, { { "fromUndo", true } });
262 }
263
264private:
265
266 void onAddFirstListener(EventType eventType) override {
267 switch (eventType) {
268 case Change:
269 ASSERT(!m_observingUndo);
270 doc()->undoHistory()->add_observer(this);
271 m_observingUndo = true;
272 break;
273 }
274 }
275
276 void onRemoveLastListener(EventType eventType) override {
277 switch (eventType) {
278 case Change: {
279 disconnectFromUndoHistory(doc());
280 break;
281 }
282 }
283 }
284
285 Doc* doc() {
286 Sprite* sprite = doc::get<Sprite>(m_spriteId);
287 if (sprite)
288 return static_cast<Doc*>(sprite->document());
289 else
290 return nullptr;
291 }
292
293 void disconnectFromUndoHistory(Doc* doc) {
294 if (m_observingUndo) {
295 doc->undoHistory()->remove_observer(this);
296 m_observingUndo = false;
297 }
298 }
299
300 ObjectId m_spriteId;
301 bool m_observingUndo = false;
302};
303
304int Events_on(lua_State* L)
305{
306 auto evs = get_ptr<Events>(L, 1);
307 const char* eventName = lua_tostring(L, 2);
308 if (!eventName)
309 return 0;
310
311 const int type = evs->eventType(eventName);
312 if (type < 0)
313 return luaL_error(L, "invalid event name to listen");
314
315 if (!lua_isfunction(L, 3))
316 return luaL_error(L, "second argument must be a function");
317
318 // Copy the callback function to add it to the global registry
319 lua_pushvalue(L, 3);
320 int callbackRef = luaL_ref(L, LUA_REGISTRYINDEX);
321 evs->add(type, callbackRef);
322
323 // Return the callback ref (this is an EventListener easier to use
324 // in Events_off())
325 lua_pushinteger(L, callbackRef);
326 return 1;
327}
328
329int Events_off(lua_State* L)
330{
331 auto evs = get_ptr<Events>(L, 1);
332 int callbackRef = LUA_REFNIL;
333
334 // Remove by listener value
335 if (lua_isinteger(L, 2)) {
336 callbackRef = lua_tointeger(L, 2);
337 }
338 // Remove by function reference
339 else if (lua_isfunction(L, 2)) {
340 lua_pushnil(L);
341 while (lua_next(L, LUA_REGISTRYINDEX) != 0) {
342 if (lua_isnumber(L, -2) &&
343 lua_isfunction(L, -1)) {
344 int i = lua_tointeger(L, -2);
345 if (// Compare value=function in 2nd argument
346 lua_compare(L, -1, 2, LUA_OPEQ) &&
347 // Check that this Events contain this reference
348 evs->hasListener(i)) {
349 callbackRef = i;
350 lua_pop(L, 2); // Pop value and key
351 break;
352 }
353 }
354 lua_pop(L, 1); // Pop the value, leave the key for next lua_next()
355 }
356 }
357 else {
358 return luaL_error(L, "first argument must be a function or a EventListener");
359 }
360
361 if (callbackRef != LUA_REFNIL &&
362 // Check that we are removing a listener from this Events and no
363 // other random value from the Lua registry
364 evs->hasListener(callbackRef)) {
365 evs->remove(callbackRef);
366 luaL_unref(L, LUA_REGISTRYINDEX, callbackRef);
367 }
368 return 0;
369}
370
371// We don't need a __gc (to call ~Events()), because Events instances
372// will be deleted when the Sprite is deleted or on App Exit
373const luaL_Reg Events_methods[] = {
374 { "on", Events_on },
375 { "off", Events_off },
376 { nullptr, nullptr }
377};
378
379} // anonymous namespace
380
381DEF_MTNAME(Events);
382
383void register_events_class(lua_State* L)
384{
385 REG_CLASS(L, Events);
386}
387
388void push_app_events(lua_State* L)
389{
390 if (!g_appEvents) {
391 App::instance()->Exit.connect([]{ g_appEvents.reset(); });
392 g_appEvents.reset(new AppEvents);
393 }
394 push_ptr<Events>(L, g_appEvents.get());
395}
396
397void push_sprite_events(lua_State* L, Sprite* sprite)
398{
399 // Clear the g_spriteEvents map on Exit() signal because if the dtor
400 // is called in the normal C++ order destruction sequence by
401 // compilation units, it could crash because each ~SpriteEvents()
402 // needs the doc::get() function, which uses the "objects"
403 // collection from "src/doc/objects.cpp" (so we cannot garantize
404 // that that "objects" collection will be destroyed after
405 // "g_spriteEvents")
406 static bool atExit = false;
407 if (!atExit) {
408 atExit = true;
409 App::instance()->Exit.connect([]{ g_spriteEvents.clear(); });
410 }
411
412 ASSERT(sprite);
413
414 SpriteEvents* spriteEvents;
415
416 auto it = g_spriteEvents.find(sprite->id());
417 if (it != g_spriteEvents.end())
418 spriteEvents = it->second.get();
419 else {
420 spriteEvents = new SpriteEvents(sprite);
421 g_spriteEvents[sprite->id()].reset(spriteEvents);
422 }
423
424 push_ptr<Events>(L, spriteEvents);
425}
426
427} // namespace script
428} // namespace app
429