diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index bfb712ac4..5791b59df 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -154,6 +154,7 @@ if(ENABLE_SCRIPTING) script/cel_class.cpp script/cels_class.cpp script/color_class.cpp + script/dialog_class.cpp script/engine.cpp script/frame_class.cpp script/frames_class.cpp diff --git a/src/app/app.cpp b/src/app/app.cpp index 86027303b..d23060d6a 100644 --- a/src/app/app.cpp +++ b/src/app/app.cpp @@ -392,6 +392,11 @@ App::~App() LOG("APP: Exit\n"); ASSERT(m_instance == this); +#ifdef ENABLE_SCRIPTING + // Destroy scripting engine + m_engine.reset(nullptr); +#endif + // Delete file formats. FileFormatsManager::destroyInstance(); diff --git a/src/app/script/color_class.cpp b/src/app/script/color_class.cpp index 62a4d6031..370793d4e 100644 --- a/src/app/script/color_class.cpp +++ b/src/app/script/color_class.cpp @@ -18,7 +18,7 @@ namespace script { namespace { -Color Color_new(lua_State* L, int index) +app::Color Color_new(lua_State* L, int index) { app::Color color; // Copy other color diff --git a/src/app/script/dialog_class.cpp b/src/app/script/dialog_class.cpp new file mode 100644 index 000000000..464609dc3 --- /dev/null +++ b/src/app/script/dialog_class.cpp @@ -0,0 +1,602 @@ +// Aseprite +// Copyright (C) 2018 David Capello +// +// 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/color.h" +#include "app/color_utils.h" +#include "app/script/engine.h" +#include "app/script/luacpp.h" +#include "app/ui/color_button.h" +#include "app/ui/expr_entry.h" +#include "ui/box.h" +#include "ui/button.h" +#include "ui/combobox.h" +#include "ui/entry.h" +#include "ui/grid.h" +#include "ui/label.h" +#include "ui/separator.h" +#include "ui/slider.h" +#include "ui/window.h" + +#include + +namespace app { +namespace script { + +using namespace ui; + +namespace { + +struct Dialog { + ui::Window window; + ui::VBox vbox; + ui::Grid grid; + ui::HBox* hbox = nullptr; + std::map dataWidgets; + int currentRadioGroup = 0; + + // Used to create a new row when a different kind of widget is added + // in the dialog. + ui::WidgetType lastWidgetType = ui::kGenericWidget; + + // Used to keep a reference to the last onclick button pressed, so + // then Dialog.data returns true for the button that closed the + // dialog. + ui::Widget* lastButton = nullptr; + + // Reference used to keep the dialog alive (so it's not garbage + // collected) when it's visible. + int showRef = LUA_REFNIL; + lua_State* L = nullptr; + + // A reference to a table with pointers to functions for events + // (e.g. onclick callbacks). + int callbacksTableRef = LUA_REFNIL; + + Dialog() + : window(ui::Window::WithTitleBar, "Script"), + grid(2, false) { + window.addChild(&grid); + window.Close.connect([this](ui::CloseEvent&){ unrefShow(); }); + } + + void refShow(lua_State* L) { + if (showRef == LUA_REFNIL) { + this->L = L; + lua_pushvalue(L, 1); + showRef = luaL_ref(L, LUA_REGISTRYINDEX); + } + } + + void unrefShow() { + if (showRef != LUA_REFNIL) { + luaL_unref(this->L, LUA_REGISTRYINDEX, showRef); + showRef = LUA_REFNIL; + L = nullptr; + } + } +}; + +int Dialog_new(lua_State* L) +{ + auto dlg = push_new(L); + if (lua_isstring(L, 1)) + dlg->window.setText(lua_tostring(L, 1)); + + lua_newtable(L); +#if 0 // Make values in callbacksTableRef table weak. (Weak references + // are lost for onclick= events with local functions when we use + // showInBackground() and we left the script scope) + { + lua_newtable(L); + lua_pushstring(L, "v"); + lua_setfield(L, -2, "__mode"); + lua_setmetatable(L, -2); + } +#endif + dlg->callbacksTableRef = luaL_ref(L, LUA_REGISTRYINDEX); + return 1; +} + +int Dialog_gc(lua_State* L) +{ + auto dlg = get_obj(L, 1); + luaL_unref(L, LUA_REGISTRYINDEX, dlg->callbacksTableRef); + dlg->~Dialog(); + return 0; +} + +int Dialog_show(lua_State* L) +{ + auto dlg = get_obj(L, 1); + dlg->refShow(L); + + bool wait = true; + if (lua_istable(L, 2)) { + int type = lua_getfield(L, 2, "wait"); + if (type == LUA_TBOOLEAN) + wait = lua_toboolean(L, -1); + lua_pop(L, 1); + } + + if (wait) + dlg->window.openWindowInForeground(); + else + dlg->window.openWindow(); + + lua_pushvalue(L, 1); + return 1; +} + +int Dialog_close(lua_State* L) +{ + auto dlg = get_obj(L, 1); + dlg->window.closeWindow(nullptr); + lua_pushvalue(L, 1); + return 1; +} + +int Dialog_add_widget(lua_State* L, Widget* widget) +{ + auto dlg = get_obj(L, 1); + const char* label = nullptr; + + // This is to separate different kind of widgets without label in + // different rows. + if (dlg->lastWidgetType != widget->type()) { + dlg->lastWidgetType = widget->type(); + dlg->hbox = nullptr; + } + + if (lua_istable(L, 2)) { + // Widget ID (used to fill the Dialog_get_data table then) + int type = lua_getfield(L, 2, "id"); + if (type == LUA_TSTRING) { + if (auto id = lua_tostring(L, -1)) { + widget->setId(id); + dlg->dataWidgets[id] = widget; + } + } + lua_pop(L, 1); + + // Label + type = lua_getfield(L, 2, "label"); + if (type == LUA_TSTRING) + label = lua_tostring(L, -1); + lua_pop(L, 1); + } + + if (label || !dlg->hbox) { + if (label) + dlg->grid.addChildInCell(new ui::Label(label), 1, 1, ui::LEFT | ui::TOP); + else + dlg->grid.addChildInCell(new ui::HBox, 1, 1, ui::LEFT | ui::TOP); + + auto hbox = new ui::HBox; + if (widget->type() == ui::kButtonWidget) + hbox->enableFlags(ui::HOMOGENEOUS); + dlg->grid.addChildInCell(hbox, 1, 1, ui::HORIZONTAL | ui::TOP); + dlg->hbox = hbox; + } + + widget->setExpansive(true); + dlg->hbox->addChild(widget); + + lua_pushvalue(L, 1); + return 1; +} + +int Dialog_newrow(lua_State* L) +{ + auto dlg = get_obj(L, 1); + dlg->hbox = nullptr; + lua_pushvalue(L, 1); + return 1; +} + +int Dialog_separator(lua_State* L) +{ + auto dlg = get_obj(L, 1); + + std::string text; + if (lua_isstring(L, 2)) { + if (auto p = lua_tostring(L, 2)) + text = p; + } + else if (lua_istable(L, 2)) { + int type = lua_getfield(L, 2, "text"); + if (type == LUA_TSTRING) { + if (auto p = lua_tostring(L, -1)) + text = p; + } + lua_pop(L, 1); + } + + auto widget = new ui::Separator(text, ui::HORIZONTAL); + dlg->grid.addChildInCell(widget, 2, 1, ui::HORIZONTAL | ui::TOP); + dlg->hbox = nullptr; + + lua_pushvalue(L, 1); + return 1; +} + +int Dialog_label(lua_State* L) +{ + std::string text; + if (lua_istable(L, 2)) { + int type = lua_getfield(L, 2, "text"); + if (type == LUA_TSTRING) { + if (auto p = lua_tostring(L, -1)) + text = p; + } + lua_pop(L, 1); + } + + auto widget = new ui::Label(text.c_str()); + return Dialog_add_widget(L, widget); +} + +template +int Dialog_button_base(lua_State* L, T** outputWidget = nullptr) +{ + auto dlg = get_obj(L, 1); + + std::string text; + if (lua_istable(L, 2)) { + int type = lua_getfield(L, 2, "text"); + if (type == LUA_TSTRING) { + if (auto p = lua_tostring(L, -1)) + text = p; + } + lua_pop(L, 1); + } + + auto widget = new T(text.c_str()); + if (outputWidget) + *outputWidget = widget; + + widget->processMnemonicFromText(); + + bool closeWindowByDefault = (widget->type() == ui::kButtonWidget); + + if (lua_istable(L, 2)) { + int type = lua_getfield(L, 2, "selected"); + if (type != LUA_TNONE) + widget->setSelected(lua_toboolean(L, -1)); + lua_pop(L, 1); + + type = lua_getfield(L, 2, "onclick"); + if (type == LUA_TFUNCTION) { + // Add a reference to the onclick function + const int ref = dlg->callbacksTableRef; + lua_rawgeti(L, LUA_REGISTRYINDEX, ref); + lua_len(L, -1); + const int n = 1+lua_tointegerx(L, -1, nullptr); + lua_pop(L, 1); + lua_pushvalue(L, -2); // Copy the function in onclick + lua_seti(L, -2, n); // Put the copy of the function in the callbacksTableRef + widget->Click.connect( + [dlg, widget, L, ref, n](ui::Event& ev) { + dlg->lastButton = widget; + + lua_rawgeti(L, LUA_REGISTRYINDEX, ref); + lua_geti(L, -1, n); + + if (lua_isfunction(L, -1)) + lua_call(L, 0, 0); + else + lua_pop(L, 1); + lua_pop(L, 1); // Pop table from the registry + }); + closeWindowByDefault = false; + } + lua_pop(L, 1); + } + + if (closeWindowByDefault) + widget->Click.connect([widget](ui::Event&){ widget->closeWindow(); }); + + return Dialog_add_widget(L, widget); +} + +int Dialog_button(lua_State* L) +{ + return Dialog_button_base(L); +} + +int Dialog_check(lua_State* L) +{ + return Dialog_button_base(L); +} + +int Dialog_radio(lua_State* L) +{ + ui::RadioButton* radio = nullptr; + const int res = Dialog_button_base(L, &radio); + if (radio) { + auto dlg = get_obj(L, 1); + bool hasLabelField = false; + + if (lua_istable(L, 2)) { + int type = lua_getfield(L, 2, "label"); + if (type == LUA_TSTRING) + hasLabelField = true; + lua_pop(L, 1); + } + + if (dlg->currentRadioGroup == 0 || + hasLabelField) { + ++dlg->currentRadioGroup; + } + + radio->setRadioGroup(dlg->currentRadioGroup); + } + return res; +} + +int Dialog_entry(lua_State* L) +{ + std::string text; + if (lua_istable(L, 2)) { + int type = lua_getfield(L, 2, "text"); + if (type == LUA_TSTRING) { + if (auto p = lua_tostring(L, -1)) + text = p; + } + lua_pop(L, 1); + } + + auto widget = new ui::Entry(4096, text.c_str()); + return Dialog_add_widget(L, widget); +} + +int Dialog_number(lua_State* L) +{ + auto widget = new ExprEntry; + + if (lua_istable(L, 2)) { + int type = lua_getfield(L, 2, "text"); + if (type == LUA_TSTRING) { + if (auto p = lua_tostring(L, -1)) + widget->setText(p); + } + lua_pop(L, 1); + + type = lua_getfield(L, 2, "decimals"); + if (type != LUA_TNONE && + type != LUA_TNIL) { + widget->setDecimals(lua_tointegerx(L, -1, nullptr)); + } + lua_pop(L, 1); + } + + return Dialog_add_widget(L, widget); +} + +int Dialog_slider(lua_State* L) +{ + int min = 0; + int max = 100; + int value = 100; + + if (lua_istable(L, 2)) { + int type = lua_getfield(L, 2, "min"); + if (type != LUA_TNONE) { + min = lua_tointegerx(L, -1, nullptr); + } + lua_pop(L, 1); + + type = lua_getfield(L, 2, "max"); + if (type != LUA_TNONE) { + max = lua_tointegerx(L, -1, nullptr); + } + lua_pop(L, 1); + + type = lua_getfield(L, 2, "value"); + if (type != LUA_TNONE) { + value = lua_tointegerx(L, -1, nullptr); + } + lua_pop(L, 1); + } + + auto widget = new ui::Slider(min, max, value); + return Dialog_add_widget(L, widget); +} + +int Dialog_combobox(lua_State* L) +{ + auto widget = new ui::ComboBox; + + if (lua_istable(L, 2)) { + int type = lua_getfield(L, 2, "options"); + if (type == LUA_TTABLE) { + lua_pushnil(L); + while (lua_next(L, -2) != 0) { + if (auto p = lua_tostring(L, -1)) + widget->addItem(p); + lua_pop(L, 1); + } + } + lua_pop(L, 1); + + type = lua_getfield(L, 2, "option"); + if (type == LUA_TSTRING) { + if (auto p = lua_tostring(L, -1)) { + int index = widget->findItemIndex(p); + if (index >= 0) + widget->setSelectedItemIndex(index); + } + } + lua_pop(L, 1); + } + + return Dialog_add_widget(L, widget); +} + +int Dialog_color(lua_State* L) +{ + app::Color color; + if (lua_istable(L, 2)) { + lua_getfield(L, 2, "color"); + color = convert_args_into_color(L, -1); + lua_pop(L, 1); + } + + auto widget = new ColorButton(color, + app_get_current_pixel_format(), + ColorButtonOptions()); + return Dialog_add_widget(L, widget); +} + +int Dialog_get_data(lua_State* L) +{ + auto dlg = get_obj(L, 1); + lua_newtable(L); + for (const auto& kv : dlg->dataWidgets) { + const ui::Widget* widget = kv.second; + switch (widget->type()) { + case ui::kButtonWidget: + case ui::kCheckWidget: + case ui::kRadioWidget: + lua_pushboolean(L, widget->isSelected() || + dlg->window.closer() == widget || + dlg->lastButton == widget); + break; + case ui::kEntryWidget: + if (auto expr = dynamic_cast(widget)) { + if (expr->decimals() == 0) + lua_pushinteger(L, widget->textInt()); + else + lua_pushnumber(L, widget->textDouble()); + } + else { + lua_pushstring(L, widget->text().c_str()); + } + break; + case ui::kLabelWidget: + lua_pushstring(L, widget->text().c_str()); + break; + case ui::kSliderWidget: + if (auto slider = dynamic_cast(widget)) { + lua_pushinteger(L, slider->getValue()); + } + break; + case ui::kComboBoxWidget: + if (auto combobox = dynamic_cast(widget)) { + if (auto sel = combobox->getSelectedItem()) + lua_pushstring(L, sel->text().c_str()); + else + lua_pushnil(L); + } + break; + default: + if (auto colorButton = dynamic_cast(widget)) + push_obj(L, colorButton->getColor()); + else + lua_pushnil(L); + break; + } + lua_setfield(L, -2, kv.first.c_str()); + } + return 1; +} + +int Dialog_set_data(lua_State* L) +{ + auto dlg = get_obj(L, 1); + if (!lua_istable(L, 2)) + return 0; + for (const auto& kv : dlg->dataWidgets) { + lua_getfield(L, 2, kv.first.c_str()); + + ui::Widget* widget = kv.second; + switch (widget->type()) { + case ui::kButtonWidget: + case ui::kCheckWidget: + case ui::kRadioWidget: + widget->setSelected(lua_toboolean(L, -1)); + break; + case ui::kEntryWidget: + if (auto expr = dynamic_cast(widget)) { + if (expr->decimals() == 0) + expr->setTextf("%d", lua_tointeger(L, -1)); + else + expr->setTextf("%.*g", expr->decimals(), lua_tonumber(L, -1)); + } + else if (auto p = lua_tostring(L, -1)) { + widget->setText(p); + } + break; + case ui::kLabelWidget: + if (auto p = lua_tostring(L, -1)) { + widget->setText(p); + } + break; + case ui::kSliderWidget: + if (auto slider = dynamic_cast(widget)) { + slider->setValue(lua_tointeger(L, -1)); + } + break; + case ui::kComboBoxWidget: + if (auto combobox = dynamic_cast(widget)) { + if (auto p = lua_tostring(L, -1)) { + int index = combobox->findItemIndex(p); + if (index >= 0) + combobox->setSelectedItemIndex(index); + } + } + break; + default: + if (auto colorButton = dynamic_cast(widget)) + colorButton->setColor(convert_args_into_color(L, -1)); + break; + } + + lua_pop(L, 1); + } + return 1; +} + +const luaL_Reg Dialog_methods[] = { + { "__gc", Dialog_gc }, + { "show", Dialog_show }, + { "close", Dialog_close }, + { "newrow", Dialog_newrow }, + { "separator", Dialog_separator }, + { "label", Dialog_label }, + { "button", Dialog_button }, + { "check", Dialog_check }, + { "radio", Dialog_radio }, + { "entry", Dialog_entry }, + { "number", Dialog_number }, + { "slider", Dialog_slider }, + { "combobox", Dialog_combobox }, + { "color", Dialog_color }, + { nullptr, nullptr } +}; + +const Property Dialog_properties[] = { + { "data", Dialog_get_data, Dialog_set_data }, + { nullptr, nullptr, nullptr } +}; + +} // anonymous namespace + +DEF_MTNAME(Dialog); + +void register_dialog_class(lua_State* L) +{ + REG_CLASS(L, Dialog); + REG_CLASS_NEW(L, Dialog); + REG_CLASS_PROPERTIES(L, Dialog); +} + +} // namespace script +} // namespace app diff --git a/src/app/script/engine.cpp b/src/app/script/engine.cpp index cad49d68a..e77052612 100644 --- a/src/app/script/engine.cpp +++ b/src/app/script/engine.cpp @@ -88,6 +88,7 @@ void register_app_command_object(lua_State* L); void register_cel_class(lua_State* L); void register_cels_class(lua_State* L); void register_color_class(lua_State* L); +void register_dialog_class(lua_State* L); void register_frame_class(lua_State* L); void register_frames_class(lua_State* L); void register_image_class(lua_State* L); @@ -216,6 +217,7 @@ Engine::Engine() register_cel_class(L); register_cels_class(L); register_color_class(L); + register_dialog_class(L); register_frame_class(L); register_frames_class(L); register_image_class(L); @@ -275,6 +277,9 @@ bool Engine::evalCode(const std::string& code, } } lua_pop(L, 1); + + // Collect script garbage. + lua_gc(L, LUA_GCCOLLECT); return ok; } diff --git a/src/app/ui/expr_entry.h b/src/app/ui/expr_entry.h index 0929c2b53..6c00d6c86 100644 --- a/src/app/ui/expr_entry.h +++ b/src/app/ui/expr_entry.h @@ -17,9 +17,8 @@ namespace app { public: ExprEntry(); - void setDecimals(int decimals) { - m_decimals = decimals; - } + int decimals() const { return m_decimals; } + void setDecimals(int decimals) { m_decimals = decimals; } protected: bool onProcessMessage(ui::Message* msg) override; diff --git a/src/ui/button.cpp b/src/ui/button.cpp index bc8b7f41f..1667b9b36 100644 --- a/src/ui/button.cpp +++ b/src/ui/button.cpp @@ -1,5 +1,5 @@ // Aseprite UI Library -// Copyright (C) 2001-2017 David Capello +// Copyright (C) 2001-2018 David Capello // // This file is released under the terms of the MIT license. // Read LICENSE.txt for more information. @@ -22,9 +22,9 @@ namespace ui { ButtonBase::ButtonBase(const std::string& text, - WidgetType type, - WidgetType behaviorType, - WidgetType drawType) + const WidgetType type, + const WidgetType behaviorType, + const WidgetType drawType) : Widget(type) , m_pressedStatus(false) , m_behaviorType(behaviorType) @@ -274,7 +274,8 @@ Button::Button(const std::string& text) // CheckBox class // ====================================================================== -CheckBox::CheckBox(const std::string& text, WidgetType drawType) +CheckBox::CheckBox(const std::string& text, + const WidgetType drawType) : ButtonBase(text, kCheckWidget, kCheckWidget, drawType) { setAlign(LEFT | MIDDLE); @@ -284,7 +285,9 @@ CheckBox::CheckBox(const std::string& text, WidgetType drawType) // RadioButton class // ====================================================================== -RadioButton::RadioButton(const std::string& text, int radioGroup, WidgetType drawType) +RadioButton::RadioButton(const std::string& text, + const int radioGroup, + const WidgetType drawType) : ButtonBase(text, kRadioWidget, kRadioWidget, drawType) { setAlign(LEFT | MIDDLE); diff --git a/src/ui/button.h b/src/ui/button.h index 442d6efdb..793bdc96c 100644 --- a/src/ui/button.h +++ b/src/ui/button.h @@ -1,5 +1,5 @@ // Aseprite UI Library -// Copyright (C) 2001-2017 David Capello +// Copyright (C) 2001-2018 David Capello // // This file is released under the terms of the MIT license. // Read LICENSE.txt for more information. @@ -25,9 +25,9 @@ namespace ui { class ButtonBase : public Widget { public: ButtonBase(const std::string& text, - WidgetType type, - WidgetType behaviorType, - WidgetType drawType); + const WidgetType type, + const WidgetType behaviorType, + const WidgetType drawType); virtual ~ButtonBase(); WidgetType behaviorType() const; @@ -60,13 +60,16 @@ namespace ui { // Check boxes class CheckBox : public ButtonBase { public: - CheckBox(const std::string& text, WidgetType drawType = kCheckWidget); + CheckBox(const std::string& text, + const WidgetType drawType = kCheckWidget); }; // Radio buttons class RadioButton : public ButtonBase { public: - RadioButton(const std::string& text, int radioGroup, WidgetType drawType = kRadioWidget); + RadioButton(const std::string& text, + const int radioGroup = 0, + const WidgetType drawType = kRadioWidget); int getRadioGroup() const; void setRadioGroup(int radioGroup);