[lua] Add Plugin:newMenuGroup() to add submenus (fix #3731)

This commit is contained in:
David Capello 2023-03-09 16:41:37 -03:00
parent b43f2a3428
commit 23557a190b
6 changed files with 217 additions and 42 deletions

View File

@ -1,5 +1,5 @@
// Aseprite // Aseprite
// Copyright (C) 2019-2022 Igara Studio S.A. // Copyright (C) 2019-2023 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello // Copyright (C) 2001-2018 David Capello
// //
// This program is distributed under the terms of // This program is distributed under the terms of
@ -283,6 +283,19 @@ AppMenuItem::Native get_native_shortcut_for_command(
return native; return native;
} }
void destroy_menu_item(ui::Widget* item)
{
if (item->parent())
item->parent()->removeChild(item);
if (auto appItem = dynamic_cast<AppMenuItem*>(item)) {
if (appItem)
appItem->disposeNative();
}
item->deferDelete();
}
} // anonymous namespace } // anonymous namespace
os::Shortcut get_os_shortcut_from_key(const Key* key) os::Shortcut get_os_shortcut_from_key(const Key* key)
@ -341,9 +354,12 @@ void AppMenus::reload()
for (auto& it : m_groups) { for (auto& it : m_groups) {
GroupInfo& group = it.second; GroupInfo& group = it.second;
MENUS_TRACE("MENUS: - groups", it.first, "with", group.items.size(), "item(s)"); MENUS_TRACE("MENUS: - groups", it.first, "with", group.items.size(), "item(s)");
group.end = nullptr; // This value will be restored later
for (auto& item : group.items) for (auto& item : group.items)
item->parent()->removeChild(item); group.menu->removeChild(item);
// These values will be restored later
group.end = nullptr;
} }
//////////////////////////////////////// ////////////////////////////////////////
@ -412,11 +428,13 @@ void AppMenus::reload()
for (auto& it : m_groups) { for (auto& it : m_groups) {
GroupInfo& group = it.second; GroupInfo& group = it.second;
if (group.end) { if (group.menu) {
MENUS_TRACE("MENUS: - re-adding group ", it.first, "with", group.items.size(), "item(s)"); MENUS_TRACE("MENUS: - re-adding group ", it.first, "with", group.items.size(), "item(s)");
auto menu = group.end->parent(); auto menu = group.menu;
int insertIndex = menu->getChildIndex(group.end); int insertIndex = menu->getChildIndex(group.end);
if (insertIndex < 0)
insertIndex = -1;
for (auto& item : group.items) { for (auto& item : group.items) {
menu->insertChild(++insertIndex, item); menu->insertChild(++insertIndex, item);
group.end = item; group.end = item;
@ -590,14 +608,51 @@ bool AppMenus::rebuildRecentList()
return true; return true;
} }
void AppMenus::addMenuGroup(const std::string& groupId,
MenuItem* menuItem)
{
GroupInfo& group = m_groups[groupId];
ASSERT(group.menu == nullptr);
ASSERT(group.end == nullptr);
group.menu = menuItem->getSubmenu();
group.end = nullptr;
}
void AppMenus::removeMenuGroup(const std::string& groupId)
{
auto it = m_groups.find(groupId);
if (it != m_groups.end()) {
GroupInfo& group = it->second;
ASSERT(group.items.empty()); // To remove a group, the group must be empty
if (group.menu->getOwnerMenuItem()) {
ui::MenuItem* item = group.menu->getOwnerMenuItem();
removeMenuItemFromGroup(
[item](Widget* i){
return item == i;
});
}
m_groups.erase(it);
}
}
void AppMenus::addMenuItemIntoGroup(const std::string& groupId, void AppMenus::addMenuItemIntoGroup(const std::string& groupId,
std::unique_ptr<MenuItem>&& menuItem) std::unique_ptr<MenuItem>&& menuItem)
{ {
GroupInfo& group = m_groups[groupId]; GroupInfo& group = m_groups[groupId];
Widget* menu = group.end->parent();
Menu* menu = group.menu;
ASSERT(menu); ASSERT(menu);
int insertIndex = menu->getChildIndex(group.end);
menu->insertChild(insertIndex+1, menuItem.get()); if (group.end) {
int insertIndex = menu->getChildIndex(group.end);
ASSERT(insertIndex >= 0);
menu->insertChild(insertIndex+1, menuItem.get());
}
else {
menu->addChild(menuItem.get());
}
group.end = menuItem.get(); group.end = menuItem.get();
group.items.push_back(menuItem.get()); group.items.push_back(menuItem.get());
@ -616,12 +671,7 @@ void AppMenus::removeMenuItemFromGroup(Pred pred)
if (item == group.end) if (item == group.end)
group.end = group.end->previousSibling(); group.end = group.end->previousSibling();
item->parent()->removeChild(item); destroy_menu_item(item);
if (auto appItem = dynamic_cast<AppMenuItem*>(item)) {
if (appItem)
appItem->disposeNative();
}
item->deferDelete();
it = group.items.erase(it); it = group.items.erase(it);
} }
@ -679,7 +729,7 @@ Menu* AppMenus::convertXmlelemToMenu(TiXmlElement* elem)
TiXmlElement* child = elem->FirstChildElement(); TiXmlElement* child = elem->FirstChildElement();
while (child) { while (child) {
Widget* menuitem = convertXmlelemToMenuitem(child); Widget* menuitem = convertXmlelemToMenuitem(child, menu);
if (menuitem) if (menuitem)
menu->addChild(menuitem); menu->addChild(menuitem);
else else
@ -692,7 +742,7 @@ Menu* AppMenus::convertXmlelemToMenu(TiXmlElement* elem)
return menu; return menu;
} }
Widget* AppMenus::convertXmlelemToMenuitem(TiXmlElement* elem) Widget* AppMenus::convertXmlelemToMenuitem(TiXmlElement* elem, Menu* menu)
{ {
const char* id = elem->Attribute("id"); const char* id = elem->Attribute("id");
const char* group = elem->Attribute("group"); const char* group = elem->Attribute("group");
@ -709,8 +759,10 @@ Widget* AppMenus::convertXmlelemToMenuitem(TiXmlElement* elem)
m_recentFilesPlaceholder = item; m_recentFilesPlaceholder = item;
} }
} }
if (group) if (group) {
m_groups[group].menu = menu;
m_groups[group].end = item; m_groups[group].end = item;
}
return item; return item;
} }
@ -743,8 +795,10 @@ Widget* AppMenus::convertXmlelemToMenuitem(TiXmlElement* elem)
if (id) menuitem->setId(id); if (id) menuitem->setId(id);
menuitem->processMnemonicFromText(); menuitem->processMnemonicFromText();
if (group) if (group) {
m_groups[group].menu = menu;
m_groups[group].end = menuitem; m_groups[group].end = menuitem;
}
if (standard && strcmp(standard, "edit") == 0) if (standard && strcmp(standard, "edit") == 0)
menuitem->setStandardEditMenu(); menuitem->setStandardEditMenu();

View File

@ -1,5 +1,5 @@
// Aseprite // Aseprite
// Copyright (C) 2019-2020 Igara Studio S.A. // Copyright (C) 2019-2023 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello // Copyright (C) 2001-2018 David Capello
// //
// This program is distributed under the terms of // This program is distributed under the terms of
@ -60,6 +60,9 @@ namespace app {
void syncNativeMenuItemKeyShortcuts(); void syncNativeMenuItemKeyShortcuts();
// Menu item handling in groups // Menu item handling in groups
void addMenuGroup(const std::string& groupId,
MenuItem* menuItem);
void removeMenuGroup(const std::string& groupId);
void addMenuItemIntoGroup(const std::string& groupId, void addMenuItemIntoGroup(const std::string& groupId,
std::unique_ptr<MenuItem>&& menuItem); std::unique_ptr<MenuItem>&& menuItem);
void removeMenuItemFromGroup(Command* cmd); void removeMenuItemFromGroup(Command* cmd);
@ -71,7 +74,7 @@ namespace app {
Menu* loadMenuById(TiXmlHandle& handle, const char *id); Menu* loadMenuById(TiXmlHandle& handle, const char *id);
Menu* convertXmlelemToMenu(TiXmlElement* elem); Menu* convertXmlelemToMenu(TiXmlElement* elem);
Widget* convertXmlelemToMenuitem(TiXmlElement* elem); Widget* convertXmlelemToMenuitem(TiXmlElement* elem, Menu* menu);
void applyShortcutToMenuitemsWithCommand(Menu* menu, Command* command, const Params& params, void applyShortcutToMenuitemsWithCommand(Menu* menu, Command* command, const Params& params,
const KeyPtr& key); const KeyPtr& key);
void syncNativeMenuItemKeyShortcuts(Menu* menu); void syncNativeMenuItemKeyShortcuts(Menu* menu);
@ -87,6 +90,7 @@ namespace app {
#endif #endif
struct GroupInfo { struct GroupInfo {
Menu* menu = nullptr;
Widget* end = nullptr; Widget* end = nullptr;
WidgetsList items; WidgetsList items;
}; };

View File

@ -1,5 +1,5 @@
// Aseprite // Aseprite
// Copyright (C) 2020-2022 Igara Studio S.A. // Copyright (C) 2020-2023 Igara Studio S.A.
// Copyright (C) 2017-2018 David Capello // Copyright (C) 2017-2018 David Capello
// //
// This program is distributed under the terms of // This program is distributed under the terms of
@ -315,6 +315,27 @@ void Extension::removeCommand(const std::string& id)
} }
} }
void Extension::addMenuGroup(const std::string& id)
{
PluginItem item;
item.type = PluginItem::MenuGroup;
item.id = id;
m_plugin.items.push_back(item);
}
void Extension::removeMenuGroup(const std::string& id)
{
for (auto it=m_plugin.items.begin(); it != m_plugin.items.end(); ) {
if (it->type == PluginItem::MenuGroup &&
it->id == id) {
it = m_plugin.items.erase(it);
}
else {
++it;
}
}
}
#endif #endif
bool Extension::canBeDisabled() const bool Extension::canBeDisabled() const
@ -685,30 +706,39 @@ void Extension::exitScripts()
m_plugin.pluginRef = LUA_REFNIL; m_plugin.pluginRef = LUA_REFNIL;
} }
// Remove plugin items automatically // Remove plugin items automatically (first commands, then menu
// groups)
for (const auto& item : m_plugin.items) { for (const auto& item : m_plugin.items) {
switch (item.type) { if (item.type == PluginItem::Command) {
case PluginItem::Command: { auto cmds = Commands::instance();
auto cmds = Commands::instance(); auto cmd = cmds->byId(item.id.c_str());
auto cmd = cmds->byId(item.id.c_str()); ASSERT(cmd);
ASSERT(cmd);
if (cmd) { if (cmd) {
#ifdef ENABLE_UI #ifdef ENABLE_UI
// TODO use a signal // TODO use a signal
AppMenus::instance()->removeMenuItemFromGroup(cmd); AppMenus::instance()->removeMenuItemFromGroup(cmd);
#endif // ENABLE_UI #endif // ENABLE_UI
cmds->remove(cmd); cmds->remove(cmd);
// This will call ~PluginCommand() and unref the command // This will call ~PluginCommand() and unref the command
// onclick callback. // onclick callback.
delete cmd; delete cmd;
}
break;
} }
} }
} }
for (const auto& item : m_plugin.items) {
if (item.type == PluginItem::MenuGroup) {
#ifdef ENABLE_UI
// TODO use a signal
AppMenus::instance()->removeMenuGroup(item.id);
#endif // ENABLE_UI
break;
}
}
m_plugin.items.clear(); m_plugin.items.clear();
} }

View File

@ -1,5 +1,5 @@
// Aseprite // Aseprite
// Copyright (C) 2020-2022 Igara Studio S.A. // Copyright (C) 2020-2023 Igara Studio S.A.
// Copyright (C) 2017-2018 David Capello // Copyright (C) 2017-2018 David Capello
// //
// This program is distributed under the terms of // This program is distributed under the terms of
@ -111,6 +111,9 @@ namespace app {
#ifdef ENABLE_SCRIPTING #ifdef ENABLE_SCRIPTING
void addCommand(const std::string& id); void addCommand(const std::string& id);
void removeCommand(const std::string& id); void removeCommand(const std::string& id);
void addMenuGroup(const std::string& id);
void removeMenuGroup(const std::string& id);
#endif #endif
bool isEnabled() const { return m_isEnabled; } bool isEnabled() const { return m_isEnabled; }
@ -155,7 +158,7 @@ namespace app {
ScriptItem(const std::string& fn); ScriptItem(const std::string& fn);
}; };
struct PluginItem { struct PluginItem {
enum Type { Command }; enum Type { Command, MenuGroup };
Type type; Type type;
std::string id; std::string id;
}; };

View File

@ -1,5 +1,5 @@
// Aseprite // Aseprite
// Copyright (C) 2018-2022 Igara Studio S.A. // Copyright (C) 2018-2023 Igara Studio S.A.
// //
// This program is distributed under the terms of // This program is distributed under the terms of
// the End-User License Agreement for Aseprite. // the End-User License Agreement for Aseprite.
@ -10,6 +10,6 @@
// Increment this value if the scripting API is modified between two // Increment this value if the scripting API is modified between two
// released Aseprite versions. // released Aseprite versions.
#define API_VERSION 21 #define API_VERSION 22
#endif #endif

View File

@ -1,5 +1,5 @@
// Aseprite // Aseprite
// Copyright (C) 2020-2022 Igara Studio S.A. // Copyright (C) 2020-2023 Igara Studio S.A.
// //
// This program is distributed under the terms of // This program is distributed under the terms of
// the End-User License Agreement for Aseprite. // the End-User License Agreement for Aseprite.
@ -108,6 +108,16 @@ void deleteCommandIfExistent(Extension* ext, const std::string& id)
} }
} }
void deleteMenuGroupIfExistent(Extension* ext, const std::string& id)
{
#ifdef ENABLE_UI
if (auto appMenus = AppMenus::instance())
appMenus->removeMenuGroup(id);
#endif
ext->removeMenuGroup(id);
}
int Plugin_gc(lua_State* L) int Plugin_gc(lua_State* L)
{ {
get_obj<Plugin>(L, 1)->~Plugin(); get_obj<Plugin>(L, 1)->~Plugin();
@ -167,7 +177,7 @@ int Plugin_newCommand(lua_State* L)
if (!group.empty() && if (!group.empty() &&
App::instance()->isGui()) { // On CLI menus do not make sense App::instance()->isGui()) { // On CLI menus do not make sense
if (auto appMenus = AppMenus::instance()) { if (auto appMenus = AppMenus::instance()) {
std::unique_ptr<MenuItem> menuItem(new AppMenuItem(title, id)); auto menuItem = std::make_unique<AppMenuItem>(title, id);
appMenus->addMenuItemIntoGroup(group, std::move(menuItem)); appMenus->addMenuItemIntoGroup(group, std::move(menuItem));
} }
} }
@ -204,6 +214,78 @@ int Plugin_deleteCommand(lua_State* L)
return 0; return 0;
} }
int Plugin_newMenuGroup(lua_State* L)
{
auto plugin = get_obj<Plugin>(L, 1);
if (lua_istable(L, 2)) {
std::string id, title, group;
lua_getfield(L, 2, "id"); // This new group 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);
lua_getfield(L, 2, "group"); // Parent group
if (const char* s = lua_tostring(L, -1)) {
group = s;
}
lua_pop(L, 1);
// Delete the group if it already exist (e.g. we are overwriting a
// previous registered group)
deleteMenuGroupIfExistent(plugin->ext, id);
plugin->ext->addMenuGroup(id);
#ifdef ENABLE_UI
// Add a new menu option if the "group" is defined
if (!group.empty() &&
App::instance()->isGui()) { // On CLI menus do not make sense
if (auto appMenus = AppMenus::instance()) {
auto menuItem = std::make_unique<AppMenuItem>(title, id);
menuItem->setSubmenu(new Menu);
appMenus->addMenuGroup(id, menuItem.get());
appMenus->addMenuItemIntoGroup(group, std::move(menuItem));
}
}
#endif // ENABLE_UI
}
return 0;
}
int Plugin_deleteMenuGroup(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 menu group id specified in plugin:deleteMenuGroup()");
deleteMenuGroupIfExistent(plugin->ext, id);
return 0;
}
int Plugin_get_preferences(lua_State* L) int Plugin_get_preferences(lua_State* L)
{ {
if (!lua_getuservalue(L, 1)) { if (!lua_getuservalue(L, 1)) {
@ -225,6 +307,8 @@ const luaL_Reg Plugin_methods[] = {
{ "__gc", Plugin_gc }, { "__gc", Plugin_gc },
{ "newCommand", Plugin_newCommand }, { "newCommand", Plugin_newCommand },
{ "deleteCommand", Plugin_deleteCommand }, { "deleteCommand", Plugin_deleteCommand },
{ "newMenuGroup", Plugin_newMenuGroup },
{ "deleteMenuGroup", Plugin_deleteMenuGroup },
{ nullptr, nullptr } { nullptr, nullptr }
}; };