Add support for scripts on extensions (#1949)

It still need some work to associate the command to menus easily.

Related issues:
  https://github.com/aseprite/aseprite/issues/1403
  https://github.com/aseprite/aseprite/issues/1949
  https://github.com/aseprite/api/issues/20
  https://community.aseprite.org/t/lua-script-extension-and-menu-api/5085
This commit is contained in:
David Capello 2020-04-02 15:47:12 -03:00
parent ba51812c89
commit 6b6b9057bf
12 changed files with 625 additions and 3 deletions

2
laf

@ -1 +1 @@
Subproject commit 98506341d68a318f4916a891ab12351a5db05f98
Subproject commit 5c0fa215cd6fba21d9bf230d2da62d2890b21baf

View File

@ -170,6 +170,7 @@ if(ENABLE_SCRIPTING)
script/palette_class.cpp
script/palettes_class.cpp
script/pixel_color_object.cpp
script/plugin_class.cpp
script/point_class.cpp
script/preferences_object.cpp
script/range_class.cpp

View File

@ -309,6 +309,12 @@ int App::initialize(const AppOptions& options)
}
#endif // ENABLE_UI
#ifdef ENABLE_SCRIPTING
// Call the init() function from all plugins
LOG("APP: Initializing scripts...\n");
extensions().executeInitActions();
#endif
// Process options
LOG("APP: Processing options...\n");
{
@ -395,7 +401,7 @@ void App::run()
// we've to print errors).
Console console;
#ifdef ENABLE_SCRIPTING
// Use the app::Console() for script erros
// Use the app::Console() for script errors
ConsoleEngineDelegate delegate;
script::ScopedEngineDelegate setEngineDelegate(m_engine.get(), &delegate);
#endif
@ -414,6 +420,13 @@ void App::run()
}
#endif // ENABLE_SCRIPTING
// ----------------------------------------------------------------------
#ifdef ENABLE_SCRIPTING
// Call the exit() function from all plugins
extensions().executeExitActions();
#endif
#ifdef ENABLE_UI
if (isGui()) {
// Select no document

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2020 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This program is distributed under the terms of
@ -71,6 +72,15 @@ Commands* Commands::add(Command* command)
return this;
}
void Commands::remove(Command* command)
{
auto lid = base::string_to_lower(command->id());
auto it = m_commands.find(lid);
ASSERT(it != m_commands.end());
if (it != m_commands.end())
m_commands.erase(it);
}
void Commands::getAllIds(std::vector<std::string>& ids)
{
for (auto& it : m_commands)

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2020 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This program is distributed under the terms of
@ -30,6 +31,9 @@ namespace app {
Command* byId(const char* id);
Commands* add(Command* command);
// Remove the command but doesn't delete it
void remove(Command* command);
void getAllIds(std::vector<std::string>& ids);
private:

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2020 Igara Studio S.A.
// Copyright (C) 2017-2018 David Capello
//
// This program is distributed under the terms of
@ -10,16 +11,26 @@
#include "app/extensions.h"
#include "app/app.h"
#include "app/commands/command.h"
#include "app/commands/commands.h"
#include "app/console.h"
#include "app/ini_file.h"
#include "app/load_matrix.h"
#include "app/pref/preferences.h"
#include "app/resource_finder.h"
#include "base/exception.h"
#include "base/file_content.h"
#include "base/file_handle.h"
#include "base/fs.h"
#include "base/fstream_path.h"
#include "render/dithering_matrix.h"
#ifdef ENABLE_SCRIPTING
#include "app/script/engine.h"
#include "app/script/luacpp.h"
#endif
#include "archive.h"
#include "archive_entry.h"
#include "json11.hpp"
@ -35,6 +46,7 @@ namespace {
const char* kPackageJson = "package.json";
const char* kInfoJson = "__info.json";
const char* kPrefLua = "__pref.lua";
const char* kAsepriteDefaultThemeExtensionName = "aseprite-theme";
class ReadArchive {
@ -181,6 +193,9 @@ void write_json_file(const std::string& path, const json11::Json& json)
} // anonymous namespace
//////////////////////////////////////////////////////////////////////
// Extension
const render::DitheringMatrix& Extension::DitheringMatrixInfo::matrix() const
{
if (!m_matrix) {
@ -219,6 +234,22 @@ Extension::~Extension()
it.second.destroyMatrix();
}
void Extension::executeInitActions()
{
#ifdef ENABLE_SCRIPTING
if (isEnabled() && hasScripts())
initScripts();
#endif
}
void Extension::executeExitActions()
{
#ifdef ENABLE_SCRIPTING
if (isEnabled() && hasScripts())
exitScripts();
#endif // ENABLE_SCRIPTING
}
void Extension::addLanguage(const std::string& id, const std::string& path)
{
m_languages[id] = path;
@ -242,6 +273,31 @@ void Extension::addDitheringMatrix(const std::string& id,
m_ditheringMatrices[id] = info;
}
#ifdef ENABLE_SCRIPTING
void Extension::addCommand(const std::string& id)
{
PluginItem item;
item.type = PluginItem::Command;
item.id = id;
m_plugin.items.push_back(item);
}
void Extension::removeCommand(const std::string& id)
{
for (auto it=m_plugin.items.begin(); it != m_plugin.items.end(); ) {
if (it->type == PluginItem::Command &&
it->id == id) {
it = m_plugin.items.erase(it);
}
else {
++it;
}
}
}
#endif
bool Extension::canBeDisabled() const
{
return (m_isEnabled &&
@ -267,6 +323,15 @@ void Extension::enable(const bool state)
flush_config_file();
m_isEnabled = state;
#ifdef ENABLE_SCRIPTING
if (m_isEnabled) {
initScripts();
}
else {
exitScripts();
}
#endif // ENABLE_SCRIPTING
}
void Extension::uninstall()
@ -314,6 +379,13 @@ void Extension::uninstallFiles(const std::string& path)
}
}
// Delete __pref.lua file
{
std::string fn = base::join_path(path, kPrefLua);
if (base::is_file(fn))
base::delete_file(fn);
}
std::sort(installedDirs.begin(),
installedDirs.end(),
[](const std::string& a,
@ -369,6 +441,230 @@ bool Extension::isDefaultTheme() const
return (name() == kAsepriteDefaultThemeExtensionName);
}
#ifdef ENABLE_SCRIPTING
// TODO move this to app/script/tableutils.h
static void serialize_table(lua_State* L, int idx, std::string& result)
{
bool first = true;
result.push_back('{');
idx = lua_absindex(L, idx);
lua_pushnil(L);
while (lua_next(L, idx) != 0) {
if (first) {
first = false;
}
else {
result.push_back(',');
}
// Save key
if (lua_type(L, -2) == LUA_TSTRING) {
if (const char* k = lua_tostring(L, -2)) {
result += k;
result.push_back('=');
}
}
// Save value
switch (lua_type(L, -1)) {
case LUA_TNIL:
default:
result += "nil";
break;
case LUA_TBOOLEAN:
if (lua_toboolean(L, -1))
result += "true";
else
result += "false";
break;
case LUA_TNUMBER:
result += lua_tostring(L, -1);
break;
case LUA_TSTRING:
result.push_back('\"');
if (const char* p = lua_tostring(L, -1)) {
for (; *p; ++p) {
switch (*p) {
case '\"':
result.push_back('\\');
result.push_back('\"');
break;
case '\\':
result.push_back('\\');
result.push_back('\\');
break;
case '\t':
result.push_back('\\');
result.push_back('t');
break;
case '\r':
result.push_back('\\');
result.push_back('n');
break;
case '\n':
result.push_back('\\');
result.push_back('n');
break;
default:
result.push_back(*p);
break;
}
}
}
result.push_back('\"');
break;
case LUA_TTABLE:
serialize_table(L, -1, result);
break;
}
lua_pop(L, 1);
}
result.push_back('}');
}
Extension::ScriptItem::ScriptItem(const std::string& fn)
: fn(fn)
, exitFunctionRef(LUA_REFNIL)
{
}
void Extension::initScripts()
{
script::Engine* engine = App::instance()->scriptEngine();
lua_State* L = engine->luaState();
// Put a new "plugin" object for init()/exit() functions
script::push_plugin(L, this);
m_plugin.pluginRef = luaL_ref(L, LUA_REGISTRYINDEX);
// Read plugin.preferences value
{
std::string fn = base::join_path(m_path, kPrefLua);
if (base::is_file(fn)) {
lua_rawgeti(L, LUA_REGISTRYINDEX, m_plugin.pluginRef);
if (luaL_loadfile(L, fn.c_str()) == LUA_OK) {
if (lua_pcall(L, 0, 1, 0) == LUA_OK) {
lua_setfield(L, -2, "preferences");
}
else {
const char* s = lua_tostring(L, -1);
if (s) {
Console().printf("%s\n", s);
}
}
lua_pop(L, 1);
}
else {
lua_pop(L, 1);
}
}
}
for (auto& script : m_plugin.scripts) {
// Reset global init()/exit() functions
engine->evalCode("init=nil exit=nil");
// Eval the code of the script (it should define an init() and an exit() function)
engine->evalFile(script.fn);
if (lua_getglobal(L, "exit") == LUA_TFUNCTION) {
// Save a reference to the exit() function of this script
script.exitFunctionRef = luaL_ref(L, LUA_REGISTRYINDEX);
}
else {
lua_pop(L, 1);
}
// Call the init() function of thi sscript with a Plugin object as first parameter
if (lua_getglobal(L, "init") == LUA_TFUNCTION) {
// Call init(plugin)
lua_rawgeti(L, LUA_REGISTRYINDEX, m_plugin.pluginRef);
lua_pcall(L, 1, 1, 0);
lua_pop(L, 1);
}
else {
lua_pop(L, 1);
}
}
}
void Extension::exitScripts()
{
script::Engine* engine = App::instance()->scriptEngine();
lua_State* L = engine->luaState();
// Call the exit() function of each script
for (auto& script : m_plugin.scripts) {
if (script.exitFunctionRef != LUA_REFNIL) {
// Get the exit() function, the "plugin" object, and call exit(plugin)
lua_rawgeti(L, LUA_REGISTRYINDEX, script.exitFunctionRef);
lua_rawgeti(L, LUA_REGISTRYINDEX, m_plugin.pluginRef);
lua_pcall(L, 1, 0, 0);
luaL_unref(L, LUA_REGISTRYINDEX, script.exitFunctionRef);
script.exitFunctionRef = LUA_REFNIL;
}
}
// Save the plugin preferences object
if (m_plugin.pluginRef != LUA_REFNIL) {
lua_rawgeti(L, LUA_REGISTRYINDEX, m_plugin.pluginRef);
lua_getfield(L, -1, "preferences");
lua_pushnil(L); // Push a nil key, to ask for the first element of the table
bool hasPreferences = (lua_next(L, -2) != 0);
if (hasPreferences)
lua_pop(L, 2); // Remove the value and the key
if (hasPreferences) {
std::string result = "return ";
serialize_table(L, -1, result);
base::write_file_content(
base::join_path(m_path, kPrefLua),
(const uint8_t*)result.c_str(), result.size());
}
lua_pop(L, 2); // Pop preferences table and plugin
luaL_unref(L, LUA_REGISTRYINDEX, m_plugin.pluginRef);
m_plugin.pluginRef = LUA_REFNIL;
}
// Remove plugin items automatically
for (const auto& item : m_plugin.items) {
switch (item.type) {
case PluginItem::Command: {
auto cmds = Commands::instance();
auto cmd = cmds->byId(item.id.c_str());
ASSERT(cmd);
if (cmd) {
cmds->remove(cmd);
// This will call ~PluginCommand() and unref the command
// onclick callback.
delete cmd;
}
break;
}
}
}
m_plugin.items.clear();
}
void Extension::addScript(const std::string& fn)
{
m_plugin.scripts.push_back(ScriptItem(fn));
}
#endif // ENABLE_SCRIPTING
//////////////////////////////////////////////////////////////////////
// Extensions
Extensions::Extensions()
{
// Create and get the user extensions directory
@ -429,6 +725,22 @@ Extensions::~Extensions()
delete ext;
}
void Extensions::executeInitActions()
{
for (auto& ext : m_extensions)
ext->executeInitActions();
ScriptsChange(nullptr);
}
void Extensions::executeExitActions()
{
for (auto& ext : m_extensions)
ext->executeExitActions();
ScriptsChange(nullptr);
}
std::string Extensions::languagePath(const std::string& langId)
{
for (auto ext : m_extensions) {
@ -739,6 +1051,37 @@ Extension* Extensions::loadExtension(const std::string& path,
extension->addDitheringMatrix(matId, matPath, matName);
}
}
#ifdef ENABLE_SCRIPTING
// Scripts
auto scripts = contributes["scripts"];
if (scripts.is_array()) {
for (const auto& script : scripts.array_items()) {
std::string scriptPath = script["path"].string_value();
if (scriptPath.empty())
continue;
// The path must be always relative to the extension
scriptPath = base::join_path(path, scriptPath);
LOG("EXT: New script '%s'\n", scriptPath.c_str());
extension->addScript(scriptPath);
}
}
// Simple version of packages.json with {... "scripts": "file.lua" ...}
else if (scripts.is_string() &&
!scripts.string_value().empty()) {
std::string scriptPath = scripts.string_value();
// The path must be always relative to the extension
scriptPath = base::join_path(path, scriptPath);
LOG("EXT: New script '%s'\n", scriptPath.c_str());
extension->addScript(scriptPath);
}
#endif // ENABLE_SCRIPTING
}
if (extension)
@ -752,6 +1095,7 @@ void Extensions::generateExtensionSignals(Extension* extension)
if (extension->hasThemes()) ThemesChange(extension);
if (extension->hasPalettes()) PalettesChange(extension);
if (extension->hasDitheringMatrices()) DitheringMatricesChange(extension);
if (extension->hasScripts()) ScriptsChange(extension);
}
} // namespace app

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2020 Igara Studio S.A.
// Copyright (C) 2017-2018 David Capello
//
// This program is distributed under the terms of
@ -61,6 +62,9 @@ namespace app {
const bool isBuiltinExtension);
~Extension();
void executeInitActions();
void executeExitActions();
const std::string& path() const { return m_path; }
const std::string& name() const { return m_name; }
const std::string& version() const { return m_version; }
@ -76,6 +80,10 @@ namespace app {
void addDitheringMatrix(const std::string& id,
const std::string& path,
const std::string& name);
#ifdef ENABLE_SCRIPTING
void addCommand(const std::string& id);
void removeCommand(const std::string& id);
#endif
bool isEnabled() const { return m_isEnabled; }
bool isInstalled() const { return m_isInstalled; }
@ -86,6 +94,10 @@ namespace app {
bool hasThemes() const { return !m_themes.empty(); }
bool hasPalettes() const { return !m_palettes.empty(); }
bool hasDitheringMatrices() const { return !m_ditheringMatrices.empty(); }
#ifdef ENABLE_SCRIPTING
bool hasScripts() const { return !m_plugin.scripts.empty(); }
void addScript(const std::string& fn);
#endif
private:
void enable(const bool state);
@ -93,11 +105,34 @@ namespace app {
void uninstallFiles(const std::string& path);
bool isCurrentTheme() const;
bool isDefaultTheme() const;
#ifdef ENABLE_SCRIPTING
void initScripts();
void exitScripts();
#endif
ExtensionItems m_languages;
ExtensionItems m_themes;
ExtensionItems m_palettes;
std::map<std::string, DitheringMatrixInfo> m_ditheringMatrices;
#ifdef ENABLE_SCRIPTING
struct ScriptItem {
std::string fn;
int exitFunctionRef;
ScriptItem(const std::string& fn);
};
struct PluginItem {
enum Type { Command };
Type type;
std::string id;
};
struct Plugin {
int pluginRef;
std::vector<ScriptItem> scripts;
std::vector<PluginItem> items;
} m_plugin;
#endif
std::string m_path;
std::string m_name;
std::string m_version;
@ -115,6 +150,9 @@ namespace app {
Extensions();
~Extensions();
void executeInitActions();
void executeExitActions();
iterator begin() { return m_extensions.begin(); }
iterator end() { return m_extensions.end(); }
@ -136,6 +174,7 @@ namespace app {
obs::signal<void(Extension*)> ThemesChange;
obs::signal<void(Extension*)> PalettesChange;
obs::signal<void(Extension*)> DitheringMatricesChange;
obs::signal<void(Extension*)> ScriptsChange;
private:
Extension* loadExtension(const std::string& path,

View File

@ -161,6 +161,7 @@ void register_layer_class(lua_State* L);
void register_layers_class(lua_State* L);
void register_palette_class(lua_State* L);
void register_palettes_class(lua_State* L);
void register_plugin_class(lua_State* L);
void register_point_class(lua_State* L);
void register_range_class(lua_State* L);
void register_rect_class(lua_State* L);
@ -370,6 +371,7 @@ Engine::Engine()
register_layers_class(L);
register_palette_class(L);
register_palettes_class(L);
register_plugin_class(L);
register_point_class(L);
register_range_class(L);
register_rect_class(L);

View File

@ -15,6 +15,7 @@
#include "app/color.h"
#include "app/commands/params.h"
#include "app/extensions.h"
#include "doc/brush.h"
#include "doc/frame.h"
#include "doc/object_ids.h"
@ -94,6 +95,8 @@ namespace app {
return m_returnCode;
}
lua_State* luaState() { return L; }
private:
void onConsolePrint(const char* text);
@ -131,6 +134,7 @@ namespace app {
void push_images(lua_State* L, const doc::ObjectIds& images);
void push_layers(lua_State* L, const doc::ObjectIds& layers);
void push_palette(lua_State* L, doc::Palette* palette);
void push_plugin(lua_State* L, Extension* ext);
void push_sprite_cel(lua_State* L, doc::Cel* cel);
void push_sprite_frame(lua_State* L, doc::Sprite* sprite, doc::frame_t frame);
void push_sprite_frames(lua_State* L, doc::Sprite* sprite);

View File

@ -0,0 +1,190 @@
// Aseprite
// Copyright (C) 2020 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/commands/command.h"
#include "app/commands/commands.h"
#include "app/script/engine.h"
#include "app/script/luacpp.h"
namespace app {
namespace script {
namespace {
struct Plugin {
Extension* ext;
Plugin(Extension* ext) : ext(ext) { }
};
class PluginCommand : public Command {
public:
PluginCommand(const std::string& id,
const std::string& title,
int onclickRef)
: Command(id.c_str(), CmdUIOnlyFlag)
, m_title(title)
, m_onclickRef(onclickRef) {
}
~PluginCommand() {
auto app = App::instance();
ASSERT(app);
if (!app)
return;
if (script::Engine* engine = app->scriptEngine()) {
lua_State* L = engine->luaState();
luaL_unref(L, LUA_REGISTRYINDEX, m_onclickRef);
}
}
protected:
std::string onGetFriendlyName() const override {
return m_title;
}
void onExecute(Context* context) override {
script::Engine* engine = App::instance()->scriptEngine();
lua_State* L = engine->luaState();
lua_rawgeti(L, LUA_REGISTRYINDEX, m_onclickRef);
lua_pcall(L, 0, 0, 0);
}
std::string m_title;
int m_onclickRef;
};
void deleteCommandIfExistent(Extension* ext, const std::string& id)
{
auto cmd = Commands::instance()->byId(id.c_str());
if (cmd) {
Commands::instance()->remove(cmd);
ext->removeCommand(id);
delete cmd;
}
}
int Plugin_gc(lua_State* L)
{
get_obj<Plugin>(L, 1)->~Plugin();
return 0;
}
int Plugin_newCommand(lua_State* L)
{
auto plugin = get_obj<Plugin>(L, 1);
if (lua_istable(L, 2)) {
std::string id, title;
lua_getfield(L, 2, "id");
if (const char* s = lua_tostring(L, -1)) {
id = s;
}
lua_pop(L, 1);
if (id.empty())
return luaL_error(L, "Empty id field in plugin:newCommand{ id=... }");
lua_getfield(L, 2, "title");
if (const char* s = lua_tostring(L, -1)) {
title = s;
}
lua_pop(L, 1);
int type = lua_getfield(L, 2, "onclick");
if (type == LUA_TFUNCTION) {
int onclickRef = luaL_ref(L, LUA_REGISTRYINDEX);
// Delete the command if it already exist (e.g. we are
// overwriting a previous registered command)
deleteCommandIfExistent(plugin->ext, id);
Commands::instance()->add(new PluginCommand(id, title, onclickRef));
plugin->ext->addCommand(id);
}
else {
lua_pop(L, 1);
}
}
return 0;
}
int Plugin_deleteCommand(lua_State* L)
{
std::string id;
auto plugin = get_obj<Plugin>(L, 1);
if (lua_istable(L, 2)) {
lua_getfield(L, 2, "id");
if (const char* s = lua_tostring(L, -1)) {
id = s;
}
lua_pop(L, 1);
}
else if (const char* s = lua_tostring(L, 2)) {
id = s;
}
if (id.empty())
return luaL_error(L, "No command id specified in plugin:deleteCommand()");
// TODO this can crash if we delete the command from the same command
deleteCommandIfExistent(plugin->ext, id);
return 0;
}
int Plugin_get_preferences(lua_State* L)
{
if (!lua_getuservalue(L, 1)) {
lua_newtable(L);
lua_pushvalue(L, -1);
lua_setuservalue(L, 1);
}
return 1;
}
int Plugin_set_preferences(lua_State* L)
{
lua_pushvalue(L, 2);
lua_setuservalue(L, 1);
return 0;
}
const luaL_Reg Plugin_methods[] = {
{ "__gc", Plugin_gc },
{ "newCommand", Plugin_newCommand },
{ "deleteCommand", Plugin_deleteCommand },
{ nullptr, nullptr }
};
const Property Plugin_properties[] = {
{ "preferences", Plugin_get_preferences, Plugin_set_preferences },
{ nullptr, nullptr, nullptr }
};
} // anonymous namespace
DEF_MTNAME(Plugin);
void register_plugin_class(lua_State* L)
{
REG_CLASS(L, Plugin);
REG_CLASS_PROPERTIES(L, Plugin);
}
void push_plugin(lua_State* L, Extension* ext)
{
push_new<Plugin>(L, ext);
}
} // namespace script
} // namespace app

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2020 Igara Studio S.A
// Copyright (C) 2001-2015 David Capello
//
// This program is distributed under the terms of
@ -10,22 +11,31 @@
#include "app/ui/main_menu_bar.h"
#include "app/app.h"
#include "app/app_menus.h"
#include "app/extensions.h"
#include "base/bind.h"
namespace app {
MainMenuBar::MainMenuBar()
{
Extensions& extensions = App::instance()->extensions();
m_extScripts =
extensions.ScriptsChange.connect(
base::Bind<void>(&MainMenuBar::reload, this));
}
void MainMenuBar::reload()
{
setMenu(NULL);
setMenu(nullptr);
// Reload all menus.
AppMenus::instance()->reload();
setMenu(AppMenus::instance()->getRootMenu());
parent()->layout();
}
} // namespace app

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2020 Igara Studio S.A
// Copyright (C) 2001-2015 David Capello
//
// This program is distributed under the terms of
@ -8,6 +9,7 @@
#define APP_UI_MAIN_MENU_BAR_H_INCLUDED
#pragma once
#include "obs/connection.h"
#include "ui/menu.h"
namespace app {
@ -17,6 +19,9 @@ namespace app {
MainMenuBar();
void reload();
private:
obs::scoped_connection m_extScripts;
};
} // namespace app