// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. #include "pch.h" #include "AllShortcutActions.h" #include "ActionMap.h" #include "AllShortcutActions.h" #include "ActionMap.g.cpp" using namespace winrt::Microsoft::Terminal::Settings::Model; using namespace winrt::Microsoft::Terminal::Control; namespace winrt::Microsoft::Terminal::Settings::Model::implementation { static InternalActionID Hash(const Model::ActionAndArgs& actionAndArgs) { size_t hashedAction{ HashUtils::HashProperty(actionAndArgs.Action()) }; size_t hashedArgs{}; if (const auto& args{ actionAndArgs.Args() }) { // Args are defined, so hash them hashedArgs = gsl::narrow_cast(args.Hash()); } else { // Args are not defined. // Check if the ShortcutAction supports args. switch (actionAndArgs.Action()) { #define ON_ALL_ACTIONS_WITH_ARGS(action) \ case ShortcutAction::action: \ /* If it does, hash the default values for the args.*/ \ hashedArgs = EmptyHash(); \ break; ALL_SHORTCUT_ACTIONS_WITH_ARGS #undef ON_ALL_ACTIONS_WITH_ARGS default: { // Otherwise, hash nullptr. std::hash argsHash; hashedArgs = argsHash(nullptr); } } } return hashedAction ^ hashedArgs; } // Method Description: // - Retrieves the Command in the current layer, if it's valid // - We internally store invalid commands as full commands. // This helper function returns nullptr when we get an invalid // command. This allows us to simply check for null when we // want a valid command. // Arguments: // - actionID: the internal ID associated with a Command // Return Value: // - If the command is valid, the command itself. // - If the command is explicitly unbound, nullptr. // - If the command cannot be found in this layer, nullopt. std::optional ActionMap::_GetActionByID(const InternalActionID actionID) const { // Check the masking actions const auto maskingPair{ _MaskingActions.find(actionID) }; if (maskingPair != _MaskingActions.end()) { // ActionMap should never point to nullptr FAIL_FAST_IF_NULL(maskingPair->second); // masking actions cannot contain nested or invalid commands, // so we can just return it directly. return maskingPair->second; } // Check current layer const auto actionMapPair{ _ActionMap.find(actionID) }; if (actionMapPair != _ActionMap.end()) { auto& cmd{ actionMapPair->second }; // ActionMap should never point to nullptr FAIL_FAST_IF_NULL(cmd); return !cmd.HasNestedCommands() && cmd.ActionAndArgs().Action() == ShortcutAction::Invalid ? nullptr : // explicitly unbound cmd; } // We don't have an answer return std::nullopt; } static void RegisterShortcutAction(ShortcutAction shortcutAction, std::unordered_map& list, std::unordered_set& visited) { const auto actionAndArgs{ make_self(shortcutAction) }; if (actionAndArgs->Action() != ShortcutAction::Invalid) { /*We have a valid action.*/ /*Check if the action was already added.*/ if (visited.find(Hash(*actionAndArgs)) == visited.end()) { /*This is an action that wasn't added!*/ /*Let's add it.*/ const auto name{ actionAndArgs->GenerateName() }; list.insert({ name, *actionAndArgs }); } } } // Method Description: // - Retrieves a map of actions that can be bound to a key Windows::Foundation::Collections::IMapView ActionMap::AvailableActions() { if (!_AvailableActionsCache) { // populate _AvailableActionsCache std::unordered_map availableActions; std::unordered_set visitedActionIDs; _PopulateAvailableActionsWithStandardCommands(availableActions, visitedActionIDs); // now add any ShortcutActions that we might have missed #define ON_ALL_ACTIONS(action) RegisterShortcutAction(ShortcutAction::action, availableActions, visitedActionIDs); ALL_SHORTCUT_ACTIONS #undef ON_ALL_ACTIONS _AvailableActionsCache = single_threaded_map(std::move(availableActions)); } return _AvailableActionsCache.GetView(); } void ActionMap::_PopulateAvailableActionsWithStandardCommands(std::unordered_map& availableActions, std::unordered_set& visitedActionIDs) const { // Update AvailableActions and visitedActionIDs with our current layer for (const auto& [actionID, cmd] : _ActionMap) { if (cmd.ActionAndArgs().Action() != ShortcutAction::Invalid) { // Only populate AvailableActions with actions that haven't been visited already. if (visitedActionIDs.find(actionID) == visitedActionIDs.end()) { const auto& name{ cmd.Name() }; if (!name.empty()) { // Update AvailableActions. const auto actionAndArgsImpl{ get_self(cmd.ActionAndArgs()) }; availableActions.insert_or_assign(name, *actionAndArgsImpl->Copy()); } // Record that we already handled adding this action to the NameMap. visitedActionIDs.insert(actionID); } } } // Update NameMap and visitedActionIDs with our parents for (const auto& parent : _parents) { parent->_PopulateAvailableActionsWithStandardCommands(availableActions, visitedActionIDs); } } // Method Description: // - Retrieves a map of command names to the commands themselves // - These commands should not be modified directly because they may result in // an invalid state for the `ActionMap` Windows::Foundation::Collections::IMapView ActionMap::NameMap() { if (!_NameMapCache) { // populate _NameMapCache std::unordered_map nameMap{}; _PopulateNameMapWithSpecialCommands(nameMap); _PopulateNameMapWithStandardCommands(nameMap); _NameMapCache = single_threaded_map(std::move(nameMap)); } return _NameMapCache.GetView(); } // Method Description: // - Populates the provided nameMap with all of our special commands and our parent's special commands. // - Special commands include nested and iterable commands. // - Performs a top-down approach by going to the root first, then recursively adding the nested commands layer-by-layer. // Arguments: // - nameMap: the nameMap we're populating. This maps the name (hstring) of a command to the command itself. void ActionMap::_PopulateNameMapWithSpecialCommands(std::unordered_map& nameMap) const { // Update NameMap with our parents. // Starting with this means we're doing a top-down approach. assert(_parents.size() <= 1); for (const auto& parent : _parents) { parent->_PopulateNameMapWithSpecialCommands(nameMap); } // Add NestedCommands to NameMap _after_ we handle our parents. // This allows us to override whatever our parents tell us. for (const auto& [name, cmd] : _NestedCommands) { if (cmd.HasNestedCommands()) { // add a valid cmd nameMap.insert_or_assign(name, cmd); } else { // remove the invalid cmd nameMap.erase(name); } } // Add IterableCommands to NameMap for (const auto& cmd : _IterableCommands) { nameMap.insert_or_assign(cmd.Name(), cmd); } } // Method Description: // - Populates the provided nameMap with all of our actions and our parents actions // while omitting the actions that were already added before // Arguments: // - nameMap: the nameMap we're populating. This maps the name (hstring) of a command to the command itself. // There should only ever by one of each command (identified by the actionID) in the nameMap. void ActionMap::_PopulateNameMapWithStandardCommands(std::unordered_map& nameMap) const { std::unordered_set visitedActionIDs; for (const auto& cmd : _GetCumulativeActions()) { // only populate with valid commands if (cmd.ActionAndArgs().Action() != ShortcutAction::Invalid) { // Only populate NameMap with actions that haven't been visited already. const auto actionID{ Hash(cmd.ActionAndArgs()) }; if (visitedActionIDs.find(actionID) == visitedActionIDs.end()) { const auto& name{ cmd.Name() }; if (!name.empty()) { // Update NameMap. nameMap.insert_or_assign(name, cmd); } // Record that we already handled adding this action to the NameMap. visitedActionIDs.emplace(actionID); } } } } // Method Description: // - Provides an accumulated list of actions that are exposed. The accumulated list includes actions added in this layer, followed by actions added by our parents. std::vector ActionMap::_GetCumulativeActions() const noexcept { // First, add actions from our current layer std::vector cumulativeActions; cumulativeActions.reserve(_MaskingActions.size() + _ActionMap.size()); // masking actions have priority. Actions here are constructed from consolidating an inherited action with changes we've found when populating this layer. std::transform(_MaskingActions.begin(), _MaskingActions.end(), std::back_inserter(cumulativeActions), [](std::pair actionPair) { return actionPair.second; }); std::transform(_ActionMap.begin(), _ActionMap.end(), std::back_inserter(cumulativeActions), [](std::pair actionPair) { return actionPair.second; }); // Now, add the accumulated actions from our parents assert(_parents.size() <= 1); for (const auto& parent : _parents) { const auto parentActions{ parent->_GetCumulativeActions() }; cumulativeActions.reserve(cumulativeActions.size() + parentActions.size()); cumulativeActions.insert(cumulativeActions.end(), parentActions.begin(), parentActions.end()); } return cumulativeActions; } Windows::Foundation::Collections::IMapView ActionMap::GlobalHotkeys() { if (!_GlobalHotkeysCache) { std::unordered_map globalHotkeys; for (const auto& [keys, cmd] : KeyBindings()) { // Only populate GlobalHotkeys with actions whose // ShortcutAction is GlobalSummon or QuakeMode if (cmd.ActionAndArgs().Action() == ShortcutAction::GlobalSummon || cmd.ActionAndArgs().Action() == ShortcutAction::QuakeMode) { globalHotkeys.emplace(keys, cmd); } } _GlobalHotkeysCache = single_threaded_map(std::move(globalHotkeys)); } return _GlobalHotkeysCache.GetView(); } Windows::Foundation::Collections::IMapView ActionMap::KeyBindings() { if (!_KeyBindingMapCache) { // populate _KeyBindingMapCache std::unordered_map keyBindingsMap; std::unordered_set unboundKeys; _PopulateKeyBindingMapWithStandardCommands(keyBindingsMap, unboundKeys); _KeyBindingMapCache = single_threaded_map(std::move(keyBindingsMap)); } return _KeyBindingMapCache.GetView(); } // Method Description: // - Populates the provided keyBindingsMap with all of our actions and our parents actions // while omitting the key bindings that were already added before. // - This needs to be a bottom up approach to ensure that we only add each key chord once. // Arguments: // - keyBindingsMap: the keyBindingsMap we're populating. This maps the key chord of a command to the command itself. // - unboundKeys: a set of keys that are explicitly unbound void ActionMap::_PopulateKeyBindingMapWithStandardCommands(std::unordered_map& keyBindingsMap, std::unordered_set& unboundKeys) const { // Update KeyBindingsMap with our current layer for (const auto& [keys, actionID] : _KeyMap) { // Get the action our KeyMap maps to. // This _cannot_ be nullopt because KeyMap can only map to // actions in this layer. // This _can_ be nullptr because nullptr means it was // explicitly unbound ( "command": "unbound", "keys": "ctrl+c" ). const auto cmd{ _GetActionByID(actionID).value() }; if (cmd) { // iterate over all of the action's bound keys const auto cmdImpl{ get_self(cmd) }; for (const auto& keys : cmdImpl->KeyMappings()) { // Only populate KeyBindingsMap with actions that... // (1) haven't been visited already // (2) aren't explicitly unbound if (keyBindingsMap.find(keys) == keyBindingsMap.end() && unboundKeys.find(keys) == unboundKeys.end()) { keyBindingsMap.emplace(keys, cmd); } } } else { // record any keys that are explicitly unbound, // but don't add them to the list of key bindings unboundKeys.emplace(keys); } } // Update keyBindingsMap and unboundKeys with our parents assert(_parents.size() <= 1); for (const auto& parent : _parents) { parent->_PopulateKeyBindingMapWithStandardCommands(keyBindingsMap, unboundKeys); } } com_ptr ActionMap::Copy() const { auto actionMap{ make_self() }; // KeyChord --> ID actionMap->_KeyMap = _KeyMap; // ID --> Command actionMap->_ActionMap.reserve(_ActionMap.size()); for (const auto& [actionID, cmd] : _ActionMap) { actionMap->_ActionMap.emplace(actionID, *winrt::get_self(cmd)->Copy()); } // ID --> Command actionMap->_MaskingActions.reserve(_MaskingActions.size()); for (const auto& [actionID, cmd] : _MaskingActions) { actionMap->_MaskingActions.emplace(actionID, *winrt::get_self(cmd)->Copy()); } // Name --> Command actionMap->_NestedCommands.reserve(_NestedCommands.size()); for (const auto& [name, cmd] : _NestedCommands) { actionMap->_NestedCommands.emplace(name, *winrt::get_self(cmd)->Copy()); } actionMap->_IterableCommands.reserve(_IterableCommands.size()); for (const auto& cmd : _IterableCommands) { actionMap->_IterableCommands.emplace_back(*winrt::get_self(cmd)->Copy()); } assert(_parents.size() <= 1); actionMap->_parents.reserve(_parents.size()); for (const auto& parent : _parents) { actionMap->_parents.emplace_back(parent->Copy()); } return actionMap; } // Method Description: // - Adds a command to the ActionMap // Arguments: // - cmd: the command we're adding void ActionMap::AddAction(const Model::Command& cmd) { // _Never_ add null to the ActionMap if (!cmd) { return; } // invalidate caches _NameMapCache = nullptr; _GlobalHotkeysCache = nullptr; _KeyBindingMapCache = nullptr; // Handle nested commands const auto cmdImpl{ get_self(cmd) }; if (cmdImpl->IsNestedCommand()) { // But check if it actually has a name to bind to first const auto name{ cmd.Name() }; if (!name.empty()) { _NestedCommands.emplace(name, cmd); } return; } // Handle iterable commands if (cmdImpl->IterateOn() != ExpandCommandType::None) { _IterableCommands.emplace_back(cmd); return; } // General Case: // Add the new command to the KeyMap. // This map directs you to an entry in the ActionMap. // Removing Actions from the Command Palette: // cmd.Name and cmd.Action have a one-to-one relationship. // If cmd.Name is empty, we must retrieve the old name and remove it. // Removing Key Bindings: // cmd.Keys and cmd.Action have a many-to-one relationship. // If cmd.Keys is empty, we don't care. // If action is "unbound"/"invalid", you're explicitly unbinding the provided cmd.keys. // NOTE: If we're unbinding a command from a different layer, we must use maskingActions // to keep track of what key mappings are still valid. // _TryUpdateActionMap may update oldCmd and maskingCmd Model::Command oldCmd{ nullptr }; Model::Command maskingCmd{ nullptr }; _TryUpdateActionMap(cmd, oldCmd, maskingCmd); _TryUpdateName(cmd, oldCmd, maskingCmd); _TryUpdateKeyChord(cmd, oldCmd, maskingCmd); } // Method Description: // - Try to add the new command to _ActionMap. // - If the command was added previously in this layer, populate oldCmd. // - If the command was added previously in another layer, populate maskingCmd. // Arguments: // - cmd: the action we're trying to register // - oldCmd: the action found in _ActionMap, if one already exists // - maskingAction: the action found in a parent layer, if one already exists void ActionMap::_TryUpdateActionMap(const Model::Command& cmd, Model::Command& oldCmd, Model::Command& maskingCmd) { // Example: // { "command": "copy", "keys": "ctrl+c" } --> add the action in for the first time // { "command": "copy", "keys": "ctrl+shift+c" } --> update oldCmd const auto actionID{ Hash(cmd.ActionAndArgs()) }; const auto& actionPair{ _ActionMap.find(actionID) }; if (actionPair == _ActionMap.end()) { // add this action in for the first time _ActionMap.emplace(actionID, cmd); } else { // We're adding an action that already exists in our layer. // Record it so that we update it with any new information. oldCmd = actionPair->second; } // Masking Actions // // Example: // parent: { "command": "copy", "keys": "ctrl+c" } --> add the action to parent._ActionMap // current: { "command": "copy", "keys": "ctrl+shift+c" } --> look through parents for the "ctrl+c" binding, add it to _MaskingActions // { "command": "copy", "keys": "ctrl+ins" } --> this should already be in _MaskingActions // Now check if this action was introduced in another layer. const auto& maskingActionPair{ _MaskingActions.find(actionID) }; if (maskingActionPair == _MaskingActions.end()) { // Check if we need to add this to our list of masking commands. FAIL_FAST_IF(_parents.size() > 1); for (const auto& parent : _parents) { // NOTE: This only checks the layer above us, but that's ok. // If we had to find one from a layer above that, parent->_MaskingActions // would have found it, so we inherit it for free! const auto& inheritedCmd{ parent->_GetActionByID(actionID) }; if (inheritedCmd && *inheritedCmd) { const auto& inheritedCmdImpl{ get_self(*inheritedCmd) }; maskingCmd = *inheritedCmdImpl->Copy(); _MaskingActions.emplace(actionID, maskingCmd); } } } else { // This is an action that we already have a mutable "masking" record for. // Record it so that we update it with any new information. maskingCmd = maskingActionPair->second; } } // Method Description: // - Update our internal state with the name of the newly registered action // Arguments: // - cmd: the action we're trying to register // - oldCmd: the action that already exists in our internal state. May be null. // - maskingCmd: the masking action that already exists in our internal state. May be null. void ActionMap::_TryUpdateName(const Model::Command& cmd, const Model::Command& oldCmd, const Model::Command& maskingCmd) { // Example: // { "name": "foo", "command": "copy" } --> we are setting a name, update oldCmd and maskingCmd // { "command": "copy" } --> no change to name, exit early const auto cmdImpl{ get_self(cmd) }; if (!cmdImpl->HasName()) { // the user is not trying to update the name. return; } // Update oldCmd: // If we have a Command in our _ActionMap that we're trying to update, // update it. const auto newName{ cmd.Name() }; if (oldCmd) { // This command has a name, check if it's new. if (newName != oldCmd.Name()) { // The new name differs from the old name, // update our name. auto oldCmdImpl{ get_self(oldCmd) }; oldCmdImpl->Name(newName); } } // Update maskingCmd: // We have a Command that is masking one from a parent layer. // We need to ensure that this has the correct name. That way, // we can return an accumulated view of a Command at this layer. // This differs from oldCmd which is mainly used for serialization // by recording the delta of the Command in this layer. if (maskingCmd) { // This command has a name, check if it's new. if (newName != maskingCmd.Name()) { // The new name differs from the old name, // update our name. auto maskingCmdImpl{ get_self(maskingCmd) }; maskingCmdImpl->Name(newName); } } // Handle a collision with NestedCommands _NestedCommands.erase(newName); } // Method Description: // - Update our internal state with the key chord of the newly registered action // Arguments: // - cmd: the action we're trying to register // - oldCmd: the action that already exists in our internal state. May be null. // - maskingCmd: the masking action that already exists in our internal state. May be null. void ActionMap::_TryUpdateKeyChord(const Model::Command& cmd, const Model::Command& oldCmd, const Model::Command& maskingCmd) { // Example: // { "command": "copy", "keys": "ctrl+c" } --> we are registering a new key chord, update oldCmd and maskingCmd // { "name": "foo", "command": "copy" } --> no change to keys, exit early const auto keys{ cmd.Keys() }; if (!keys) { // the user is not trying to update the keys. return; } // Handle collisions const auto oldKeyPair{ _KeyMap.find(keys) }; if (oldKeyPair != _KeyMap.end()) { // Collision: The key chord was already in use. // // Example: // { "command": "copy", "keys": "ctrl+c" } --> register "ctrl+c" (different branch) // { "command": "paste", "keys": "ctrl+c" } --> Collision! (this branch) // // Remove the old one. (unbind "copy" in the example above) const auto actionPair{ _ActionMap.find(oldKeyPair->second) }; const auto conflictingCmd{ actionPair->second }; const auto conflictingCmdImpl{ get_self(conflictingCmd) }; conflictingCmdImpl->EraseKey(keys); } else if (const auto& conflictingCmd{ GetActionByKeyChord(keys) }) { // Collision with ancestor: The key chord was already in use, but by an action in another layer // // Example: // parent: { "command": "copy", "keys": "ctrl+c" } --> register "ctrl+c" (different branch) // current: { "command": "paste", "keys": "ctrl+c" } --> Collision with ancestor! (this branch, sub-branch 1) // { "command": "unbound", "keys": "ctrl+c" } --> Collision with masking action! (this branch, sub-branch 2) const auto conflictingActionID{ Hash(conflictingCmd.ActionAndArgs()) }; const auto maskingCmdPair{ _MaskingActions.find(conflictingActionID) }; if (maskingCmdPair == _MaskingActions.end()) { // This is the first time we're colliding with an action from a different layer, // so let's add this action to _MaskingActions and update it appropriately. // Create a copy of the conflicting action, // and erase the conflicting key chord from the copy. const auto conflictingCmdImpl{ get_self(conflictingCmd) }; const auto conflictingCmdCopy{ conflictingCmdImpl->Copy() }; conflictingCmdCopy->EraseKey(keys); _MaskingActions.emplace(conflictingActionID, *conflictingCmdCopy); } else { // We've collided with this action before. Let's resolve a collision with a masking action. const auto maskingCmdImpl{ get_self(maskingCmdPair->second) }; maskingCmdImpl->EraseKey(keys); } } // Assign the new action in the _KeyMap. const auto actionID{ Hash(cmd.ActionAndArgs()) }; _KeyMap.insert_or_assign(keys, actionID); // Additive operation: // Register the new key chord with oldCmd (an existing _ActionMap entry) // Example: // { "command": "copy", "keys": "ctrl+c" } --> register "ctrl+c" (section above) // { "command": "copy", "keys": "ctrl+shift+c" } --> also register "ctrl+shift+c" to the same Command (oldCmd) if (oldCmd) { // Update inner Command with new key chord auto oldCmdImpl{ get_self(oldCmd) }; oldCmdImpl->RegisterKey(keys); } // Additive operation: // Register the new key chord with maskingCmd (an existing _maskingAction entry) // Example: // parent: { "command": "copy", "keys": "ctrl+c" } --> register "ctrl+c" to parent._ActionMap (different branch in a different layer) // current: { "command": "copy", "keys": "ctrl+shift+c" } --> also register "ctrl+shift+c" to the same Command (maskingCmd) if (maskingCmd) { // Update inner Command with new key chord auto maskingCmdImpl{ get_self(maskingCmd) }; maskingCmdImpl->RegisterKey(keys); } } // Method Description: // - Determines whether the given key chord is explicitly unbound // Arguments: // - keys: the key chord to check // Return value: // - true if the keychord is explicitly unbound // - false if either the keychord is bound, or not bound at all bool ActionMap::IsKeyChordExplicitlyUnbound(Control::KeyChord const& keys) const { // We use the fact that the ..Internal call returns nullptr for explicitly unbound // key chords, and nullopt for keychord that are not bound - it allows us to distinguish // between unbound and lack of binding. return _GetActionByKeyChordInternal(keys) == nullptr; } // Method Description: // - Retrieves the assigned command that can be invoked with the given key chord // Arguments: // - keys: the key chord of the command to search for // Return Value: // - the command with the given key chord // - nullptr if the key chord doesn't exist Model::Command ActionMap::GetActionByKeyChord(Control::KeyChord const& keys) const { return _GetActionByKeyChordInternal(keys).value_or(nullptr); } // Method Description: // - Retrieves the assigned command with the given key chord. // - Can return nullopt to differentiate explicit unbinding vs lack of binding. // Arguments: // - keys: the key chord of the command to search for // Return Value: // - the command with the given key chord // - nullptr if the key chord is explicitly unbound // - nullopt if it was not bound in this layer std::optional ActionMap::_GetActionByKeyChordInternal(const ActionMapKeyChord keys) const { // KeyChord's constructor ensures that Modifiers() & Vkey() is a valid value at a minimum. // This allows ActionMap to identify KeyChords which should "layer" (overwrite) each other. // For instance win+sc(41) and win+` both specify the same KeyChord on an US keyboard layout // from the perspective of a user. Either of the two should correctly overwrite the other. // As such we need to pretend as if ScanCode doesn't exist. assert(keys.vkey != 0); // Check the current layer if (const auto actionIDPair = _KeyMap.find(keys); actionIDPair != _KeyMap.end()) { // the command was explicitly bound, // return what we found (invalid commands exposed as nullptr) return _GetActionByID(actionIDPair->second); } // the command was not bound in this layer, // ask my parents assert(_parents.size() <= 1); for (const auto& parent : _parents) { const auto& inheritedCmd{ parent->_GetActionByKeyChordInternal(keys) }; if (inheritedCmd) { return *inheritedCmd; } } // This action is not explicitly bound return std::nullopt; } // Method Description: // - Retrieves the key chord for the provided action // Arguments: // - action: the shortcut action (an action type) we're looking for // Return Value: // - the key chord that executes the given action // - nullptr if the action is not bound to a key chord Control::KeyChord ActionMap::GetKeyBindingForAction(ShortcutAction const& action) const { return GetKeyBindingForAction(action, nullptr); } // Method Description: // - Retrieves the key chord for the provided action // Arguments: // - action: the shortcut action (an action type) we're looking for // - myArgs: the action args for the action we're looking for // Return Value: // - the key chord that executes the given action // - nullptr if the action is not bound to a key chord Control::KeyChord ActionMap::GetKeyBindingForAction(ShortcutAction const& myAction, IActionArgs const& myArgs) const { if (myAction == ShortcutAction::Invalid) { return nullptr; } // Check our internal state. const auto actionAndArgs = winrt::make(myAction, myArgs); const auto hash{ Hash(actionAndArgs) }; if (const auto& cmd{ _GetActionByID(hash) }) { return cmd->Keys(); } // Check our parents assert(_parents.size() <= 1); for (const auto& parent : _parents) { if (const auto& keys{ parent->GetKeyBindingForAction(myAction, myArgs) }) { return keys; } } // This key binding does not exist return nullptr; } // Method Description: // - Rebinds a key binding to a new key chord // Arguments: // - oldKeys: the key binding that we are rebinding // - newKeys: the new key chord that is being used to replace oldKeys // Return Value: // - true, if successful. False, otherwise. bool ActionMap::RebindKeys(Control::KeyChord const& oldKeys, Control::KeyChord const& newKeys) { const auto& cmd{ GetActionByKeyChord(oldKeys) }; if (!cmd) { // oldKeys must be bound. Otherwise, we don't know what action to bind. return false; } if (newKeys) { // Bind newKeys const auto newCmd{ make_self() }; newCmd->ActionAndArgs(cmd.ActionAndArgs()); newCmd->RegisterKey(newKeys); AddAction(*newCmd); } // unbind oldKeys DeleteKeyBinding(oldKeys); return true; } // Method Description: // - Unbind a key chord // Arguments: // - keys: the key chord that is being unbound // Return Value: // - void ActionMap::DeleteKeyBinding(KeyChord const& keys) { // create an "unbound" command // { "command": "unbound", "keys": } const auto cmd{ make_self() }; cmd->ActionAndArgs(make()); cmd->RegisterKey(keys); AddAction(*cmd); } // Method Description: // - Add a new key binding // - If the key chord is already in use, the conflicting command is overwritten. // Arguments: // - keys: the key chord that is being bound // - action: the action that the keys are being bound to // Return Value: // - void ActionMap::RegisterKeyBinding(Control::KeyChord keys, Model::ActionAndArgs action) { auto cmd{ make_self() }; cmd->RegisterKey(keys); cmd->ActionAndArgs(action); AddAction(*cmd); } }