Skip to content

Commit

Permalink
Refactoring ActionShortcutsManager's API and internals to support mul…
Browse files Browse the repository at this point in the history
…tiple shortcuts per action
  • Loading branch information
mikekazakov committed Dec 26, 2024
1 parent 59f146b commit 9bd3175
Show file tree
Hide file tree
Showing 6 changed files with 247 additions and 148 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ - (NCViewerView *)makeViewerWithFrame:(NSRect)frame

- (NCViewerViewController *)makeViewerController
{
auto shortcuts = [](std::string_view _name) {
return nc::core::ActionsShortcutsManager::Instance().ShortCutFromAction(_name).value();
using nc::core::ActionsShortcutsManager;
auto shortcuts = [](std::string_view _name) -> ActionsShortcutsManager::ShortCut {
auto sc = ActionsShortcutsManager::Instance().ShortCutFromAction(_name).value();
return sc.empty() ? ActionsShortcutsManager::ShortCut{} : sc.front();
};
return [[NCViewerViewController alloc] initWithHistory:self.internalViewerHistory
config:self.globalConfig
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,15 @@ namespace nc::core {
class ActionsShortcutsManager : nc::base::ObservableBase
{
public:
// Shortcut represents a key and its modifiers that have to be pressed to trigger an action.
using ShortCut = nc::utility::ActionShortcut;

// An ordered list of shortcuts.
// The relative order of the shortcuts must be preserved as it has semantic meaning for e.g. menus.
// Empty shortcuts should not be stored in such vectors.
// An inlined vector is used to avoid memory allocating for such tiny memory blocks.
using ShortCuts = absl::InlinedVector<ShortCut, 4>;

// ActionTags represents a list of numberic action tags.
// Normally they are tiny, thus an inline vector is used to avoid memory allocation.
using ActionTags = absl::InlinedVector<int, 4>;
Expand All @@ -46,16 +53,16 @@ class ActionsShortcutsManager : nc::base::ObservableBase
// 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<ShortCut> ShortCutFromAction(std::string_view _action) const noexcept;
std::optional<ShortCuts> ShortCutFromAction(std::string_view _action) const noexcept;

// 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<ShortCut> ShortCutFromTag(int _tag) const noexcept;
std::optional<ShortCuts> ShortCutFromTag(int _tag) const noexcept;

// Returns a default shortcut for an action specified by its numeric tag.
// Returns std::nullopt such action cannot be found.
std::optional<ShortCut> DefaultShortCutFromTag(int _tag) const noexcept;
std::optional<ShortCuts> DefaultShortCutFromTag(int _tag) const noexcept;

// 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
Expand All @@ -72,10 +79,17 @@ class ActionsShortcutsManager : nc::base::ObservableBase
// Removes any hotkeys overrides.
void RevertToDefaults();

// 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);

// 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<const ShortCut> _shortcuts);

#ifdef __OBJC__
void SetMenuShortCuts(NSMenu *_menu) const;
#endif
Expand Down Expand Up @@ -105,9 +119,19 @@ class ActionsShortcutsManager : nc::base::ObservableBase
// Removes the specified actions tag from the list of action tags that use the specified shortcut.
void UnregisterShortcutUsage(ShortCut _shortcut, int _tag) noexcept;

ankerl::unordered_dense::map<int, ShortCut> m_ShortCutsDefaults;
ankerl::unordered_dense::map<int, ShortCut> m_ShortCutsOverrides;
// Returns a container without empty shortcuts, while preserving the original relative order of the remaining items.
static ShortCuts WithoutEmptyShortCuts(const ShortCuts &_shortcuts) noexcept;

// Maps an action tag to the default ordered list of its shortcuts.
ankerl::unordered_dense::map<int, ShortCuts> m_ShortCutsDefaults;

// Maps an action tag to the overriden ordered list of its shortcuts.
ankerl::unordered_dense::map<int, ShortCuts> m_ShortCutsOverrides;

// Maps a shortcut to an unordered list of action tags that use it.
ankerl::unordered_dense::map<ShortCut, TagsUsingShortCut> m_ShortCutsUsage;

// Config instance used to read from and write to the shortcut overrides.
nc::config::Config &m_Config;
};

Expand Down
130 changes: 76 additions & 54 deletions Source/NimbleCommander/NimbleCommander/Core/ActionsShortcutsManager.mm
Original file line number Diff line number Diff line change
Expand Up @@ -460,9 +460,9 @@ static constexpr auto make_array_n(T &&value)
.size() == std::size(g_ActionsTags));

// Set up the shortcut defaults from the hardcoded map
for( auto [action, shortcut] : g_DefaultShortcuts ) {
if( auto i = g_ActionToTag.find(std::string_view{action}); i != g_ActionToTag.end() ) {
m_ShortCutsDefaults[i->second] = ShortCut{shortcut};
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] = WithoutEmptyShortCuts(ShortCuts{ShortCut{shortcut_string}});
}
}

Expand Down Expand Up @@ -499,22 +499,21 @@ static constexpr auto make_array_n(T &&value)
for( NSMenuItem *i : array ) {
if( i.submenu != nil ) {
SetMenuShortCuts(i.submenu);
continue;
}
else {
const int tag = static_cast<int>(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{}];
}
}

const int tag = static_cast<int>(i.tag);
if( auto shortcut_override = m_ShortCutsOverrides.find(tag); shortcut_override != m_ShortCutsOverrides.end() ) {
const auto &shortcuts = shortcut_override->second;
[i nc_setKeyEquivalentWithShortcut:shortcuts.empty() ? ShortCut{} : shortcuts.front()];
}
else if( auto shortcut_defaults = m_ShortCutsDefaults.find(tag);
shortcut_defaults != m_ShortCutsDefaults.end() ) {
const auto &shortcuts = shortcut_defaults->second;
[i nc_setKeyEquivalentWithShortcut:shortcuts.empty() ? ShortCut{} : shortcuts.front()];
}
else if( g_TagToAction.contains(tag) ) {
[i nc_setKeyEquivalentWithShortcut:ShortCut{}];
}
}
}
Expand All @@ -528,15 +527,18 @@ static constexpr auto make_array_n(T &&value)
return;

m_ShortCutsOverrides.clear();
for( auto i = v.MemberBegin(), e = v.MemberEnd(); i != e; ++i )
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()};
if( att != g_ActionToTag.end() ) {
m_ShortCutsOverrides[att->second] = WithoutEmptyShortCuts(ShortCuts{ShortCut{i->value.GetString()}});
}
}
// TODO: support for array values when multiple shortcuts are stored
}
}

std::optional<ActionsShortcutsManager::ShortCut>
std::optional<ActionsShortcutsManager::ShortCuts>
ActionsShortcutsManager::ShortCutFromAction(std::string_view _action) const noexcept
{
const std::optional<int> tag = TagFromAction(_action);
Expand All @@ -545,26 +547,25 @@ static constexpr auto make_array_n(T &&value)
return ShortCutFromTag(*tag);
}

std::optional<ActionsShortcutsManager::ShortCut> ActionsShortcutsManager::ShortCutFromTag(int _tag) const noexcept
std::optional<ActionsShortcutsManager::ShortCuts> ActionsShortcutsManager::ShortCutFromTag(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 {};
}

std::optional<ActionsShortcutsManager::ShortCut>
std::optional<ActionsShortcutsManager::ShortCuts>
ActionsShortcutsManager::DefaultShortCutFromTag(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 {};
}

Expand Down Expand Up @@ -605,6 +606,11 @@ static constexpr auto make_array_n(T &&value)
}

bool ActionsShortcutsManager::SetShortCutOverride(const std::string_view _action, const ShortCut _sc)
{
return SetShortCutsOverride(_action, std::span<const ShortCut>{&_sc, 1});
}

bool ActionsShortcutsManager::SetShortCutsOverride(std::string_view _action, std::span<const ShortCut> _shortcuts)
{
const std::optional<int> tag = TagFromAction(_action);
if( !tag )
Expand All @@ -614,46 +620,47 @@ static constexpr auto make_array_n(T &&value)
if( default_it == m_ShortCutsDefaults.end() )
return false; // this should never happen

const auto override_it = m_ShortCutsOverrides.find(*tag);
const ShortCuts new_shortcuts = WithoutEmptyShortCuts(ShortCuts(_shortcuts.begin(), _shortcuts.end()));

if( default_it->second == _sc ) {
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;
}

// Unregister the usage of the override shortcut
UnregisterShortcutUsage(override_it->second, *tag);
// Unregister the usage of the override shortcuts
for( const ShortCut &shortcut : override_it->second )
UnregisterShortcutUsage(shortcut, *tag);

// Register the usage of the default shortcut if it's defined
if( default_it->second ) {
RegisterShortcutUsage(default_it->second, *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 == _sc ) {
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 shortcut
UnregisterShortcutUsage(override_it->second, *tag);
// Unregister the usage of the override shortcuts
for( const ShortCut &shortcut : override_it->second )
UnregisterShortcutUsage(shortcut, *tag);
}

if( _sc ) {
// Register the usage of the new override if it's defined
RegisterShortcutUsage(_sc, *tag);
}
// Register the usage of the new override shortcuts
for( const ShortCut &shortcut : new_shortcuts )
RegisterShortcutUsage(shortcut, *tag);

// Set the override
m_ShortCutsOverrides[*tag] = _sc;
m_ShortCutsOverrides[*tag] = new_shortcuts;
}

// immediately write to config file
Expand All @@ -674,12 +681,15 @@ static constexpr auto make_array_n(T &&value)
using namespace rapidjson;
nc::config::Value overrides{kObjectType};

// TODO: add support for storing multiple shortcuts
for( auto &i : g_ActionsTags ) {
auto scover = m_ShortCutsOverrides.find(i.second);
if( scover != end(m_ShortCutsOverrides) )
if( scover != m_ShortCutsOverrides.end() ) {
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);
}
}

m_Config.Set(g_OverridesConfigPath, overrides);
Expand Down Expand Up @@ -732,16 +742,28 @@ static constexpr auto make_array_n(T &&value)
{
m_ShortCutsUsage.clear(); // build the map means starting from scratch

for( const auto [tag, default_shortcut] : m_ShortCutsDefaults ) {
for( const auto [tag, default_shortcuts] : m_ShortCutsDefaults ) {
if( const auto it = m_ShortCutsOverrides.find(tag); it != m_ShortCutsOverrides.end() ) {
if( it->second )
RegisterShortcutUsage(it->second, tag);
for( const ShortCut &shortcut : it->second ) {
if( shortcut )
RegisterShortcutUsage(shortcut, tag);
}
}
else {
if( default_shortcut )
RegisterShortcutUsage(default_shortcut, tag);
for( const ShortCut &shortcut : default_shortcuts ) {
if( shortcut )
RegisterShortcutUsage(shortcut, tag);
}
}
}
}

ActionsShortcutsManager::ShortCuts ActionsShortcutsManager::WithoutEmptyShortCuts(const ShortCuts &_shortcuts) noexcept
{
ShortCuts shortcuts = _shortcuts;
auto to_erase = std::ranges::remove_if(shortcuts, [](const ShortCut &_sc) { return _sc == ShortCut{}; });
shortcuts.erase(to_erase.begin(), to_erase.end());
return shortcuts;
}

} // namespace nc::core
Loading

0 comments on commit 9bd3175

Please sign in to comment.