mirror of
https://github.com/aseprite/aseprite.git
synced 2025-04-01 01:20:25 +00:00
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
344 lines
8.1 KiB
C++
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
|