diff --git a/data/strings/en.ini b/data/strings/en.ini index 6a49f0807..fded586fa 100644 --- a/data/strings/en.ini +++ b/data/strings/en.ini @@ -1282,6 +1282,7 @@ download_themes = Download Themes open_theme_folder = Open &Folder language_extensions = Languages theme_extensions = Themes +keys_extensions = Keyboard Shortcuts script_extensions = Scripts palette_extensions = Palettes dithering_matrix_extensions = Dithering Matrices diff --git a/src/app/app_menus.cpp b/src/app/app_menus.cpp index c2418d849..65f04a8a2 100644 --- a/src/app/app_menus.cpp +++ b/src/app/app_menus.cpp @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2019-2020 Igara Studio S.A. +// Copyright (C) 2019-2022 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -16,6 +16,7 @@ #include "app/commands/commands.h" #include "app/commands/params.h" #include "app/console.h" +#include "app/extensions.h" #include "app/gui_xml.h" #include "app/i18n/strings.h" #include "app/recent_files.h" @@ -417,10 +418,22 @@ void AppMenus::reload() .FirstChild("gui") .FirstChild("keyboard").ToElement(); + // From a fresh start, load the default keys KeyboardShortcuts::instance()->clear(); KeyboardShortcuts::instance()->importFile(xmlKey, KeySource::Original); - // Load user settings + // Load extension-defined keys + for (const Extension* ext : App::instance()->extensions()) { + if (ext->isEnabled() && + ext->hasKeys()) { + for (const auto& kv : ext->keys()) { + KeyboardShortcuts::instance()->importFile( + kv.second, KeySource::ExtensionDefined); + } + } + } + + // Load user-defined keys { ResourceFinder rf; rf.includeUserDir("user.aseprite-keys"); diff --git a/src/app/commands/cmd_keyboard_shortcuts.cpp b/src/app/commands/cmd_keyboard_shortcuts.cpp index 120c54825..d544b80bc 100644 --- a/src/app/commands/cmd_keyboard_shortcuts.cpp +++ b/src/app/commands/cmd_keyboard_shortcuts.cpp @@ -58,7 +58,7 @@ using namespace ui; namespace { -typedef std::map MenuKeys; +using MenuKeys = std::map; class HeaderSplitter : public Splitter { public: @@ -195,7 +195,7 @@ private: window.openWindowInForeground(); if (window.isModified()) { - m_key->disableAccel(origAccel); + m_key->disableAccel(origAccel, KeySource::UserDefined); if (!window.accel().isEmpty()) m_key->add(window.accel(), KeySource::UserDefined, m_keys); } @@ -215,7 +215,7 @@ private: accel.toString())) != 1) return; - m_key->disableAccel(accel); + m_key->disableAccel(accel, KeySource::UserDefined); window()->layout(); } diff --git a/src/app/commands/cmd_options.cpp b/src/app/commands/cmd_options.cpp index 99c13b969..80f0ce451 100644 --- a/src/app/commands/cmd_options.cpp +++ b/src/app/commands/cmd_options.cpp @@ -1277,6 +1277,10 @@ private: if (extensionsList()->getItemsCount() > 0) return; + loadExtensionsByCategory( + Extension::Category::Keys, + Strings::options_keys_extensions()); + loadExtensionsByCategory( Extension::Category::Languages, Strings::options_language_extensions()); diff --git a/src/app/extensions.cpp b/src/app/extensions.cpp index cde81cd99..e97febdcd 100644 --- a/src/app/extensions.cpp +++ b/src/app/extensions.cpp @@ -257,6 +257,12 @@ void Extension::executeExitActions() #endif // ENABLE_SCRIPTING } +void Extension::addKeys(const std::string& id, const std::string& path) +{ + m_keys[id] = path; + updateCategory(Category::Keys); +} + void Extension::addLanguage(const std::string& id, const std::string& path) { m_languages[id] = path; @@ -456,8 +462,10 @@ bool Extension::isDefaultTheme() const void Extension::updateCategory(const Category newCategory) { - if (m_category == Category::None) + if (m_category == Category::None || + m_category == Category::Keys) { m_category = newCategory; + } else if (m_category != newCategory) m_category = Category::Multiple; } @@ -1016,6 +1024,24 @@ Extension* Extensions::loadExtension(const std::string& path, auto contributes = json["contributes"]; if (contributes.is_object()) { + // Keys + auto keys = contributes["keys"]; + if (keys.is_array()) { + for (const auto& key : keys.array_items()) { + std::string keyId = key["id"].string_value(); + std::string keyPath = key["path"].string_value(); + + // The path must be always relative to the extension + keyPath = base::join_path(path, keyPath); + + LOG("EXT: New keyboard shortcuts '%s' in '%s'\n", + keyId.c_str(), + keyPath.c_str()); + + extension->addKeys(keyId, keyPath); + } + } + // Languages auto languages = contributes["languages"]; if (languages.is_array()) { @@ -1130,6 +1156,7 @@ Extension* Extensions::loadExtension(const std::string& path, void Extensions::generateExtensionSignals(Extension* extension) { + if (extension->hasKeys()) KeysChange(extension); if (extension->hasLanguages()) LanguagesChange(extension); if (extension->hasThemes()) ThemesChange(extension); if (extension->hasPalettes()) PalettesChange(extension); diff --git a/src/app/extensions.h b/src/app/extensions.h index ab2e7492b..00b222d27 100644 --- a/src/app/extensions.h +++ b/src/app/extensions.h @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2020 Igara Studio S.A. +// Copyright (C) 2020-2022 Igara Studio S.A. // Copyright (C) 2017-2018 David Capello // // This program is distributed under the terms of @@ -36,6 +36,7 @@ namespace app { public: enum class Category { None, + Keys, Languages, Themes, Scripts, @@ -78,10 +79,12 @@ namespace app { const std::string& displayName() const { return m_displayName; } const Category category() const { return m_category; } + const ExtensionItems& keys() const { return m_keys; } const ExtensionItems& languages() const { return m_languages; } const ExtensionItems& themes() const { return m_themes; } const ExtensionItems& palettes() const { return m_palettes; } + void addKeys(const std::string& id, const std::string& path); void addLanguage(const std::string& id, const std::string& path); void addTheme(const std::string& id, const std::string& path); void addPalette(const std::string& id, const std::string& path); @@ -98,6 +101,7 @@ namespace app { bool canBeDisabled() const; bool canBeUninstalled() const; + bool hasKeys() const { return !m_keys.empty(); } bool hasLanguages() const { return !m_languages.empty(); } bool hasThemes() const { return !m_themes.empty(); } bool hasPalettes() const { return !m_palettes.empty(); } @@ -119,6 +123,7 @@ namespace app { void exitScripts(); #endif + ExtensionItems m_keys; ExtensionItems m_languages; ExtensionItems m_themes; ExtensionItems m_palettes; @@ -180,6 +185,7 @@ namespace app { std::vector ditheringMatrices(); obs::signal NewExtension; + obs::signal KeysChange; obs::signal LanguagesChange; obs::signal ThemesChange; obs::signal PalettesChange; diff --git a/src/app/ui/key.h b/src/app/ui/key.h index 7ff1f41ec..923193808 100644 --- a/src/app/ui/key.h +++ b/src/app/ui/key.h @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2018 Igara Studio S.A. +// Copyright (C) 2018-2022 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -15,6 +15,7 @@ #include "ui/accelerator.h" #include +#include #include namespace ui { @@ -31,6 +32,7 @@ namespace app { enum class KeySource { Original, + ExtensionDefined, UserDefined }; @@ -98,20 +100,20 @@ namespace app { return KeyAction(int(a) & int(b)); } + using KeySourceAccelList = std::vector>; + class Key { public: + Key(const Key& key); Key(Command* command, const Params& params, KeyContext keyContext); Key(KeyType type, tools::Tool* tool); explicit Key(KeyAction action); explicit Key(WheelAction action); KeyType type() const { return m_type; } - const ui::Accelerators& accels() const { - return (m_useUsers ? m_users: m_accels); - } - const ui::Accelerators& origAccels() const { return m_accels; } - const ui::Accelerators& userAccels() const { return m_users; } - const ui::Accelerators& userRemovedAccels() const { return m_userRemoved; } + const ui::Accelerators& accels() const; + const KeySourceAccelList addsKeys() const { return m_adds; } + const KeySourceAccelList delsKeys() const { return m_dels; } void add(const ui::Accelerator& accel, const KeySource source, @@ -122,9 +124,15 @@ namespace app { bool isLooselyPressed() const; bool hasAccel(const ui::Accelerator& accel) const; - void disableAccel(const ui::Accelerator& accel); + bool hasUserDefinedAccels() const; - // Resets user accelerators to the original ones. + // The KeySource indicates from where the key was disabled + // (e.g. if it was removed from an extension-defined file, or from + // user-defined). + void disableAccel(const ui::Accelerator& accel, + const KeySource source); + + // Resets user accelerators to the original & extension-defined ones. void reset(); void copyOriginalToUser(); @@ -144,10 +152,11 @@ namespace app { private: KeyType m_type; - ui::Accelerators m_accels; // Default accelerators (from gui.xml) - ui::Accelerators m_users; // User-defined accelerators - ui::Accelerators m_userRemoved; // Default accelerators removed by user - bool m_useUsers; + KeySourceAccelList m_adds; + KeySourceAccelList m_dels; + // Final list of accelerators after processing the + // addition/deletion of extension-defined & user-defined keys. + mutable std::unique_ptr m_accels; KeyContext m_keycontext; // for KeyType::Command @@ -161,8 +170,8 @@ namespace app { WheelAction m_wheelAction; }; - typedef std::shared_ptr KeyPtr; - typedef std::vector Keys; + using KeyPtr = std::shared_ptr ; + using Keys = std::vector; std::string convertKeyContextToString(KeyContext keyContext); std::string convertKeyContextToUserFriendlyString(KeyContext keyContext); diff --git a/src/app/ui/keyboard_shortcuts.cpp b/src/app/ui/keyboard_shortcuts.cpp index 4803bae0f..e0d728857 100644 --- a/src/app/ui/keyboard_shortcuts.cpp +++ b/src/app/ui/keyboard_shortcuts.cpp @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2018-2020 Igara Studio S.A. +// Copyright (C) 2018-2022 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -124,6 +124,32 @@ namespace { return std::string(); } + void erase_accel(app::KeySourceAccelList& kvs, + const app::KeySource source, + const ui::Accelerator& accel) { + for (auto it=kvs.begin(); it!=kvs.end(); ) { + auto& kv = *it; + if (kv.first == source && + kv.second == accel) { + it = kvs.erase(it); + } + else + ++it; + } + } + + void erase_accels(app::KeySourceAccelList& kvs, + const app::KeySource source) { + for (auto it=kvs.begin(); it!=kvs.end(); ) { + auto& kv = *it; + if (kv.first == source) { + it = kvs.erase(it); + } + else + ++it; + } + } + } // anonymous namespace namespace base { @@ -171,9 +197,33 @@ using namespace ui; ////////////////////////////////////////////////////////////////////// // Key +Key::Key(const Key& k) + : m_type(k.m_type) + , m_adds(k.m_adds) + , m_dels(k.m_dels) + , m_keycontext(k.m_keycontext) +{ + switch (m_type) { + case KeyType::Command: + m_command = k.m_command; + m_params = k.m_params; + break; + case KeyType::Tool: + case KeyType::Quicktool: + m_tool = k.m_tool; + break; + case KeyType::Action: + m_action = k.m_action; + break; + case KeyType::WheelAction: + m_action = k.m_action; + m_wheelAction = k.m_wheelAction; + break; + } +} + Key::Key(Command* command, const Params& params, KeyContext keyContext) : m_type(KeyType::Command) - , m_useUsers(false) , m_keycontext(keyContext) , m_command(command) , m_params(params) @@ -182,7 +232,6 @@ Key::Key(Command* command, const Params& params, KeyContext keyContext) Key::Key(KeyType type, tools::Tool* tool) : m_type(type) - , m_useUsers(false) , m_keycontext(KeyContext::Any) , m_tool(tool) { @@ -190,7 +239,6 @@ Key::Key(KeyType type, tools::Tool* tool) Key::Key(KeyAction action) : m_type(KeyType::Action) - , m_useUsers(false) , m_keycontext(KeyContext::Any) , m_action(action) { @@ -239,35 +287,63 @@ Key::Key(KeyAction action) Key::Key(WheelAction wheelAction) : m_type(KeyType::WheelAction) - , m_useUsers(false) , m_keycontext(KeyContext::MouseWheel) , m_action(KeyAction::None) , m_wheelAction(wheelAction) { } +const ui::Accelerators& Key::accels() const +{ + if (!m_accels) { + m_accels = std::make_unique(); + + // Add default keys + for (const auto& kv : m_adds) { + if (kv.first == KeySource::Original) + m_accels->add(kv.second); + } + + // Delete/add extension-defined keys + for (const auto& kv : m_dels) { + if (kv.first == KeySource::ExtensionDefined) + m_accels->remove(kv.second); + else { + ASSERT(kv.first != KeySource::Original); + } + } + for (const auto& kv : m_adds) { + if (kv.first == KeySource::ExtensionDefined) + m_accels->add(kv.second); + } + + // Delete/add user-defined keys + for (const auto& kv : m_dels) { + if (kv.first == KeySource::UserDefined) + m_accels->remove(kv.second); + } + for (const auto& kv : m_adds) { + if (kv.first == KeySource::UserDefined) + m_accels->add(kv.second); + } + } + return *m_accels; +} + void Key::add(const ui::Accelerator& accel, const KeySource source, KeyboardShortcuts& globalKeys) { - Accelerators* accels = &m_accels; - - if (source == KeySource::UserDefined) { - if (!m_useUsers) { - m_useUsers = true; - m_users = m_accels; - } - accels = &m_users; - } + m_adds.emplace_back(source, accel); + m_accels.reset(); // Remove the accelerator from other commands - if (source == KeySource::UserDefined) { - globalKeys.disableAccel(accel, m_keycontext, this); - m_userRemoved.remove(accel); - } + if (source == KeySource::ExtensionDefined || + source == KeySource::UserDefined) { + erase_accel(m_dels, source, accel); - // Add the accelerator - accels->add(accel); + globalKeys.disableAccel(accel, source, m_keycontext, this); + } } const ui::Accelerator* Key::isPressed(const Message* msg, @@ -322,31 +398,47 @@ bool Key::hasAccel(const ui::Accelerator& accel) const return accels().has(accel); } -void Key::disableAccel(const ui::Accelerator& accel) +bool Key::hasUserDefinedAccels() const { - if (!m_useUsers) { - m_useUsers = true; - m_users = m_accels; + for (const auto& kv : m_adds) { + if (kv.first == KeySource::UserDefined) + return true; } + return false; +} - m_users.remove(accel); +void Key::disableAccel(const ui::Accelerator& accel, + const KeySource source) +{ + // It doesn't make sense that the default keyboard shortcuts file + // (gui.xml) removes some accelerator. + ASSERT(source != KeySource::Original); - if (m_accels.has(accel)) - m_userRemoved.add(accel); + erase_accel(m_adds, source, accel); + erase_accel(m_dels, source, accel); + + m_dels.emplace_back(source, accel); + m_accels.reset(); } void Key::reset() { - m_users.clear(); - m_userRemoved.clear(); - m_useUsers = false; + erase_accels(m_adds, KeySource::UserDefined); + erase_accels(m_dels, KeySource::UserDefined); + m_accels.reset(); } void Key::copyOriginalToUser() { - m_users = m_accels; - m_userRemoved.clear(); - m_useUsers = true; + // Erase all user-defined keys + erase_accels(m_adds, KeySource::UserDefined); + erase_accels(m_dels, KeySource::UserDefined); + + // Then copy all original & extension-defined keys as user-defined + auto copy = m_adds; + for (const auto& kv : copy) + m_adds.emplace_back(KeySource::UserDefined, kv.second); + m_accels.reset(); } std::string Key::triggerString() const @@ -465,7 +557,7 @@ void KeyboardShortcuts::importFile(TiXmlElement* rootElement, KeySource source) } } else - key->disableAccel(accel); + key->disableAccel(accel, source); } } } @@ -494,7 +586,7 @@ void KeyboardShortcuts::importFile(TiXmlElement* rootElement, KeySource source) if (!removed) key->add(accel, source, *this); else - key->disableAccel(accel); + key->disableAccel(accel, source); } } } @@ -522,7 +614,7 @@ void KeyboardShortcuts::importFile(TiXmlElement* rootElement, KeySource source) if (!removed) key->add(accel, source, *this); else - key->disableAccel(accel); + key->disableAccel(accel, source); } } } @@ -550,7 +642,7 @@ void KeyboardShortcuts::importFile(TiXmlElement* rootElement, KeySource source) if (!removed) key->add(accel, source, *this); else - key->disableAccel(accel); + key->disableAccel(accel, source); } } } @@ -578,7 +670,7 @@ void KeyboardShortcuts::importFile(TiXmlElement* rootElement, KeySource source) if (!removed) key->add(accel, source, *this); else - key->disableAccel(accel); + key->disableAccel(accel, source); } } } @@ -633,11 +725,13 @@ void KeyboardShortcuts::exportKeys(TiXmlElement& parent, KeyType type) if (key->type() != type) continue; - for (const ui::Accelerator& accel : key->userRemovedAccels()) - exportAccel(parent, key.get(), accel, true); + for (const auto& kv : key->delsKeys()) + if (kv.first == KeySource::UserDefined) + exportAccel(parent, key.get(), kv.second, true); - for (const ui::Accelerator& accel : key->userAccels()) - exportAccel(parent, key.get(), accel, false); + for (const auto& kv : key->addsKeys()) + if (kv.first == KeySource::UserDefined) + exportAccel(parent, key.get(), kv.second, false); } } @@ -773,17 +867,19 @@ KeyPtr KeyboardShortcuts::wheelAction(WheelAction wheelAction) } void KeyboardShortcuts::disableAccel(const ui::Accelerator& accel, + const KeySource source, const KeyContext keyContext, const Key* newKey) { for (KeyPtr& key : m_keys) { - if (key->keycontext() == keyContext && + if (key.get() != newKey && + key->keycontext() == keyContext && key->hasAccel(accel) && // Tools can contain the same keyboard shortcut (key->type() != KeyType::Tool || newKey == nullptr || newKey->type() != KeyType::Tool)) { - key->disableAccel(accel); + key->disableAccel(accel, source); } } } @@ -885,7 +981,7 @@ bool KeyboardShortcuts::hasMouseWheelCustomization() const { for (const KeyPtr& key : m_keys) { if (key->type() == KeyType::WheelAction && - !key->userAccels().empty()) + key->hasUserDefinedAccels()) return true; } return false; diff --git a/src/app/ui/keyboard_shortcuts.h b/src/app/ui/keyboard_shortcuts.h index 2bd04a863..934fe62c2 100644 --- a/src/app/ui/keyboard_shortcuts.h +++ b/src/app/ui/keyboard_shortcuts.h @@ -1,4 +1,5 @@ // Aseprite +// Copyright (C) 2022 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -50,6 +51,7 @@ namespace app { KeyPtr wheelAction(WheelAction action); void disableAccel(const ui::Accelerator& accel, + const KeySource source, const KeyContext keyContext, const Key* newKey); diff --git a/src/app/ui/main_menu_bar.cpp b/src/app/ui/main_menu_bar.cpp index 8461a6dd1..6ab3cbc76 100644 --- a/src/app/ui/main_menu_bar.cpp +++ b/src/app/ui/main_menu_bar.cpp @@ -24,9 +24,11 @@ MainMenuBar::MainMenuBar() { Extensions& extensions = App::instance()->extensions(); - m_extScripts = - extensions.ScriptsChange.connect( - [this]{ reload(); }); + // Reload the main menu if there are changes in keyboard shortcuts + // or scripts when extensions are installed/uninstalled or + // enabled/disabled. + m_extKeys = extensions.KeysChange.connect( [this]{ reload(); }); + m_extScripts = extensions.ScriptsChange.connect( [this]{ reload(); }); } void MainMenuBar::reload() diff --git a/src/app/ui/main_menu_bar.h b/src/app/ui/main_menu_bar.h index ec76e5e2f..fed0da4c3 100644 --- a/src/app/ui/main_menu_bar.h +++ b/src/app/ui/main_menu_bar.h @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2020 Igara Studio S.A +// Copyright (C) 2020-2022 Igara Studio S.A // Copyright (C) 2001-2015 David Capello // // This program is distributed under the terms of @@ -21,6 +21,7 @@ namespace app { void reload(); private: + obs::scoped_connection m_extKeys; obs::scoped_connection m_extScripts; };