mirror of
https://github.com/microsoft/terminal.git
synced 2025-12-10 18:43:54 -06:00
1267 lines
55 KiB
C++
1267 lines
55 KiB
C++
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT license.
|
|
|
|
#include "pch.h"
|
|
#include "AllShortcutActions.h"
|
|
#include "ActionMap.h"
|
|
#include "Command.h"
|
|
#include <LibraryResources.h>
|
|
#include <til/io.h>
|
|
|
|
#include "ActionMap.g.cpp"
|
|
#include "ActionArgFactory.g.cpp"
|
|
|
|
using namespace winrt::Microsoft::Terminal::Settings::Model;
|
|
using namespace winrt::Microsoft::Terminal::Control;
|
|
using namespace winrt::Windows::Foundation::Collections;
|
|
|
|
namespace winrt::Microsoft::Terminal::Settings::Model::implementation
|
|
{
|
|
static InternalActionID Hash(const Model::ActionAndArgs& actionAndArgs)
|
|
{
|
|
til::hasher hasher;
|
|
|
|
// action will be hashed last.
|
|
// This allows us to first seed a til::hasher
|
|
// with the return value of IActionArgs::Hash().
|
|
const auto action = actionAndArgs.Action();
|
|
|
|
if (const auto args = actionAndArgs.Args())
|
|
{
|
|
hasher = til::hasher{ gsl::narrow_cast<size_t>(args.Hash()) };
|
|
}
|
|
else
|
|
{
|
|
size_t hash = 0;
|
|
|
|
// Args are not defined.
|
|
// Check if the ShortcutAction supports args.
|
|
switch (action)
|
|
{
|
|
#define ON_ALL_ACTIONS_WITH_ARGS(action) \
|
|
case ShortcutAction::action: \
|
|
{ \
|
|
/* If it does, hash the default values for the args. */ \
|
|
static const auto cachedHash = gsl::narrow_cast<size_t>( \
|
|
winrt::make_self<implementation::action##Args>()->Hash()); \
|
|
hash = cachedHash; \
|
|
break; \
|
|
}
|
|
ALL_SHORTCUT_ACTIONS_WITH_ARGS
|
|
INTERNAL_SHORTCUT_ACTIONS_WITH_ARGS
|
|
#undef ON_ALL_ACTIONS_WITH_ARGS
|
|
default:
|
|
break;
|
|
}
|
|
|
|
hasher = til::hasher{ hash };
|
|
}
|
|
|
|
hasher.write(action);
|
|
return hasher.finalize();
|
|
}
|
|
|
|
winrt::hstring ActionArgFactory::GetNameForAction(Model::ShortcutAction action)
|
|
{
|
|
// Use a magic static to initialize this map, because we won't be able
|
|
// to load the resources at _init_, only at runtime.
|
|
static auto actionNames = []() {
|
|
return std::unordered_map<ShortcutAction, winrt::hstring>{
|
|
{ ShortcutAction::AdjustFontSize, RS_(L"AdjustFontSizeCommandKey") },
|
|
{ ShortcutAction::CloseOtherPanes, RS_(L"CloseOtherPanesCommandKey") },
|
|
{ ShortcutAction::CloseOtherTabs, RS_(L"CloseOtherTabs") },
|
|
{ ShortcutAction::ClosePane, RS_(L"ClosePaneCommandKey") },
|
|
{ ShortcutAction::CloseTab, RS_(L"CloseTab") },
|
|
{ ShortcutAction::CloseTabsAfter, RS_(L"CloseTabsAfter") },
|
|
{ ShortcutAction::CloseWindow, RS_(L"CloseWindowCommandKey") },
|
|
{ ShortcutAction::CopyText, RS_(L"CopyTextCommandKey") },
|
|
{ ShortcutAction::DuplicateTab, RS_(L"DuplicateTabCommandKey") },
|
|
{ ShortcutAction::ExecuteCommandline, RS_(L"ExecuteCommandlineCommandKey") },
|
|
{ ShortcutAction::Find, RS_(L"FindCommandKey") },
|
|
{ ShortcutAction::MoveFocus, RS_(L"MoveFocusCommandKey") },
|
|
{ ShortcutAction::MovePane, RS_(L"MovePaneCommandKey") },
|
|
{ ShortcutAction::SwapPane, RS_(L"SwapPaneCommandKey") },
|
|
{ ShortcutAction::NewTab, RS_(L"NewTabCommandKey") },
|
|
{ ShortcutAction::NextTab, RS_(L"NextTabCommandKey") },
|
|
{ ShortcutAction::OpenNewTabDropdown, RS_(L"OpenNewTabDropdownCommandKey") },
|
|
{ ShortcutAction::OpenSettings, RS_(L"OpenSettingsUICommandKey") },
|
|
{ ShortcutAction::OpenTabColorPicker, RS_(L"OpenTabColorPickerCommandKey") },
|
|
{ ShortcutAction::PasteText, RS_(L"PasteTextCommandKey") },
|
|
{ ShortcutAction::PrevTab, RS_(L"PrevTabCommandKey") },
|
|
{ ShortcutAction::RenameTab, RS_(L"ResetTabNameCommandKey") },
|
|
{ ShortcutAction::OpenTabRenamer, RS_(L"OpenTabRenamerCommandKey") },
|
|
{ ShortcutAction::ResetFontSize, RS_(L"ResetFontSizeCommandKey") },
|
|
{ ShortcutAction::ResizePane, RS_(L"ResizePaneCommandKey") },
|
|
{ ShortcutAction::ScrollDown, RS_(L"ScrollDownCommandKey") },
|
|
{ ShortcutAction::ScrollDownPage, RS_(L"ScrollDownPageCommandKey") },
|
|
{ ShortcutAction::ScrollUp, RS_(L"ScrollUpCommandKey") },
|
|
{ ShortcutAction::ScrollUpPage, RS_(L"ScrollUpPageCommandKey") },
|
|
{ ShortcutAction::ScrollToTop, RS_(L"ScrollToTopCommandKey") },
|
|
{ ShortcutAction::ScrollToBottom, RS_(L"ScrollToBottomCommandKey") },
|
|
{ ShortcutAction::ScrollToMark, RS_(L"ScrollToPreviousMarkCommandKey") },
|
|
{ ShortcutAction::AddMark, RS_(L"AddMarkCommandKey") },
|
|
{ ShortcutAction::ClearMark, RS_(L"ClearMarkCommandKey") },
|
|
{ ShortcutAction::ClearAllMarks, RS_(L"ClearAllMarksCommandKey") },
|
|
{ ShortcutAction::SendInput, RS_(L"SendInput") },
|
|
{ ShortcutAction::SetColorScheme, RS_(L"SetColorScheme") },
|
|
{ ShortcutAction::SetTabColor, RS_(L"ResetTabColorCommandKey") },
|
|
{ ShortcutAction::SplitPane, RS_(L"SplitPaneCommandKey") },
|
|
{ ShortcutAction::SwitchToTab, RS_(L"SwitchToTabCommandKey") },
|
|
{ ShortcutAction::TabSearch, RS_(L"TabSearchCommandKey") },
|
|
{ ShortcutAction::ToggleAlwaysOnTop, RS_(L"ToggleAlwaysOnTopCommandKey") },
|
|
{ ShortcutAction::ToggleCommandPalette, RS_(L"ToggleCommandPaletteCommandKey") },
|
|
{ ShortcutAction::Suggestions, RS_(L"Suggestions") },
|
|
{ ShortcutAction::ToggleFocusMode, RS_(L"ToggleFocusModeCommandKey") },
|
|
{ ShortcutAction::SetFocusMode, RS_(L"SetFocusMode") },
|
|
{ ShortcutAction::ToggleFullscreen, RS_(L"ToggleFullscreenCommandKey") },
|
|
{ ShortcutAction::SetFullScreen, RS_(L"SetFullScreen") },
|
|
{ ShortcutAction::SetMaximized, RS_(L"SetMaximized") },
|
|
{ ShortcutAction::TogglePaneZoom, RS_(L"TogglePaneZoomCommandKey") },
|
|
{ ShortcutAction::ToggleSplitOrientation, RS_(L"ToggleSplitOrientationCommandKey") },
|
|
{ ShortcutAction::ToggleShaderEffects, RS_(L"ToggleShaderEffectsCommandKey") },
|
|
{ ShortcutAction::MoveTab, RS_(L"MoveTab") },
|
|
{ ShortcutAction::BreakIntoDebugger, RS_(L"BreakIntoDebuggerCommandKey") },
|
|
{ ShortcutAction::FindMatch, RS_(L"FindMatch") },
|
|
{ ShortcutAction::TogglePaneReadOnly, RS_(L"TogglePaneReadOnlyCommandKey") },
|
|
{ ShortcutAction::EnablePaneReadOnly, RS_(L"EnablePaneReadOnlyCommandKey") },
|
|
{ ShortcutAction::DisablePaneReadOnly, RS_(L"DisablePaneReadOnlyCommandKey") },
|
|
{ ShortcutAction::NewWindow, RS_(L"NewWindowCommandKey") },
|
|
{ ShortcutAction::IdentifyWindow, RS_(L"IdentifyWindowCommandKey") },
|
|
{ ShortcutAction::IdentifyWindows, RS_(L"IdentifyWindowsCommandKey") },
|
|
{ ShortcutAction::RenameWindow, RS_(L"ResetWindowNameCommandKey") },
|
|
{ ShortcutAction::OpenWindowRenamer, RS_(L"OpenWindowRenamerCommandKey") },
|
|
{ ShortcutAction::DisplayWorkingDirectory, RS_(L"DisplayWorkingDirectoryCommandKey") },
|
|
{ ShortcutAction::GlobalSummon, RS_(L"GlobalSummonCommandKey") },
|
|
{ ShortcutAction::SearchForText, RS_(L"SearchForText") },
|
|
{ ShortcutAction::QuakeMode, RS_(L"QuakeModeCommandKey") },
|
|
{ ShortcutAction::FocusPane, RS_(L"FocusPane") },
|
|
{ ShortcutAction::OpenSystemMenu, RS_(L"OpenSystemMenuCommandKey") },
|
|
{ ShortcutAction::ExportBuffer, RS_(L"ExportBuffer") },
|
|
{ ShortcutAction::ClearBuffer, RS_(L"ClearBuffer") },
|
|
{ ShortcutAction::MultipleActions, RS_(L"MultipleActions") },
|
|
{ ShortcutAction::Quit, RS_(L"QuitCommandKey") },
|
|
{ ShortcutAction::AdjustOpacity, RS_(L"AdjustOpacity") },
|
|
{ ShortcutAction::RestoreLastClosed, RS_(L"RestoreLastClosedCommandKey") },
|
|
{ ShortcutAction::SelectCommand, RS_(L"SelectCommand") },
|
|
{ ShortcutAction::SelectOutput, RS_(L"SelectOutput") },
|
|
{ ShortcutAction::SelectAll, RS_(L"SelectAllCommandKey") },
|
|
{ ShortcutAction::MarkMode, RS_(L"MarkModeCommandKey") },
|
|
{ ShortcutAction::ToggleBlockSelection, RS_(L"ToggleBlockSelectionCommandKey") },
|
|
{ ShortcutAction::SwitchSelectionEndpoint, RS_(L"SwitchSelectionEndpointCommandKey") },
|
|
{ ShortcutAction::ColorSelection, RS_(L"ColorSelection") },
|
|
{ ShortcutAction::ShowContextMenu, RS_(L"ShowContextMenuCommandKey") },
|
|
{ ShortcutAction::ExpandSelectionToWord, RS_(L"ExpandSelectionToWordCommandKey") },
|
|
{ ShortcutAction::RestartConnection, RS_(L"RestartConnectionKey") },
|
|
{ ShortcutAction::ToggleBroadcastInput, RS_(L"ToggleBroadcastInputCommandKey") },
|
|
{ ShortcutAction::OpenScratchpad, RS_(L"OpenScratchpadKey") },
|
|
{ ShortcutAction::OpenAbout, RS_(L"OpenAboutCommandKey") },
|
|
{ ShortcutAction::QuickFix, RS_(L"QuickFixCommandKey") },
|
|
{ ShortcutAction::OpenCWD, RS_(L"OpenCWDCommandKey") },
|
|
{ ShortcutAction::SaveSnippet, RS_(L"SaveSnippetNamePrefix") },
|
|
};
|
|
}();
|
|
|
|
const auto found = actionNames.find(action);
|
|
return found != actionNames.end() ? found->second : winrt::hstring{};
|
|
}
|
|
|
|
winrt::Windows::Foundation::Collections::IMap<Model::ShortcutAction, winrt::hstring> ActionArgFactory::AvailableShortcutActionsAndNames()
|
|
{
|
|
std::unordered_map<ShortcutAction, winrt::hstring> actionNames;
|
|
#define ON_ALL_ACTIONS(action) actionNames.emplace(ShortcutAction::action, GetNameForAction(ShortcutAction::action));
|
|
|
|
ALL_SHORTCUT_ACTIONS
|
|
|
|
#undef ON_ALL_ACTIONS
|
|
return winrt::single_threaded_map(std::move(actionNames));
|
|
}
|
|
|
|
Model::IActionArgs ActionArgFactory::GetEmptyArgsForAction(Model::ShortcutAction shortcutAction)
|
|
{
|
|
switch (shortcutAction)
|
|
{
|
|
#define ON_ALL_ACTIONS_WITH_ARGS(name) \
|
|
case Model::ShortcutAction::name: \
|
|
return winrt::make<name##Args>();
|
|
|
|
ALL_SHORTCUT_ACTIONS_WITH_ARGS
|
|
|
|
#undef ON_ALL_ACTIONS_WITH_ARGS
|
|
|
|
default:
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Detects if any of the user's actions are identical to the inbox actions,
|
|
// and if so, deletes them and redirects their keybindings to the inbox actions
|
|
// - We have to do this here instead of when loading since we don't actually have
|
|
// any parents while loading the user settings, the parents are added after
|
|
void ActionMap::_FinalizeInheritance()
|
|
{
|
|
// first, gather the inbox actions from the relevant parent
|
|
std::unordered_map<InternalActionID, Model::Command> inboxActions;
|
|
winrt::com_ptr<implementation::ActionMap> foundParent{ nullptr };
|
|
for (const auto& parent : _parents)
|
|
{
|
|
const auto parentMap = parent->_ActionMap;
|
|
if (parentMap.begin() != parentMap.end() && parentMap.begin()->second.Origin() == OriginTag::InBox)
|
|
{
|
|
// only one parent contains all the inbox actions and that parent contains only inbox actions,
|
|
// so if we found an inbox action we know this is the parent we are looking for
|
|
foundParent = parent;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (foundParent)
|
|
{
|
|
for (const auto& [_, cmd] : foundParent->_ActionMap)
|
|
{
|
|
inboxActions.emplace(Hash(cmd.ActionAndArgs()), cmd);
|
|
}
|
|
}
|
|
|
|
std::unordered_map<KeyChord, winrt::hstring, KeyChordHash, KeyChordEquality> keysToReassign;
|
|
|
|
// now, look through our _ActionMap for commands that
|
|
// - had an ID generated for them
|
|
// - do not have a name/icon path
|
|
// - have a hash that matches a command in the inbox actions
|
|
std::erase_if(_ActionMap, [&](const auto& pair) {
|
|
const auto userCmdImpl{ get_self<Command>(pair.second) };
|
|
if (userCmdImpl->IDWasGenerated() && !userCmdImpl->HasName() && userCmdImpl->IconPath().empty())
|
|
{
|
|
const auto userActionHash = Hash(userCmdImpl->ActionAndArgs());
|
|
if (const auto inboxCmd = inboxActions.find(userActionHash); inboxCmd != inboxActions.end())
|
|
{
|
|
for (const auto& [key, cmdID] : _KeyMap)
|
|
{
|
|
// for any of our keys that point to the user action, point them to the inbox action instead
|
|
if (cmdID == pair.first)
|
|
{
|
|
keysToReassign.insert_or_assign(key, inboxCmd->second.ID());
|
|
}
|
|
}
|
|
|
|
// remove this pair
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
});
|
|
|
|
for (const auto [key, cmdID] : keysToReassign)
|
|
{
|
|
_KeyMap.insert_or_assign(key, cmdID);
|
|
}
|
|
}
|
|
|
|
bool ActionMap::FixupsAppliedDuringLoad() const
|
|
{
|
|
return _fixupsAppliedDuringLoad;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Retrieves the Command referred to be the given ID
|
|
// - Will recurse through parents if we don't find it in this layer
|
|
// Arguments:
|
|
// - actionID: the internal ID associated with a Command
|
|
// Return Value:
|
|
// - The command if it exists in this layer, otherwise nullptr
|
|
Model::Command ActionMap::_GetActionByID(const winrt::hstring& actionID) const
|
|
{
|
|
// 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;
|
|
}
|
|
|
|
for (const auto& parent : _parents)
|
|
{
|
|
if (const auto inheritedCmd = parent->_GetActionByID(actionID))
|
|
{
|
|
return inheritedCmd;
|
|
}
|
|
}
|
|
|
|
// We don't have an answer
|
|
return nullptr;
|
|
}
|
|
|
|
static void RegisterShortcutAction(ShortcutAction shortcutAction, std::unordered_map<hstring, Model::ActionAndArgs>& list, std::unordered_set<InternalActionID>& visited)
|
|
{
|
|
const auto actionAndArgs{ make_self<ActionAndArgs>(shortcutAction) };
|
|
/*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 if it has a name.*/
|
|
if (const auto name{ actionAndArgs->GenerateName() }; !name.empty())
|
|
{
|
|
list.insert({ name, *actionAndArgs });
|
|
}
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Retrieves a map of actions that can be bound to a key
|
|
IMapView<hstring, Model::ActionAndArgs> ActionMap::AvailableActions()
|
|
{
|
|
if (!_AvailableActionsCache)
|
|
{
|
|
// populate _AvailableActionsCache
|
|
std::unordered_map<hstring, Model::ActionAndArgs> availableActions;
|
|
std::unordered_set<InternalActionID> 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
|
|
// Don't include internal actions here
|
|
#undef ON_ALL_ACTIONS
|
|
|
|
_AvailableActionsCache = single_threaded_map(std::move(availableActions));
|
|
}
|
|
return _AvailableActionsCache.GetView();
|
|
}
|
|
|
|
void ActionMap::_PopulateAvailableActionsWithStandardCommands(std::unordered_map<hstring, Model::ActionAndArgs>& availableActions, std::unordered_set<InternalActionID>& visitedActionIDs) const
|
|
{
|
|
// Update AvailableActions and visitedActionIDs with our current layer
|
|
for (const auto& [_, cmd] : _ActionMap)
|
|
{
|
|
// Only populate AvailableActions with actions that haven't been visited already.
|
|
const auto actionID = Hash(cmd.ActionAndArgs());
|
|
if (!visitedActionIDs.contains(actionID))
|
|
{
|
|
const auto name{ cmd.Name() };
|
|
if (!name.empty())
|
|
{
|
|
// Update AvailableActions.
|
|
const auto actionAndArgsImpl{ get_self<ActionAndArgs>(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`
|
|
IMapView<hstring, Model::Command> ActionMap::NameMap()
|
|
{
|
|
if (!_NameMapCache)
|
|
{
|
|
if (_CumulativeIDToActionMapCache.empty())
|
|
{
|
|
_RefreshKeyBindingCaches();
|
|
}
|
|
// populate _NameMapCache
|
|
std::unordered_map<hstring, Model::Command> 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<hstring, Model::Command>& nameMap) const
|
|
{
|
|
// Update NameMap with our parents.
|
|
// Starting with this means we're doing a top-down approach.
|
|
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
|
|
void ActionMap::_PopulateNameMapWithStandardCommands(std::unordered_map<hstring, Model::Command>& nameMap) const
|
|
{
|
|
for (const auto& [_, cmd] : _CumulativeIDToActionMapCache)
|
|
{
|
|
const auto& name{ cmd.Name() };
|
|
if (!name.empty())
|
|
{
|
|
// there might be a collision here, where there could be 2 different commands with the same name
|
|
// in this case, prioritize the user's action
|
|
// TODO GH #17166: we should no longer use Command.Name to identify commands anywhere
|
|
if (!nameMap.contains(name) || cmd.Origin() == OriginTag::User)
|
|
{
|
|
// either a command with this name does not exist, or this is a user-defined command with a name
|
|
// in either case, update the name map with the command (if this is a user-defined command with
|
|
// the same name as an existing command, the existing one will get overwritten)
|
|
nameMap.insert_or_assign(name, cmd);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Recursively populate keyToActionMap with ours and our parents' key -> id pairs
|
|
// - Recursively populate actionToKeyMap with ours and our parents' id -> key pairs
|
|
// - This is a bottom-up approach
|
|
// - Child's pairs override parents' pairs
|
|
void ActionMap::_PopulateCumulativeKeyMaps(std::unordered_map<Control::KeyChord, winrt::hstring, KeyChordHash, KeyChordEquality>& keyToActionMap, std::unordered_map<winrt::hstring, Control::KeyChord>& actionToKeyMap)
|
|
{
|
|
for (const auto& [keys, cmdID] : _KeyMap)
|
|
{
|
|
if (!keyToActionMap.contains(keys))
|
|
{
|
|
keyToActionMap.emplace(keys, cmdID);
|
|
}
|
|
if (!actionToKeyMap.contains(cmdID))
|
|
{
|
|
actionToKeyMap.emplace(cmdID, keys);
|
|
}
|
|
}
|
|
|
|
for (const auto& parent : _parents)
|
|
{
|
|
parent->_PopulateCumulativeKeyMaps(keyToActionMap, actionToKeyMap);
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Recursively populate actionMap with ours and our parents' id -> command pairs
|
|
// - This is a bottom-up approach
|
|
// - Actions of the parents are overridden by the children
|
|
void ActionMap::_PopulateCumulativeActionMap(std::unordered_map<hstring, Model::Command>& actionMap)
|
|
{
|
|
for (const auto& [cmdID, cmd] : _ActionMap)
|
|
{
|
|
if (!actionMap.contains(cmdID))
|
|
{
|
|
actionMap.emplace(cmdID, cmd);
|
|
}
|
|
}
|
|
|
|
for (const auto& parent : _parents)
|
|
{
|
|
parent->_PopulateCumulativeActionMap(actionMap);
|
|
}
|
|
}
|
|
|
|
IMapView<Control::KeyChord, Model::Command> ActionMap::GlobalHotkeys()
|
|
{
|
|
if (!_GlobalHotkeysCache)
|
|
{
|
|
_RefreshKeyBindingCaches();
|
|
}
|
|
return _GlobalHotkeysCache.GetView();
|
|
}
|
|
|
|
IMapView<Control::KeyChord, Model::Command> ActionMap::KeyBindings()
|
|
{
|
|
if (!_ResolvedKeyToActionMapCache)
|
|
{
|
|
_RefreshKeyBindingCaches();
|
|
}
|
|
return _ResolvedKeyToActionMapCache.GetView();
|
|
}
|
|
|
|
IVectorView<Model::Command> ActionMap::AllCommands()
|
|
{
|
|
if (!_ResolvedKeyToActionMapCache)
|
|
{
|
|
_RefreshKeyBindingCaches();
|
|
}
|
|
return _AllCommandsCache.GetView();
|
|
}
|
|
|
|
void ActionMap::_RefreshKeyBindingCaches()
|
|
{
|
|
_CumulativeKeyToActionMapCache.clear();
|
|
_CumulativeIDToActionMapCache.clear();
|
|
_CumulativeActionToKeyMapCache.clear();
|
|
std::unordered_map<KeyChord, Model::Command, KeyChordHash, KeyChordEquality> globalHotkeys;
|
|
std::unordered_map<KeyChord, Model::Command, KeyChordHash, KeyChordEquality> resolvedKeyToActionMap;
|
|
std::vector<Model::Command> allCommandsVector;
|
|
|
|
_PopulateCumulativeKeyMaps(_CumulativeKeyToActionMapCache, _CumulativeActionToKeyMapCache);
|
|
_PopulateCumulativeActionMap(_CumulativeIDToActionMapCache);
|
|
|
|
for (const auto& [keys, cmdID] : _CumulativeKeyToActionMapCache)
|
|
{
|
|
if (const auto idCmdPair = _CumulativeIDToActionMapCache.find(cmdID); idCmdPair != _CumulativeIDToActionMapCache.end())
|
|
{
|
|
resolvedKeyToActionMap.emplace(keys, idCmdPair->second);
|
|
|
|
// Only populate GlobalHotkeys with actions whose
|
|
// ShortcutAction is GlobalSummon or QuakeMode
|
|
if (idCmdPair->second.ActionAndArgs().Action() == ShortcutAction::GlobalSummon || idCmdPair->second.ActionAndArgs().Action() == ShortcutAction::QuakeMode)
|
|
{
|
|
globalHotkeys.emplace(keys, idCmdPair->second);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const auto& [_, cmd] : _CumulativeIDToActionMapCache)
|
|
{
|
|
allCommandsVector.emplace_back(cmd);
|
|
}
|
|
|
|
_ResolvedKeyToActionMapCache = single_threaded_map(std::move(resolvedKeyToActionMap));
|
|
_GlobalHotkeysCache = single_threaded_map(std::move(globalHotkeys));
|
|
_AllCommandsCache = single_threaded_vector(std::move(allCommandsVector));
|
|
}
|
|
|
|
com_ptr<ActionMap> ActionMap::Copy() const
|
|
{
|
|
auto actionMap{ make_self<ActionMap>() };
|
|
|
|
// KeyChord --> ID
|
|
actionMap->_KeyMap = _KeyMap;
|
|
|
|
// ID --> Command
|
|
actionMap->_ActionMap.reserve(_ActionMap.size());
|
|
for (const auto& [actionID, cmd] : _ActionMap)
|
|
{
|
|
const auto copiedCmd = winrt::get_self<Command>(cmd)->Copy();
|
|
actionMap->_ActionMap.emplace(actionID, *copiedCmd);
|
|
copiedCmd->IDChanged({ actionMap.get(), &ActionMap::_CommandIDChangedHandler });
|
|
}
|
|
|
|
// Name --> Command
|
|
actionMap->_NestedCommands.reserve(_NestedCommands.size());
|
|
for (const auto& [name, cmd] : _NestedCommands)
|
|
{
|
|
actionMap->_NestedCommands.emplace(name, *winrt::get_self<Command>(cmd)->Copy());
|
|
}
|
|
|
|
actionMap->_IterableCommands.reserve(_IterableCommands.size());
|
|
for (const auto& cmd : _IterableCommands)
|
|
{
|
|
actionMap->_IterableCommands.emplace_back(*winrt::get_self<Command>(cmd)->Copy());
|
|
}
|
|
|
|
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, const Control::KeyChord& keys)
|
|
{
|
|
// _Never_ add null to the ActionMap
|
|
if (!cmd)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// invalidate caches
|
|
_CumulativeKeyToActionMapCache.clear();
|
|
_CumulativeIDToActionMapCache.clear();
|
|
_CumulativeActionToKeyMapCache.clear();
|
|
_NameMapCache = nullptr;
|
|
_GlobalHotkeysCache = nullptr;
|
|
_ResolvedKeyToActionMapCache = nullptr;
|
|
|
|
// Handle nested commands
|
|
const auto cmdImpl{ get_self<Command>(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 _ActionMap
|
|
// Add the new keybinding to the _KeyMap
|
|
|
|
_TryUpdateActionMap(cmd);
|
|
_TryUpdateKeyChord(cmd, keys);
|
|
}
|
|
|
|
// Method Description:
|
|
// - Try to add the new command to _ActionMap
|
|
// Arguments:
|
|
// - cmd: the action we're trying to register
|
|
void ActionMap::_TryUpdateActionMap(const Model::Command& cmd)
|
|
{
|
|
// if the shortcut action is invalid, then this is for unbinding and _TryUpdateKeyChord will handle that
|
|
if (cmd.ActionAndArgs().Action() != ShortcutAction::Invalid)
|
|
{
|
|
const auto cmdImpl{ get_self<implementation::Command>(cmd) };
|
|
if (cmd.Origin() == OriginTag::User && cmd.ID().empty())
|
|
{
|
|
// the user did not define an ID for their non-nested, non-iterable, valid command - generate one for them
|
|
cmdImpl->GenerateID();
|
|
}
|
|
|
|
// only add to the _ActionMap if there is an ID
|
|
if (auto cmdID = cmd.ID(); !cmdID.empty())
|
|
{
|
|
// in the legacy scenario, a user might have several of the same action but only one of them has defined an icon or a name
|
|
// eg. { "command": "paste", "name": "myPaste", "keys":"ctrl+a" }
|
|
// { "command": "paste", "keys": "ctrl+b" }
|
|
// once they port over to the new implementation, we will reduce it to just one Command object with a generated ID
|
|
// but several key binding entries, like so
|
|
// { "command": "newTab", "id": "User.paste" } -> in the actions map
|
|
// { "keys": "ctrl+a", "id": "User.paste" } -> in the keybindings map
|
|
// { "keys": "ctrl+b", "id": "User.paste" } -> in the keybindings map
|
|
// however, we have to make sure that we preserve the icon/name that might have been there in one of the command objects
|
|
// to do that, we check if this command we're adding had an ID that was generated
|
|
// if so, we check if there already exists a command with that generated ID, and if there is we port over any name/icon there might be
|
|
// (this may cause us to overwrite in scenarios where the user has an existing command that has the same generated ID but
|
|
// performs a different action or has different args, but that falls under "play stupid games")
|
|
if (cmdImpl->IDWasGenerated())
|
|
{
|
|
if (const auto foundCmd{ _GetActionByID(cmdID) })
|
|
{
|
|
const auto foundCmdImpl{ get_self<implementation::Command>(foundCmd) };
|
|
if (foundCmdImpl->HasName() && !cmdImpl->HasName())
|
|
{
|
|
cmdImpl->Name(foundCmdImpl->Name());
|
|
}
|
|
if (!foundCmdImpl->IconPath().empty() && cmdImpl->IconPath().empty())
|
|
{
|
|
cmdImpl->IconPath(foundCmdImpl->IconPath());
|
|
}
|
|
}
|
|
}
|
|
cmd.IDChanged({ this, &ActionMap::_CommandIDChangedHandler });
|
|
_ActionMap.insert_or_assign(cmdID, cmd);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Update our internal state with the key chord of the newly registered action
|
|
// Arguments:
|
|
// - cmd: the action we're trying to register
|
|
void ActionMap::_TryUpdateKeyChord(const Model::Command& cmd, const Control::KeyChord& keys)
|
|
{
|
|
// Example (this is a legacy case, where the keys are provided in the same block as the command):
|
|
// { "command": "copy", "keys": "ctrl+c" } --> we are registering a new key chord
|
|
// { "name": "foo", "command": "copy" } --> no change to keys, exit early
|
|
if (!keys)
|
|
{
|
|
// the user is not trying to update the keys.
|
|
return;
|
|
}
|
|
|
|
// Assign the new action in the _KeyMap
|
|
// However, there's a strange edge case here - since we're parsing a legacy or modern block,
|
|
// the user might have { "command": null, "id": "someID", "keys": "ctrl+c" }
|
|
// i.e. they provided an ID for a null command (which they really shouldn't, there's no purpose)
|
|
// in this case, we do _not_ want to use the id they provided, we want to use an empty id
|
|
// (empty id in the _KeyMap indicates the keychord was explicitly unbound)
|
|
const auto action = cmd.ActionAndArgs().Action();
|
|
const auto id = action == ShortcutAction::Invalid ? hstring{} : cmd.ID();
|
|
_KeyMap.insert_or_assign(keys, id);
|
|
_changeLog.emplace(KeysKey);
|
|
}
|
|
|
|
void ActionMap::_CommandIDChangedHandler(const Model::Command& senderCmd, const winrt::hstring& oldID)
|
|
{
|
|
const auto newID = senderCmd.ID();
|
|
if (newID != oldID)
|
|
{
|
|
if (const auto foundCmd{ _GetActionByID(newID) })
|
|
{
|
|
if (foundCmd.ActionAndArgs() != senderCmd.ActionAndArgs())
|
|
{
|
|
// we found a command that has the same ID as this one, but that command has different ActionAndArgs
|
|
// this means that foundCommand's action and/or args have been changed since its ID was generated,
|
|
// generate a new one for it
|
|
// Note: this is recursive! Found command's ID being changed lands us back in here to resolve any cascading collisions
|
|
foundCmd.GenerateID();
|
|
}
|
|
}
|
|
// update _ActionMap with the ID change
|
|
_ActionMap.erase(oldID);
|
|
_ActionMap.emplace(newID, senderCmd);
|
|
|
|
// update _KeyMap so that all keys that pointed to the old ID now point to the new ID
|
|
std::unordered_set<KeyChord, KeyChordHash, KeyChordEquality> keysToRemap{};
|
|
for (const auto& [keys, cmdID] : _KeyMap)
|
|
{
|
|
if (cmdID == oldID)
|
|
{
|
|
keysToRemap.insert(keys);
|
|
}
|
|
}
|
|
for (const auto& keys : keysToRemap)
|
|
{
|
|
_KeyMap.erase(keys);
|
|
_KeyMap.emplace(keys, newID);
|
|
}
|
|
PropagateCommandIDChanged.raise(senderCmd, oldID);
|
|
}
|
|
_RefreshKeyBindingCaches();
|
|
}
|
|
|
|
// 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(const Control::KeyChord& 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(const Control::KeyChord& keys) const
|
|
{
|
|
return _GetActionByKeyChordInternal(keys).value_or(nullptr);
|
|
}
|
|
|
|
Model::Command ActionMap::GetActionByID(const winrt::hstring& cmdID) const
|
|
{
|
|
return _GetActionByID(cmdID);
|
|
}
|
|
|
|
// Method Description:
|
|
// - Retrieves the assigned command ID 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 ID with the given key chord
|
|
// - an empty string if the key chord is explicitly unbound
|
|
// - nullopt if it is not bound
|
|
std::optional<winrt::hstring> ActionMap::_GetActionIdByKeyChordInternal(const Control::KeyChord& keys) const
|
|
{
|
|
if (const auto keyIDPair = _KeyMap.find(keys); keyIDPair != _KeyMap.end())
|
|
{
|
|
// the keychord is defined in this layer, return the ID
|
|
return keyIDPair->second;
|
|
}
|
|
|
|
// search through our parents
|
|
for (const auto& parent : _parents)
|
|
{
|
|
if (const auto foundCmdID = parent->_GetActionIdByKeyChordInternal(keys))
|
|
{
|
|
return foundCmdID;
|
|
}
|
|
}
|
|
|
|
// we did not find the keychord anywhere, it's not bound and not explicitly unbound either
|
|
return std::nullopt;
|
|
}
|
|
|
|
// 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 is not bound
|
|
std::optional<Model::Command> ActionMap::_GetActionByKeyChordInternal(const Control::KeyChord& keys) const
|
|
{
|
|
if (const auto actionIDOptional = _GetActionIdByKeyChordInternal(keys))
|
|
{
|
|
if (!actionIDOptional->empty())
|
|
{
|
|
// there is an ID associated with these keys, find the command
|
|
if (const auto foundCmd = _GetActionByID(*actionIDOptional))
|
|
{
|
|
return foundCmd;
|
|
}
|
|
}
|
|
// the ID is an empty string, these keys are explicitly unbound
|
|
return nullptr;
|
|
}
|
|
|
|
return std::nullopt;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Retrieves the key chord for the provided action
|
|
// Arguments:
|
|
// - cmdID: the ID of the command 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(const winrt::hstring& cmdID)
|
|
{
|
|
if (!_ResolvedKeyToActionMapCache)
|
|
{
|
|
_RefreshKeyBindingCaches();
|
|
}
|
|
|
|
if (_CumulativeActionToKeyMapCache.contains(cmdID))
|
|
{
|
|
return _CumulativeActionToKeyMapCache.at(cmdID);
|
|
}
|
|
|
|
// This key binding does not exist
|
|
return nullptr;
|
|
}
|
|
|
|
IVector<Control::KeyChord> ActionMap::AllKeyBindingsForAction(const winrt::hstring& cmdID)
|
|
{
|
|
if (!_ResolvedKeyToActionMapCache)
|
|
{
|
|
_RefreshKeyBindingCaches();
|
|
}
|
|
|
|
std::vector<Control::KeyChord> keybindingsList;
|
|
for (const auto& [key, ID] : _CumulativeKeyToActionMapCache)
|
|
{
|
|
if (ID == cmdID)
|
|
{
|
|
keybindingsList.emplace_back(key);
|
|
}
|
|
}
|
|
return single_threaded_vector(std::move(keybindingsList));
|
|
}
|
|
|
|
// 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(const Control::KeyChord& oldKeys, const Control::KeyChord& newKeys)
|
|
{
|
|
const auto cmd{ GetActionByKeyChord(oldKeys) };
|
|
if (!cmd)
|
|
{
|
|
// oldKeys must be bound. Otherwise, we don't know what action to bind.
|
|
return false;
|
|
}
|
|
|
|
if (auto oldKeyPair = _KeyMap.find(oldKeys); oldKeyPair != _KeyMap.end())
|
|
{
|
|
// oldKeys is bound in our layer, replace it with newKeys
|
|
_KeyMap.insert_or_assign(newKeys, cmd.ID());
|
|
_KeyMap.erase(oldKeyPair);
|
|
}
|
|
else
|
|
{
|
|
// oldKeys is bound in some other layer, set newKeys to cmd in this layer, and oldKeys to unbound in this layer
|
|
_KeyMap.insert_or_assign(newKeys, cmd.ID());
|
|
_KeyMap.insert_or_assign(oldKeys, L"");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Unbind a key chord
|
|
// Arguments:
|
|
// - keys: the key chord that is being unbound
|
|
// Return Value:
|
|
// - <none>
|
|
void ActionMap::DeleteKeyBinding(const KeyChord& keys)
|
|
{
|
|
if (auto keyPair = _KeyMap.find(keys); keyPair != _KeyMap.end())
|
|
{
|
|
// this keychord is bound in our layer, delete it
|
|
_KeyMap.erase(keyPair);
|
|
}
|
|
|
|
// either the keychord was never in this layer or we just deleted it above,
|
|
// if GetActionByKeyChord still returns a command that means the keychord is bound in another layer
|
|
if (GetActionByKeyChord(keys))
|
|
{
|
|
// set to unbound in this layer
|
|
_KeyMap.emplace(keys, L"");
|
|
}
|
|
}
|
|
|
|
void ActionMap::AddKeyBinding(Control::KeyChord keys, const winrt::hstring& cmdID)
|
|
{
|
|
_KeyMap.insert_or_assign(keys, cmdID);
|
|
_changeLog.emplace(KeysKey);
|
|
_RefreshKeyBindingCaches();
|
|
}
|
|
|
|
// 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:
|
|
// - <none>
|
|
void ActionMap::RegisterKeyBinding(Control::KeyChord keys, Model::ActionAndArgs action)
|
|
{
|
|
auto cmd{ make_self<Command>() };
|
|
cmd->ActionAndArgs(action);
|
|
cmd->GenerateID();
|
|
AddAction(*cmd, keys);
|
|
}
|
|
|
|
void ActionMap::DeleteUserCommand(const winrt::hstring& cmdID)
|
|
{
|
|
_ActionMap.erase(cmdID);
|
|
_RefreshKeyBindingCaches();
|
|
}
|
|
|
|
// This is a helper to aid in sorting commands by their `Name`s, alphabetically.
|
|
static bool _compareSchemeNames(const ColorScheme& lhs, const ColorScheme& rhs)
|
|
{
|
|
std::wstring leftName{ lhs.Name() };
|
|
std::wstring rightName{ rhs.Name() };
|
|
return leftName.compare(rightName) < 0;
|
|
}
|
|
|
|
void ActionMap::ExpandCommands(const IVectorView<Model::Profile>& profiles,
|
|
const IMapView<winrt::hstring, Model::ColorScheme>& schemes)
|
|
{
|
|
// TODO in review - It's a little weird to stash the expanded commands
|
|
// into a separate map. Is it possible to just replace the name map with
|
|
// the post-expanded commands?
|
|
//
|
|
// WHILE also making sure that upon re-saving the commands, we don't
|
|
// actually serialize the results of the expansion. I don't think it is.
|
|
|
|
std::vector<Model::ColorScheme> sortedSchemes;
|
|
sortedSchemes.reserve(schemes.Size());
|
|
|
|
for (const auto& nameAndScheme : schemes)
|
|
{
|
|
sortedSchemes.push_back(nameAndScheme.Value());
|
|
}
|
|
std::sort(sortedSchemes.begin(),
|
|
sortedSchemes.end(),
|
|
_compareSchemeNames);
|
|
|
|
auto copyOfCommands = winrt::single_threaded_map<winrt::hstring, Model::Command>();
|
|
|
|
const auto& commandsToExpand{ NameMap() };
|
|
for (auto nameAndCommand : commandsToExpand)
|
|
{
|
|
copyOfCommands.Insert(nameAndCommand.Key(), nameAndCommand.Value());
|
|
}
|
|
|
|
implementation::Command::ExpandCommands(copyOfCommands,
|
|
profiles,
|
|
winrt::param::vector_view<Model::ColorScheme>{ sortedSchemes });
|
|
|
|
_ExpandedCommandsCache = winrt::single_threaded_vector<Model::Command>();
|
|
for (const auto& [_, command] : copyOfCommands)
|
|
{
|
|
_ExpandedCommandsCache.Append(command);
|
|
}
|
|
}
|
|
IVector<Model::Command> ActionMap::ExpandedCommands()
|
|
{
|
|
return _ExpandedCommandsCache;
|
|
}
|
|
|
|
#pragma region Snippets
|
|
std::vector<Model::Command> _filterToSnippets(IMapView<hstring, Model::Command> nameMap,
|
|
winrt::hstring currentCommandline,
|
|
const std::vector<Model::Command>& localCommands)
|
|
{
|
|
std::vector<Model::Command> results{};
|
|
|
|
const auto numBackspaces = currentCommandline.size();
|
|
// Helper to clone a sendInput command into a new Command, with the
|
|
// input trimmed to account for the currentCommandline
|
|
auto createInputAction = [&](const Model::Command& command) -> Model::Command {
|
|
winrt::com_ptr<implementation::Command> cmdImpl;
|
|
cmdImpl.copy_from(winrt::get_self<implementation::Command>(command));
|
|
|
|
const auto inArgs{ command.ActionAndArgs().Args().try_as<Model::SendInputArgs>() };
|
|
const auto inputString{ inArgs ? inArgs.Input() : L"" };
|
|
auto args = winrt::make_self<SendInputArgs>(
|
|
winrt::hstring{ fmt::format(FMT_COMPILE(L"{:\x7f^{}}{}"),
|
|
L"",
|
|
numBackspaces,
|
|
inputString) });
|
|
Model::ActionAndArgs actionAndArgs{ ShortcutAction::SendInput, *args };
|
|
|
|
auto copy = cmdImpl->Copy();
|
|
copy->ActionAndArgs(actionAndArgs);
|
|
|
|
if (!copy->HasName())
|
|
{
|
|
// Here, we want to manually generate a send input name, but
|
|
// without visualizing space and backspace
|
|
//
|
|
// This is exactly the body of SendInputArgs::GenerateName, but
|
|
// with visualize_nonspace_control_codes instead of
|
|
// visualize_control_codes, to make filtering in the suggestions
|
|
// UI easier.
|
|
|
|
const auto escapedInput = til::visualize_nonspace_control_codes(std::wstring{ inputString });
|
|
const auto name = RS_fmt(L"SendInputCommandKey", escapedInput);
|
|
copy->Name(winrt::hstring{ name });
|
|
}
|
|
|
|
return *copy;
|
|
};
|
|
|
|
// Helper to copy this command into a snippet-styled command, and any
|
|
// nested commands
|
|
const auto addCommand = [&](auto& command) {
|
|
// If this is not a nested command, and it's a sendInput command...
|
|
if (!command.HasNestedCommands() &&
|
|
command.ActionAndArgs().Action() == ShortcutAction::SendInput)
|
|
{
|
|
// copy it into the results.
|
|
results.push_back(createInputAction(command));
|
|
}
|
|
// If this is nested...
|
|
else if (command.HasNestedCommands())
|
|
{
|
|
// Look for any sendInput commands nested underneath us
|
|
std::vector<Model::Command> empty{};
|
|
auto innerResults = winrt::single_threaded_vector<Model::Command>(_filterToSnippets(command.NestedCommands(), currentCommandline, empty));
|
|
|
|
if (innerResults.Size() > 0)
|
|
{
|
|
// This command did have at least one sendInput under it
|
|
|
|
// Create a new Command, which is a copy of this Command,
|
|
// which only has SendInputs in it
|
|
winrt::com_ptr<implementation::Command> cmdImpl;
|
|
cmdImpl.copy_from(winrt::get_self<implementation::Command>(command));
|
|
auto copy = cmdImpl->Copy();
|
|
copy->NestedCommands(innerResults.GetView());
|
|
|
|
results.push_back(*copy);
|
|
}
|
|
}
|
|
};
|
|
|
|
// iterate over all the commands in all our actions...
|
|
for (auto&& [_, command] : nameMap)
|
|
{
|
|
addCommand(command);
|
|
}
|
|
// ... and all the local commands passed in here
|
|
for (const auto& command : localCommands)
|
|
{
|
|
addCommand(command);
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
void ActionMap::AddSendInputAction(winrt::hstring name, winrt::hstring input, const Control::KeyChord keys)
|
|
{
|
|
auto newAction = winrt::make<ActionAndArgs>();
|
|
newAction.Action(ShortcutAction::SendInput);
|
|
auto sendInputArgs = winrt::make<SendInputArgs>(input);
|
|
newAction.Args(sendInputArgs);
|
|
auto cmd{ make_self<Command>() };
|
|
if (!name.empty())
|
|
{
|
|
cmd->Name(name);
|
|
}
|
|
cmd->ActionAndArgs(newAction);
|
|
cmd->GenerateID();
|
|
AddAction(*cmd, keys);
|
|
}
|
|
|
|
// Look for a .wt.json file in the given directory. If it exists,
|
|
// read it, parse it's JSON, and retrieve all the sendInput actions.
|
|
std::unordered_map<hstring, Model::Command> ActionMap::_loadLocalSnippets(const std::filesystem::path& currentWorkingDirectory)
|
|
{
|
|
// This returns an empty string if we fail to load the file.
|
|
std::filesystem::path localSnippetsPath = currentWorkingDirectory / std::filesystem::path{ ".wt.json" };
|
|
const auto data = til::io::read_file_as_utf8_string_if_exists(localSnippetsPath);
|
|
if (data.empty())
|
|
{
|
|
return {};
|
|
}
|
|
|
|
Json::Value root;
|
|
std::string errs;
|
|
const std::unique_ptr<Json::CharReader> reader{ Json::CharReaderBuilder{}.newCharReader() };
|
|
if (!reader->parse(data.data(), data.data() + data.size(), &root, &errs))
|
|
{
|
|
// In the real settings parser, we'd throw here:
|
|
// throw winrt::hresult_error(WEB_E_INVALID_JSON_STRING, winrt::to_hstring(errs));
|
|
//
|
|
// That seems overly aggressive for something that we don't
|
|
// really own. Instead, just bail out.
|
|
return {};
|
|
}
|
|
|
|
std::unordered_map<hstring, Model::Command> result;
|
|
if (auto actions{ root[JsonKey("snippets")] })
|
|
{
|
|
for (const auto& json : actions)
|
|
{
|
|
const auto snippet = Command::FromSnippetJson(json);
|
|
result.insert_or_assign(snippet->Name(), *snippet);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
winrt::Windows::Foundation::IAsyncOperation<IVector<Model::Command>> ActionMap::FilterToSnippets(
|
|
winrt::hstring currentCommandline,
|
|
winrt::hstring currentWorkingDirectory)
|
|
{
|
|
// enumerate all the parent directories we want to import snippets from
|
|
std::filesystem::path directory{ std::wstring_view{ currentWorkingDirectory } };
|
|
std::vector<std::filesystem::path> directories;
|
|
while (!directory.empty())
|
|
{
|
|
directories.push_back(directory);
|
|
auto parentPath = directory.parent_path();
|
|
if (directory == parentPath)
|
|
{
|
|
break;
|
|
}
|
|
directory = std::move(parentPath);
|
|
}
|
|
|
|
{
|
|
// Check if all the directories are already in the cache
|
|
const auto& cache{ _cwdLocalSnippetsCache.lock_shared() };
|
|
if (std::ranges::all_of(directories, [&](auto&& dir) { return cache->contains(dir); }))
|
|
{
|
|
// Load snippets from directories in reverse order.
|
|
// This ensures that we prioritize snippets closer to the cwd.
|
|
// The map makes it easy to avoid duplicates.
|
|
std::unordered_map<hstring, Model::Command> localSnippetsMap;
|
|
for (auto rit = directories.rbegin(); rit != directories.rend(); ++rit)
|
|
{
|
|
// register snippets from cache
|
|
for (const auto& [name, snippet] : cache->at(*rit))
|
|
{
|
|
localSnippetsMap.insert_or_assign(name, snippet);
|
|
}
|
|
}
|
|
|
|
std::vector<Model::Command> localSnippets;
|
|
localSnippets.reserve(localSnippetsMap.size());
|
|
std::ranges::transform(localSnippetsMap,
|
|
std::back_inserter(localSnippets),
|
|
[](const auto& kvPair) { return kvPair.second; });
|
|
co_return winrt::single_threaded_vector<Model::Command>(_filterToSnippets(NameMap(),
|
|
currentCommandline,
|
|
localSnippets));
|
|
}
|
|
} // release the lock on the cache
|
|
|
|
// Don't do I/O on the main thread
|
|
co_await winrt::resume_background();
|
|
|
|
// Load snippets from directories in reverse order.
|
|
// This ensures that we prioritize snippets closer to the cwd.
|
|
// The map makes it easy to avoid duplicates.
|
|
const auto& cache{ _cwdLocalSnippetsCache.lock() };
|
|
std::unordered_map<hstring, Model::Command> localSnippetsMap;
|
|
for (auto rit = directories.rbegin(); rit != directories.rend(); ++rit)
|
|
{
|
|
const auto& dir = *rit;
|
|
if (const auto cacheIterator = cache->find(dir); cacheIterator != cache->end())
|
|
{
|
|
// register snippets from cache
|
|
for (const auto& [name, snippet] : cache->at(*rit))
|
|
{
|
|
localSnippetsMap.insert_or_assign(name, snippet);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// we don't have this directory in the cache, so we need to load it
|
|
auto result = _loadLocalSnippets(dir);
|
|
cache->insert_or_assign(dir, result);
|
|
|
|
// register snippets from cache
|
|
std::ranges::for_each(result, [&localSnippetsMap](const auto& kvPair) {
|
|
localSnippetsMap.insert_or_assign(kvPair.first, kvPair.second);
|
|
});
|
|
}
|
|
}
|
|
|
|
std::vector<Model::Command> localSnippets;
|
|
localSnippets.reserve(localSnippetsMap.size());
|
|
std::ranges::transform(localSnippetsMap,
|
|
std::back_inserter(localSnippets),
|
|
[](const auto& kvPair) { return kvPair.second; });
|
|
co_return winrt::single_threaded_vector<Model::Command>(_filterToSnippets(NameMap(),
|
|
currentCommandline,
|
|
localSnippets));
|
|
}
|
|
#pragma endregion
|
|
}
|