diff --git a/Source/NimbleCommander/NimbleCommander.xcodeproj/project.pbxproj b/Source/NimbleCommander/NimbleCommander.xcodeproj/project.pbxproj index e79973944..5d0566f0c 100644 --- a/Source/NimbleCommander/NimbleCommander.xcodeproj/project.pbxproj +++ b/Source/NimbleCommander/NimbleCommander.xcodeproj/project.pbxproj @@ -197,6 +197,7 @@ CF36DBE01F6BCDA0004A018E /* WebDAVConnectionSheetController.xib in Resources */ = {isa = PBXBuildFile; fileRef = CF36DBE51F6BCDA0004A018E /* WebDAVConnectionSheetController.xib */; }; CF36DBE11F6BCDA0004A018E /* WebDAVConnectionSheetController.xib in Resources */ = {isa = PBXBuildFile; fileRef = CF36DBE51F6BCDA0004A018E /* WebDAVConnectionSheetController.xib */; }; CF36DBE21F6BCDA0004A018E /* WebDAVConnectionSheetController.xib in Resources */ = {isa = PBXBuildFile; fileRef = CF36DBE51F6BCDA0004A018E /* WebDAVConnectionSheetController.xib */; }; + CF371DAA2D18472E0034EB3F /* ActionsShortcutsManager_UT.cpp in Sources */ = {isa = PBXBuildFile; fileRef = CF371DA92D18472E0034EB3F /* ActionsShortcutsManager_UT.cpp */; }; CF45CA281D3E3A6F0074F04C /* PreferencesWindowToolsTab.xib in Resources */ = {isa = PBXBuildFile; fileRef = CF45CA2C1D3E3A6F0074F04C /* PreferencesWindowToolsTab.xib */; }; CF45CA291D3E3A6F0074F04C /* PreferencesWindowToolsTab.xib in Resources */ = {isa = PBXBuildFile; fileRef = CF45CA2C1D3E3A6F0074F04C /* PreferencesWindowToolsTab.xib */; }; CF45CA2D1D3E3AA70074F04C /* ExternalToolParameterValueSheetController.xib in Resources */ = {isa = PBXBuildFile; fileRef = CF45CA311D3E3AA70074F04C /* ExternalToolParameterValueSheetController.xib */; }; @@ -951,6 +952,7 @@ CF31F66F1DFE644B005A1A40 /* Layout.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = Layout.mm; path = NimbleCommander/States/FilePanels/Brief/Layout.mm; sourceTree = SOURCE_ROOT; }; CF36DBE41F6BCDA0004A018E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/WebDAVConnectionSheetController.xib; sourceTree = ""; }; CF36DBE61F6BCF5B004A018E /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/WebDAVConnectionSheetController.strings; sourceTree = ""; }; + CF371DA92D18472E0034EB3F /* ActionsShortcutsManager_UT.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = ActionsShortcutsManager_UT.cpp; path = NimbleCommander/Tests/ActionsShortcutsManager_UT.cpp; sourceTree = ""; }; CF3989C12B44447D006103C1 /* Base.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Base.xcodeproj; path = ../Base/Base.xcodeproj; sourceTree = ""; }; CF3B7F6E201F20D300BF2090 /* PanelControllerActionsDispatcher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PanelControllerActionsDispatcher.h; path = NimbleCommander/States/FilePanels/PanelControllerActionsDispatcher.h; sourceTree = SOURCE_ROOT; }; CF3B7F6F201F20D300BF2090 /* PanelControllerActionsDispatcher.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = PanelControllerActionsDispatcher.mm; path = NimbleCommander/States/FilePanels/PanelControllerActionsDispatcher.mm; sourceTree = SOURCE_ROOT; }; @@ -1698,12 +1700,13 @@ CF5DE0BF2584153B00604DEE /* Tests */ = { isa = PBXGroup; children = ( + CF371DA92D18472E0034EB3F /* ActionsShortcutsManager_UT.cpp */, CF764CE62587661300D7ED17 /* DragSender_UT.mm */, CF764D422587837200D7ED17 /* PanelBriefViewDynamicWidthLayoutEngine_UT.mm */, CF764D4E258785CC00D7ED17 /* PanelBriefViewFixedNumberLayoutEngine_UT.mm */, CF764D562587875E00D7ED17 /* PanelBriefViewFixedWidthLayoutEngine_UT.mm */, - CF5DE0C12584157B00604DEE /* Tests.cpp */, CF5DE0C02584157B00604DEE /* Tests.h */, + CF5DE0C12584157B00604DEE /* Tests.cpp */, CF67F1BE2954689000B92944 /* Theme_UT.mm */, CFD2D26028A03DC0003C18F9 /* ThemesManager_UT.mm */, ); @@ -3199,6 +3202,7 @@ files = ( CF67F1BF2954689000B92944 /* Theme_UT.mm in Sources */, CF764D572587875E00D7ED17 /* PanelBriefViewFixedWidthLayoutEngine_UT.mm in Sources */, + CF371DAA2D18472E0034EB3F /* ActionsShortcutsManager_UT.cpp in Sources */, CF764D4F258785CC00D7ED17 /* PanelBriefViewFixedNumberLayoutEngine_UT.mm in Sources */, CF0A48902BDDA64700833160 /* PFMoveToApplicationsShim.mm in Sources */, CF0A48912BDDA64A00833160 /* SparkleShim.m in Sources */, diff --git a/Source/NimbleCommander/NimbleCommander/Bootstrap/AppDelegate+ViewerCreation.mm b/Source/NimbleCommander/NimbleCommander/Bootstrap/AppDelegate+ViewerCreation.mm index fad564c0a..be472138a 100644 --- a/Source/NimbleCommander/NimbleCommander/Bootstrap/AppDelegate+ViewerCreation.mm +++ b/Source/NimbleCommander/NimbleCommander/Bootstrap/AppDelegate+ViewerCreation.mm @@ -20,12 +20,9 @@ - (NCViewerView *)makeViewerWithFrame:(NSRect)frame - (NCViewerViewController *)makeViewerController { - auto shortcuts = [](std::string_view _name) { - return ActionsShortcutsManager::Instance().ShortCutFromAction(_name); - }; return [[NCViewerViewController alloc] initWithHistory:self.internalViewerHistory config:self.globalConfig - shortcutsProvider:shortcuts]; + shortcuts:nc::core::ActionsShortcutsManager::Instance()]; } @end diff --git a/Source/NimbleCommander/NimbleCommander/Bootstrap/AppDelegate.mm b/Source/NimbleCommander/NimbleCommander/Bootstrap/AppDelegate.mm index b58b45535..59320385c 100644 --- a/Source/NimbleCommander/NimbleCommander/Bootstrap/AppDelegate.mm +++ b/Source/NimbleCommander/NimbleCommander/Bootstrap/AppDelegate.mm @@ -138,23 +138,6 @@ static void ResetDefaults() g_State->Commit(); } -static void UpdateMenuItemsPlaceholders(int _tag) -{ - static const auto app_name = - static_cast([NSBundle.mainBundle.infoDictionary objectForKey:@"CFBundleName"]); - - if( auto menu_item = [NSApp.mainMenu itemWithTagHierarchical:_tag] ) { - auto title = menu_item.title; - title = [title stringByReplacingOccurrencesOfString:@"{AppName}" withString:app_name]; - menu_item.title = title; - } -} - -static void UpdateMenuItemsPlaceholders(const char *_action) -{ - UpdateMenuItemsPlaceholders(ActionsShortcutsManager::TagFromAction(_action)); -} - static void CheckDefaultsReset() { const auto erase_mask = @@ -299,8 +282,7 @@ - (void)applicationWillFinishLaunching:(NSNotification *) [[maybe_unused]] _noti [self updateMainMenuFeaturesByVersionAndState]; // update menu with current shortcuts layout - ActionsShortcutsManager::Instance().SetMenuShortCuts([NSApp mainMenu]); - + [NSApp.mainMenu nc_setMenuItemShortcutsWithActionsShortcutsManager:nc::core::ActionsShortcutsManager::Instance()]; [self wireMenuDelegates]; if( nc::base::AmISandboxed() ) { @@ -318,9 +300,11 @@ - (void)applicationWillFinishLaunching:(NSNotification *) [[maybe_unused]] _noti - (void)wireMenuDelegates { // set up menu delegates. do this via DI to reduce links to AppDelegate in whole codebase - auto item_for_action = [](const char *_action) { - auto tag = ActionsShortcutsManager::TagFromAction(_action); - return [NSApp.mainMenu itemWithTagHierarchical:tag]; + auto item_for_action = [](const char *_action) -> NSMenuItem * { + const std::optional tag = nc::core::ActionsShortcutsManager::Instance().TagFromAction(_action); + if( tag == std::nullopt ) + return nil; + return [NSApp.mainMenu itemWithTagHierarchical:*tag]; }; static auto layouts_delegate = [[PanelViewLayoutsMenuDelegate alloc] initWithStorage:*self.panelLayouts]; @@ -371,7 +355,9 @@ - (void)wireMenuDelegates - (void)updateMainMenuFeaturesByVersionAndState { // disable some features available in menu by configuration limitation - auto tag_from_lit = [](const char *s) { return ActionsShortcutsManager::TagFromAction(s); }; + auto tag_from_lit = [](const char *s) { + return nc::core::ActionsShortcutsManager::Instance().TagFromAction(s).value(); + }; auto current_menuitem = [&](const char *s) { return [NSApp.mainMenu itemWithTagHierarchical:tag_from_lit(s)]; }; auto hide = [&](const char *s) { auto item = current_menuitem(s); @@ -408,13 +394,6 @@ - (void)applicationDidFinishLaunching:(NSNotification *) [[maybe_unused]] _notif #pragma clang diagnostic pop NSUpdateDynamicServices(); - // Since we have different app names (Nimble Commander and Nimble Commander Pro) and one - // fixed menu, we have to emplace the right title upon startup in some menu elements. - UpdateMenuItemsPlaceholders("menu.nimble_commander.about"); - UpdateMenuItemsPlaceholders("menu.nimble_commander.hide"); - UpdateMenuItemsPlaceholders("menu.nimble_commander.quit"); - UpdateMenuItemsPlaceholders(17000); // Menu->Help - [self temporaryFileStorage]; // implicitly runs the background temp storage purging // Non-MAS version extended logic below: @@ -649,10 +628,11 @@ - (IBAction)OnMenuToggleAdminMode:(id) [[maybe_unused]] _sender - (BOOL)validateMenuItem:(NSMenuItem *)item { - auto tag = item.tag; + static const int admin_mode_tag = + nc::core::ActionsShortcutsManager::Instance().TagFromAction("menu.nimble_commander.toggle_admin_mode").value(); + const long tag = item.tag; - IF_MENU_TAG("menu.nimble_commander.toggle_admin_mode") - { + if( tag == admin_mode_tag ) { bool enabled = nc::routedio::RoutedIO::Instance().Enabled(); item.title = enabled ? NSLocalizedString(@"Disable Admin Mode", "Menu item title for disabling an admin mode") : NSLocalizedString(@"Enable Admin Mode", "Menu item title for enabling an admin mode"); diff --git a/Source/NimbleCommander/NimbleCommander/Bootstrap/Base.lproj/MainMenu.xib b/Source/NimbleCommander/NimbleCommander/Bootstrap/Base.lproj/MainMenu.xib index c4830d36b..7659db882 100644 --- a/Source/NimbleCommander/NimbleCommander/Bootstrap/Base.lproj/MainMenu.xib +++ b/Source/NimbleCommander/NimbleCommander/Bootstrap/Base.lproj/MainMenu.xib @@ -1,8 +1,8 @@ - + - + @@ -17,7 +17,7 @@ - + @@ -46,7 +46,7 @@ - + @@ -65,7 +65,7 @@ - + @@ -1029,7 +1029,7 @@ CQ - + diff --git a/Source/NimbleCommander/NimbleCommander/Bootstrap/ru.lproj/MainMenu.strings b/Source/NimbleCommander/NimbleCommander/Bootstrap/ru.lproj/MainMenu.strings index 934f0c85a..25791e02f 100644 --- a/Source/NimbleCommander/NimbleCommander/Bootstrap/ru.lproj/MainMenu.strings +++ b/Source/NimbleCommander/NimbleCommander/Bootstrap/ru.lproj/MainMenu.strings @@ -76,8 +76,8 @@ /* Class = "NSMenu"; title = "Nimble Commander"; ObjectID = "57"; */ "57.title" = "Nimble Commander"; -/* Class = "NSMenuItem"; title = "About {AppName}"; ObjectID = "58"; */ -"58.title" = "О {AppName}"; +/* Class = "NSMenuItem"; title = "About Nimble Commander"; ObjectID = "58"; */ +"58.title" = "О Nimble Commander"; /* Class = "NSMenuItem"; title = "Enter"; ObjectID = "72"; */ "72.title" = "Войти"; @@ -103,11 +103,11 @@ /* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */ "131.title" = "Службы"; -/* Class = "NSMenuItem"; title = "Hide {AppName}"; ObjectID = "134"; */ -"134.title" = "Скрыть {AppName}"; +/* Class = "NSMenuItem"; title = "Hide Nimble Commander"; ObjectID = "134"; */ +"134.title" = "Скрыть Nimble Commander"; -/* Class = "NSMenuItem"; title = "Quit {AppName}"; ObjectID = "136"; */ -"136.title" = "Завершить {AppName}"; +/* Class = "NSMenuItem"; title = "Quit Nimble Commander"; ObjectID = "136"; */ +"136.title" = "Завершить Nimble Commander"; /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */ "145.title" = "Скрыть остальные"; @@ -355,8 +355,8 @@ /* Class = "NSMenuItem"; title = "External Editor"; ObjectID = "IBo-yJ-DeD"; */ "IBo-yJ-DeD.title" = "Внешний редактор"; -/* Class = "NSMenuItem"; title = "{AppName} Help"; ObjectID = "iBP-LD-aNS"; */ -"iBP-LD-aNS.title" = "Справка {AppName}"; +/* Class = "NSMenuItem"; title = "Nimble Commander Help"; ObjectID = "iBP-LD-aNS"; */ +"iBP-LD-aNS.title" = "Справка Nimble Commander"; /* Class = "NSMenuItem"; title = "Home"; ObjectID = "iGm-8g-qdX"; */ "iGm-8g-qdX.title" = "Личное"; diff --git a/Source/NimbleCommander/NimbleCommander/Core/ActionsShortcutsManager.h b/Source/NimbleCommander/NimbleCommander/Core/ActionsShortcutsManager.h index 680f19822..c7c940f02 100644 --- a/Source/NimbleCommander/NimbleCommander/Core/ActionsShortcutsManager.h +++ b/Source/NimbleCommander/NimbleCommander/Core/ActionsShortcutsManager.h @@ -10,52 +10,77 @@ #include #include #include +#include +#include #include #include #include -#include +#include -class ActionsShortcutsManager : nc::base::ObservableBase +namespace nc::config { +class Config; +} + +namespace nc::core { + +class ActionsShortcutsManager : public nc::utility::ActionsShortcutsManager, private nc::base::ObservableBase { public: - using ShortCut = nc::utility::ActionShortcut; - struct AutoUpdatingShortCut; - class ShortCutsUpdater; + // Create a new shortcut manager which will use the provided config to store the overides. + ActionsShortcutsManager(nc::config::Config &_config); + + // Destructor. + virtual ~ActionsShortcutsManager(); + // A shared instance of a manager, it uses the GlobalConfig() as its data backend. static ActionsShortcutsManager &Instance(); - /** - * Return -1 on if tag corresponing _action wasn't found. - */ - static int TagFromAction(std::string_view _action) noexcept; - - /** - * return "" on if action corresponing _tag wasn't found. - */ - static std::string_view ActionFromTag(int _tag) noexcept; - - /** - * Return default if can't be found. - * Overrides has priority over defaults. - */ - ShortCut ShortCutFromAction(std::string_view _action) const noexcept; - - /** - * Return default if can't be found. - * Overrides has priority over defaults. - */ - ShortCut ShortCutFromTag(int _tag) const noexcept; - - /** - * Return default if can't be found. - */ - ShortCut DefaultShortCutFromTag(int _tag) const; + // Returns a numeric tag that corresponds to the given action name. + std::optional TagFromAction(std::string_view _action) const noexcept override; + + // Returns an action name of the given numeric tag. + std::optional ActionFromTag(int _tag) const noexcept override; + + // Returns a shortcut assigned to the specified action. + // Returns std::nullopt such action cannot be found. + // Overrides have priority over the default shortcuts. + std::optional ShortcutsFromAction(std::string_view _action) const noexcept override; + + // Returns a shortcut assigned to the specified numeric action tag. + // Returns std::nullopt such action cannot be found. + // Overrides have priority over the default shortcuts. + std::optional ShortcutsFromTag(int _tag) const noexcept override; + // Returns a default shortcut for an action specified by its numeric tag. + // Returns std::nullopt such action cannot be found. + std::optional DefaultShortcutsFromTag(int _tag) const noexcept override; + + // Returns an unordered list of numeric tags of actions that have the specified shortcut. + // An optional domain parameter can be specified to filter the output by only leaving actions that have the + // specified domain in their name. + std::optional ActionTagsFromShortcut(Shortcut _sc, + std::string_view _in_domain = {}) const noexcept override; + + // Syntax sugar around ActionTagsFromShortCut(_sc, _in_domain) and find_first_of(_of_tags). + // Returns the first tag from the specified set. + // The order is not defined in case of ambiguities. + std::optional FirstOfActionTagsFromShortcut(std::span _of_tags, + Shortcut _sc, + std::string_view _in_domain = {}) const noexcept override; + + // Removes any hotkeys overrides. void RevertToDefaults(); - bool SetShortCutOverride(std::string_view _action, const ShortCut &_sc); + // Sets the custom shortkey for the specified action. + // Returns true if any change was done to the actions maps. + // If the _action doesn't exist or already has the same value, returns false. + // This function is effectively a syntax sugar for SetShortCutsOverride(_action, {&_sc, 1}). + bool SetShortcutOverride(std::string_view _action, Shortcut _sc); - void SetMenuShortCuts(NSMenu *_menu) const; + // Sets the custom shortkeys for the specified action. + // Returns true if any change was done to the actions maps. + // If the _action doesn't exist or already has the same value, returns false. + bool SetShortcutsOverride(std::string_view _action, std::span _shortcuts); static std::span> AllShortcuts(); @@ -63,35 +88,50 @@ class ActionsShortcutsManager : nc::base::ObservableBase ObservationTicket ObserveChanges(std::function _callback); private: - ActionsShortcutsManager(); + // An unordered list of numeric tags indicating which actions are using a shortcut. + // An inlined vector is used to avoid memory allocating for such tiny memory blocks. + using TagsUsingShortcut = absl::InlinedVector; + ActionsShortcutsManager(const ActionsShortcutsManager &) = delete; void ReadOverrideFromConfig(); void WriteOverridesToConfig() const; - ankerl::unordered_dense::map m_ShortCutsDefaults; - ankerl::unordered_dense::map m_ShortCutsOverrides; -}; + // Clears the shortcuts usage map and builds it from the defaults and the overrides + void BuildShortcutUsageMap() noexcept; -class ActionsShortcutsManager::ShortCutsUpdater -{ -public: - struct UpdateTarget { - ShortCut *shortcut; - const char *action; - }; + // Adds the specified action tag to a list of actions that use the specified shortcut. + // The shortcut should not be empty. + void RegisterShortcutUsage(Shortcut _shortcut, int _tag) noexcept; - ShortCutsUpdater(std::span _targets); + // Removes the specified actions tag from the list of action tags that use the specified shortcut. + void UnregisterShortcutUsage(Shortcut _shortcut, int _tag) noexcept; -private: - void CheckAndUpdate() const; - std::vector> m_Targets; - ObservationTicket m_Ticket; + // Returns a container without empty shortcuts, while preserving the original relative order of the remaining items. + // Duplicates are removed as well. + static Shortcuts SanitizedShortcuts(const Shortcuts &_shortcuts) noexcept; + + // Maps an action tag to the default ordered list of its shortcuts. + ankerl::unordered_dense::map m_ShortcutsDefaults; + + // Maps an action tag to the overriden ordered list of its shortcuts. + ankerl::unordered_dense::map m_ShortcutsOverrides; + + // Maps a shortcut to an unordered list of action tags that use it. + ankerl::unordered_dense::map m_ShortcutsUsage; + + // Config instance used to read from and write to the shortcut overrides. + nc::config::Config &m_Config; }; -#define IF_MENU_TAG_TOKENPASTE(x, y) x##y -#define IF_MENU_TAG_TOKENPASTE2(x, y) IF_MENU_TAG_TOKENPASTE(x, y) -#define IF_MENU_TAG(str) \ - static const int IF_MENU_TAG_TOKENPASTE2(__tag_no_, __LINE__) = \ - ActionsShortcutsManager::Instance().TagFromAction(str); \ - if( tag == IF_MENU_TAG_TOKENPASTE2(__tag_no_, __LINE__) ) +} // namespace nc::core + +#ifdef __OBJC__ + +@interface NSMenu (ActionsShortcutsManagerSupport) + +- (void)nc_setMenuItemShortcutsWithActionsShortcutsManager:(const nc::core::ActionsShortcutsManager &)_asm; + +@end + +#endif diff --git a/Source/NimbleCommander/NimbleCommander/Core/ActionsShortcutsManager.mm b/Source/NimbleCommander/NimbleCommander/Core/ActionsShortcutsManager.mm index 382bc739e..3ca14d82d 100644 --- a/Source/NimbleCommander/NimbleCommander/Core/ActionsShortcutsManager.mm +++ b/Source/NimbleCommander/NimbleCommander/Core/ActionsShortcutsManager.mm @@ -3,9 +3,12 @@ #include #include #include +#include #include #include +namespace nc::core { + // this key should not exist in config defaults static const auto g_OverridesConfigPath = "hotkeyOverrides_v1"; @@ -448,156 +451,207 @@ static constexpr auto make_array_n(T &&value) return frozen::make_unordered_map(items); }(); -ActionsShortcutsManager::ShortCutsUpdater::ShortCutsUpdater(std::span _targets) +ActionsShortcutsManager::ActionsShortcutsManager(nc::config::Config &_config) : m_Config(_config) { - auto &am = ActionsShortcutsManager::Instance(); - m_Targets.reserve(_targets.size()); - for( auto target : _targets ) - m_Targets.emplace_back(target.shortcut, ActionsShortcutsManager::TagFromAction(target.action)); - m_Ticket = am.ObserveChanges([this] { CheckAndUpdate(); }); - - CheckAndUpdate(); -} + static_assert(sizeof(TagsUsingShortcut) == 24); -void ActionsShortcutsManager::ShortCutsUpdater::CheckAndUpdate() const -{ - auto &am = ActionsShortcutsManager::Instance(); - for( auto &i : m_Targets ) - *i.first = am.ShortCutFromTag(i.second); -} - -ActionsShortcutsManager::ActionsShortcutsManager() -{ // safety checks against malformed g_ActionsTags, only in Debug builds assert((ankerl::unordered_dense::map{std::begin(g_ActionsTags), std::end(g_ActionsTags)}) .size() == std::size(g_ActionsTags)); - for( auto &d : g_DefaultShortcuts ) { - auto i = g_ActionToTag.find(std::string_view{d.first}); - if( i != g_ActionToTag.end() ) - m_ShortCutsDefaults[i->second] = nc::utility::ActionShortcut{d.second}; + // Set up the shortcut defaults from the hardcoded map + for( auto [action, shortcut_string] : g_DefaultShortcuts ) { + if( auto it = g_ActionToTag.find(std::string_view{action}); it != g_ActionToTag.end() ) { + m_ShortcutsDefaults[it->second] = SanitizedShortcuts(Shortcuts{Shortcut{shortcut_string}}); + } } + // Set up the shortcut overrides ReadOverrideFromConfig(); -} -ActionsShortcutsManager &ActionsShortcutsManager::Instance() -{ - static ActionsShortcutsManager *manager = new ActionsShortcutsManager; - return *manager; + // Set up the shortcut usage map from the defaults and the overrides + BuildShortcutUsageMap(); } -int ActionsShortcutsManager::TagFromAction(std::string_view _action) noexcept +ActionsShortcutsManager::~ActionsShortcutsManager() = default; + +ActionsShortcutsManager &ActionsShortcutsManager::Instance() { - const auto it = g_ActionToTag.find(_action); - return it == g_ActionToTag.end() ? -1 : it->second; + [[clang::no_destroy]] static ActionsShortcutsManager manager(GlobalConfig()); + return manager; } -std::string_view ActionsShortcutsManager::ActionFromTag(int _tag) noexcept +std::optional ActionsShortcutsManager::TagFromAction(std::string_view _action) const noexcept { - const auto it = g_TagToAction.find(_tag); - return it == g_TagToAction.end() ? "" : std::string_view{it->second.data(), it->second.size()}; + if( const auto it = g_ActionToTag.find(_action); it != g_ActionToTag.end() ) + return it->second; + return std::nullopt; } -void ActionsShortcutsManager::SetMenuShortCuts(NSMenu *_menu) const +std::optional ActionsShortcutsManager::ActionFromTag(int _tag) const noexcept { - NSArray *const array = _menu.itemArray; - for( NSMenuItem *i : array ) { - if( i.submenu != nil ) { - SetMenuShortCuts(i.submenu); - } - else { - const int tag = static_cast(i.tag); - auto scover = m_ShortCutsOverrides.find(tag); - if( scover != m_ShortCutsOverrides.end() ) { - [i nc_setKeyEquivalentWithShortcut:scover->second]; - } - else { - auto sc = m_ShortCutsDefaults.find(tag); - if( sc != m_ShortCutsDefaults.end() ) { - [i nc_setKeyEquivalentWithShortcut:sc->second]; - } - else if( g_TagToAction.find(tag) != g_TagToAction.end() ) { - [i nc_setKeyEquivalentWithShortcut:nc::utility::ActionShortcut{}]; - } - } - } - } + if( const auto it = g_TagToAction.find(_tag); it != g_TagToAction.end() ) + return std::string_view{it->second.data(), it->second.size()}; + return std::nullopt; } void ActionsShortcutsManager::ReadOverrideFromConfig() { using namespace rapidjson; - auto v = GlobalConfig().Get(g_OverridesConfigPath); + auto v = m_Config.Get(g_OverridesConfigPath); if( v.GetType() != kObjectType ) return; - m_ShortCutsOverrides.clear(); - for( auto i = v.MemberBegin(), e = v.MemberEnd(); i != e; ++i ) - if( i->name.GetType() == kStringType && i->value.GetType() == kStringType ) { - auto att = g_ActionToTag.find(std::string_view{i->name.GetString()}); - if( att != g_ActionToTag.end() ) - m_ShortCutsOverrides[att->second] = nc::utility::ActionShortcut{i->value.GetString()}; + m_ShortcutsOverrides.clear(); + for( auto it = v.MemberBegin(), e = v.MemberEnd(); it != e; ++it ) { + if( it->name.GetType() != kStringType ) + continue; + + const auto att = g_ActionToTag.find(std::string_view{it->name.GetString()}); + if( att == g_ActionToTag.end() ) + continue; + + if( it->value.GetType() == kStringType ) { + m_ShortcutsOverrides[att->second] = SanitizedShortcuts(Shortcuts{Shortcut{it->value.GetString()}}); } + if( it->value.GetType() == kArrayType ) { + Shortcuts shortcuts; + const unsigned shortcuts_size = it->value.Size(); + for( unsigned idx = 0; idx < shortcuts_size; ++idx ) { + const auto &shortcut = it->value[idx]; + if( shortcut.IsString() ) + shortcuts.push_back(Shortcut{shortcut.GetString()}); + } + m_ShortcutsOverrides[att->second] = SanitizedShortcuts(shortcuts); + } + } } -ActionsShortcutsManager::ShortCut ActionsShortcutsManager::ShortCutFromAction(std::string_view _action) const noexcept +std::optional +ActionsShortcutsManager::ShortcutsFromAction(std::string_view _action) const noexcept { - const int tag = TagFromAction(_action); - if( tag <= 0 ) + const std::optional tag = TagFromAction(_action); + if( !tag ) return {}; - return ShortCutFromTag(tag); + return ShortcutsFromTag(*tag); } -ActionsShortcutsManager::ShortCut ActionsShortcutsManager::ShortCutFromTag(int _tag) const noexcept +std::optional ActionsShortcutsManager::ShortcutsFromTag(int _tag) const noexcept { - auto sc_override = m_ShortCutsOverrides.find(_tag); - if( sc_override != m_ShortCutsOverrides.end() ) + if( auto sc_override = m_ShortcutsOverrides.find(_tag); sc_override != m_ShortcutsOverrides.end() ) { return sc_override->second; + } - auto sc_default = m_ShortCutsDefaults.find(_tag); - if( sc_default != m_ShortCutsDefaults.end() ) + if( auto sc_default = m_ShortcutsDefaults.find(_tag); sc_default != m_ShortcutsDefaults.end() ) { return sc_default->second; + } return {}; } -ActionsShortcutsManager::ShortCut ActionsShortcutsManager::DefaultShortCutFromTag(int _tag) const +std::optional +ActionsShortcutsManager::DefaultShortcutsFromTag(int _tag) const noexcept { - auto sc_default = m_ShortCutsDefaults.find(_tag); - if( sc_default != m_ShortCutsDefaults.end() ) + if( auto sc_default = m_ShortcutsDefaults.find(_tag); sc_default != m_ShortcutsDefaults.end() ) { return sc_default->second; - + } return {}; } -bool ActionsShortcutsManager::SetShortCutOverride(const std::string_view _action, const ShortCut &_sc) +std::optional +ActionsShortcutsManager::ActionTagsFromShortcut(const Shortcut _sc, const std::string_view _in_domain) const noexcept +{ + auto it = m_ShortcutsUsage.find(_sc); + if( it == m_ShortcutsUsage.end() ) + return std::nullopt; // this shortcut is not used at all + + ActionTags tags = it->second; + if( !_in_domain.empty() ) { + // need to filter the tag depending to their domain, aka action name prefix + auto not_in_domain = [&](const int tag) { + return !ActionFromTag(tag).value_or(std::string_view{}).starts_with(_in_domain); + }; + + auto to_erase = std::ranges::remove_if(tags, not_in_domain); + tags.erase(to_erase.begin(), to_erase.end()); + } + + if( tags.empty() ) + return std::nullopt; + + return tags; +} + +std::optional +ActionsShortcutsManager::FirstOfActionTagsFromShortcut(std::span _of_tags, + const Shortcut _sc, + const std::string_view _in_domain) const noexcept +{ + if( const auto tags = ActionTagsFromShortcut(_sc, _in_domain) ) { + if( auto it = std::ranges::find_first_of(*tags, _of_tags); it != tags->end() ) + return *it; + } + return std::nullopt; +} + +bool ActionsShortcutsManager::SetShortcutOverride(const std::string_view _action, const Shortcut _sc) +{ + return SetShortcutsOverride(_action, std::span{&_sc, 1}); +} + +bool ActionsShortcutsManager::SetShortcutsOverride(std::string_view _action, std::span _shortcuts) { - const auto tag = TagFromAction(_action); - if( tag <= 0 ) + const std::optional tag = TagFromAction(_action); + if( !tag ) return false; - if( m_ShortCutsDefaults[tag] == _sc ) { - // hotkey is same as the default one - if( m_ShortCutsOverrides.count(tag) ) { - // if something was written as override - erase it - m_ShortCutsOverrides.erase(tag); + const auto default_it = m_ShortcutsDefaults.find(*tag); + if( default_it == m_ShortcutsDefaults.end() ) + return false; // this should never happen - // immediately write to config file - WriteOverridesToConfig(); - FireObservers(); - return true; + const Shortcuts new_shortcuts = SanitizedShortcuts(Shortcuts(_shortcuts.begin(), _shortcuts.end())); + + const auto override_it = m_ShortcutsOverrides.find(*tag); + if( std::ranges::equal(default_it->second, new_shortcuts) ) { + // The shortcut is same as the default one for this action + + if( override_it == m_ShortcutsOverrides.end() ) { + // The shortcut of this action was previously overriden - nothing to do + return false; } - return false; + + // Unregister the usage of the override shortcuts + for( const Shortcut &shortcut : override_it->second ) + UnregisterShortcutUsage(shortcut, *tag); + + // Register the usage of the default shortcuts + for( const Shortcut &shortcut : default_it->second ) + RegisterShortcutUsage(shortcut, *tag); + + // Remove the override + m_ShortcutsOverrides.erase(*tag); } + else { + // The shortcut is not the same as the default for this action + + if( override_it != m_ShortcutsOverrides.end() && override_it->second == new_shortcuts ) { + return false; // Nothing new, it's the same as currently defined in the overrides + } + + if( override_it != m_ShortcutsOverrides.end() ) { + // Unregister the usage of the override shortcuts + for( const Shortcut &shortcut : override_it->second ) + UnregisterShortcutUsage(shortcut, *tag); + } - const auto current_override = m_ShortCutsOverrides.find(tag); - if( current_override != end(m_ShortCutsOverrides) ) - if( current_override->second == _sc ) - return false; // nothing new, it's the same as currently in overrides + // Register the usage of the new override shortcuts + for( const Shortcut &shortcut : new_shortcuts ) + RegisterShortcutUsage(shortcut, *tag); - m_ShortCutsOverrides[tag] = _sc; + // Set the override + m_ShortcutsOverrides[*tag] = new_shortcuts; + } // immediately write to config file WriteOverridesToConfig(); @@ -607,7 +661,7 @@ static constexpr auto make_array_n(T &&value) void ActionsShortcutsManager::RevertToDefaults() { - m_ShortCutsOverrides.clear(); + m_ShortcutsOverrides.clear(); WriteOverridesToConfig(); FireObservers(); } @@ -618,14 +672,26 @@ static constexpr auto make_array_n(T &&value) nc::config::Value overrides{kObjectType}; for( auto &i : g_ActionsTags ) { - auto scover = m_ShortCutsOverrides.find(i.second); - if( scover != end(m_ShortCutsOverrides) ) + auto scover = m_ShortcutsOverrides.find(i.second); + if( scover == m_ShortcutsOverrides.end() ) { + continue; + } + if( scover->second.size() < 2 ) { + const std::string shortcut = scover->second.empty() ? std::string{} : scover->second.front().ToPersString(); overrides.AddMember(nc::config::MakeStandaloneString(i.first), - nc::config::MakeStandaloneString(scover->second.ToPersString()), + nc::config::MakeStandaloneString(shortcut), nc::config::g_CrtAllocator); + } + else { + nc::config::Value shortcuts{kArrayType}; + for( const Shortcut &sc : scover->second ) { + shortcuts.PushBack(nc::config::MakeStandaloneString(sc.ToPersString()), nc::config::g_CrtAllocator); + } + overrides.AddMember(nc::config::MakeStandaloneString(i.first), shortcuts, nc::config::g_CrtAllocator); + } } - GlobalConfig().Set(g_OverridesConfigPath, overrides); + m_Config.Set(g_OverridesConfigPath, overrides); } std::span> ActionsShortcutsManager::AllShortcuts() @@ -637,3 +703,98 @@ static constexpr auto make_array_n(T &&value) { return ObservableBase::AddObserver(_callback); } + +void ActionsShortcutsManager::RegisterShortcutUsage(const Shortcut _shortcut, const int _tag) noexcept +{ + assert(static_cast(_shortcut)); // only non-empty shortcuts should be registered + if( !static_cast(_shortcut) ) + return; + + if( auto it = m_ShortcutsUsage.find(_shortcut); it == m_ShortcutsUsage.end() ) { + // this shortcut wasn't used before + m_ShortcutsUsage[_shortcut].push_back(_tag); + } + else { + // this shortcut was already used. Add the tag only if it's not already there - preserve uniqueness + if( std::ranges::find(it->second, _tag) == it->second.end() ) { + it->second.push_back(_tag); + } + } +} + +void ActionsShortcutsManager::UnregisterShortcutUsage(Shortcut _shortcut, int _tag) noexcept +{ + if( auto it = m_ShortcutsUsage.find(_shortcut); it != m_ShortcutsUsage.end() ) { + auto &tags = it->second; + + auto to_erase = std::ranges::remove(tags, _tag); + tags.erase(to_erase.begin(), to_erase.end()); + + if( tags.empty() ) { + // No need to keep an empty record in the usage map + m_ShortcutsUsage.erase(it); + } + } +} + +void ActionsShortcutsManager::BuildShortcutUsageMap() noexcept +{ + m_ShortcutsUsage.clear(); // build the map means starting from scratch + + for( const auto &[tag, default_shortcuts] : m_ShortcutsDefaults ) { + if( const auto it = m_ShortcutsOverrides.find(tag); it != m_ShortcutsOverrides.end() ) { + for( const Shortcut &shortcut : it->second ) { + if( shortcut ) + RegisterShortcutUsage(shortcut, tag); + } + } + else { + for( const Shortcut &shortcut : default_shortcuts ) { + if( shortcut ) + RegisterShortcutUsage(shortcut, tag); + } + } + } +} + +ActionsShortcutsManager::Shortcuts ActionsShortcutsManager::SanitizedShortcuts(const Shortcuts &_shortcuts) noexcept +{ + Shortcuts shortcuts = _shortcuts; + + // Remove any empty shortcuts. + { + auto to_erase = std::ranges::remove_if(shortcuts, [](const Shortcut &_sc) { return _sc == Shortcut{}; }); + shortcuts.erase(to_erase.begin(), to_erase.end()); + } + + // Remove any duplicates. + // Technically speaking this is O(N^2), but N is normally ~= 1, so it doesn't matter. + for( auto it = shortcuts.begin(); it != shortcuts.end(); ++it ) { + shortcuts.erase(std::remove_if(std::next(it), shortcuts.end(), [&](const Shortcut &_sc) { return _sc == *it; }), + shortcuts.end()); + } + + return shortcuts; +} + +} // namespace nc::core + +@implementation NSMenu (ActionsShortcutsManagerSupport) + +- (void)nc_setMenuItemShortcutsWithActionsShortcutsManager:(const nc::core::ActionsShortcutsManager &)_asm +{ + NSArray *const array = self.itemArray; + for( NSMenuItem *i : array ) { + if( i.submenu != nil ) { + [i.submenu nc_setMenuItemShortcutsWithActionsShortcutsManager:_asm]; + continue; + } + + const int tag = static_cast(i.tag); + if( const auto shortcuts = _asm.ShortcutsFromTag(tag) ) { + [i nc_setKeyEquivalentWithShortcut:shortcuts->empty() ? nc::utility::ActionShortcut{} : shortcuts->front()]; + } + } +} + +@end diff --git a/Source/NimbleCommander/NimbleCommander/Preferences/Base.lproj/PreferencesWindowHotkeysTab.xib b/Source/NimbleCommander/NimbleCommander/Preferences/Base.lproj/PreferencesWindowHotkeysTab.xib index 42fffe3e7..26a97b6d8 100644 --- a/Source/NimbleCommander/NimbleCommander/Preferences/Base.lproj/PreferencesWindowHotkeysTab.xib +++ b/Source/NimbleCommander/NimbleCommander/Preferences/Base.lproj/PreferencesWindowHotkeysTab.xib @@ -1,8 +1,8 @@ - + - + @@ -10,10 +10,14 @@ + + + + @@ -25,9 +29,6 @@