aseprite/src/app/script/events_class.cpp
David Capello cd342f5630 [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
2021-10-07 18:56:39 -03:00

344 lines
8.1 KiB
C++

// 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 <cstring>
#include <map>
#include <memory>
namespace app {
namespace script {
using namespace doc;
namespace {
using EventListener = int;
class AppEvents;
class SpriteEvents;
static std::unique_ptr<AppEvents> g_appEvents;
static std::map<doc::ObjectId, std::unique_ptr<SpriteEvents>> 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<int(m_listeners.size()); ++i) {
EventListeners& listeners = m_listeners[i];
auto it = listeners.begin();
auto end = listeners.end();
bool removed = false;
for (; it != end; ) {
if (*it == callbackRef) {
removed = true;
it = listeners.erase(it);
end = listeners.end();
}
else
++it;
}
if (removed && listeners.empty())
onRemoveLastListener(i);
}
}
protected:
void call(EventType eventType) {
if (eventType >= 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<EventListener>;
std::vector<EventListeners> 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<Sprite>(m_spriteId);
if (sprite)
return static_cast<Doc*>(sprite->document());
else
return nullptr;
}
ObjectId m_spriteId;
bool m_observingUndo = false;
};
int Events_on(lua_State* L)
{
auto evs = get_ptr<Events>(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<Events>(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<Events>(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<Events>(L, spriteEvents);
}
} // namespace script
} // namespace app