diff --git a/src/cascadia/TerminalSettingsEditor/Actions.cpp b/src/cascadia/TerminalSettingsEditor/Actions.cpp index bd9403cece6..f68ae57dfae 100644 --- a/src/cascadia/TerminalSettingsEditor/Actions.cpp +++ b/src/cascadia/TerminalSettingsEditor/Actions.cpp @@ -4,61 +4,236 @@ #include "pch.h" #include "Actions.h" #include "Actions.g.cpp" +#include "KeyBindingViewModel.g.cpp" #include "ActionsPageNavigationState.g.cpp" -#include "EnumEntry.h" +#include "LibraryResources.h" using namespace winrt::Windows::Foundation; +using namespace winrt::Windows::Foundation::Collections; using namespace winrt::Windows::System; using namespace winrt::Windows::UI::Core; +using namespace winrt::Windows::UI::Xaml; +using namespace winrt::Windows::UI::Xaml::Controls; +using namespace winrt::Windows::UI::Xaml::Data; using namespace winrt::Windows::UI::Xaml::Navigation; using namespace winrt::Microsoft::Terminal::Settings::Model; namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { + KeyBindingViewModel::KeyBindingViewModel(const Control::KeyChord& keys, const Command& cmd) : + _Keys{ keys }, + _KeyChordText{ Model::KeyChordSerialization::ToString(keys) }, + _Command{ cmd } + { + // Add a property changed handler to our own property changed event. + // This propagates changes from the settings model to anybody listening to our + // unique view model members. + PropertyChanged([this](auto&&, const PropertyChangedEventArgs& args) { + const auto viewModelProperty{ args.PropertyName() }; + if (viewModelProperty == L"Keys") + { + _KeyChordText = Model::KeyChordSerialization::ToString(_Keys); + _NotifyChanges(L"KeyChordText"); + } + }); + } + + void KeyBindingViewModel::ToggleEditMode() + { + // toggle edit mode + IsInEditMode(!_IsInEditMode); + if (_IsInEditMode) + { + // if we're in edit mode, + // pre-populate the text box with the current keys + _ProposedKeys = KeyChordText(); + _NotifyChanges(L"ProposedKeys"); + } + } + + void KeyBindingViewModel::AttemptAcceptChanges() + { + auto args{ make_self(_Keys, _Keys) }; + try + { + // Attempt to convert the provided key chord text + const auto newKeyChord{ KeyChordSerialization::FromString(_ProposedKeys) }; + args->NewKeys(newKeyChord); + } + catch (hresult_invalid_argument) + { + // Converting the text into a key chord failed + // TODO CARLOS: + // This is tricky. I still haven't found a way to reference the + // key chord text box. It's hidden behind the data template. + // Ideally, some kind of notification would alert the user, but + // to make it look nice, we need it to somehow target the text box. + } + _RebindKeysRequestedHandlers(*this, *args); + } + Actions::Actions() { InitializeComponent(); - - _filteredActions = winrt::single_threaded_observable_vector(); } void Actions::OnNavigatedTo(const NavigationEventArgs& e) { _State = e.Parameter().as(); - std::vector keyBindingList; - for (const auto& [_, command] : _State.Settings().GlobalSettings().ActionMap().NameMap()) + // Convert the key bindings from our settings into a view model representation + const auto& keyBindingMap{ _State.Settings().ActionMap().KeyBindings() }; + std::vector keyBindingList; + keyBindingList.reserve(keyBindingMap.Size()); + for (const auto& [keys, cmd] : keyBindingMap) { - // Filter out nested commands, and commands that aren't bound to a - // key. This page is currently just for displaying the actions that - // _are_ bound to keys. - if (command.HasNestedCommands() || !command.Keys()) + const auto& container{ make(keys, cmd) }; + container.PropertyChanged({ this, &Actions::_ViewModelPropertyChangedHandler }); + container.DeleteKeyBindingRequested({ this, &Actions::_ViewModelDeleteKeyBindingHandler }); + container.RebindKeysRequested({ this, &Actions::_ViewModelRebindKeysHandler }); + keyBindingList.push_back(container); + } + + std::sort(begin(keyBindingList), end(keyBindingList), KeyBindingViewModelComparator{}); + _KeyBindingList = single_threaded_observable_vector(std::move(keyBindingList)); + } + + void Actions::_ViewModelPropertyChangedHandler(const IInspectable& sender, const Windows::UI::Xaml::Data::PropertyChangedEventArgs& args) + { + const auto senderVM{ sender.as() }; + const auto propertyName{ args.PropertyName() }; + if (propertyName == L"IsInEditMode") + { + if (senderVM.IsInEditMode()) + { + // make sure this is the only VM in edit mode + for (const auto& kbdVM : _KeyBindingList) + { + if (senderVM != kbdVM && kbdVM.IsInEditMode()) + { + kbdVM.ToggleEditMode(); + } + } + } + else { - continue; + // Focus on the list view item + KeyBindingsListView().ContainerFromItem(senderVM).as().Focus(FocusState::Programmatic); } - keyBindingList.push_back(command); } - std::sort(begin(keyBindingList), end(keyBindingList), CommandComparator{}); - _filteredActions = single_threaded_observable_vector(std::move(keyBindingList)); } - Collections::IObservableVector Actions::FilteredActions() + void Actions::_ViewModelDeleteKeyBindingHandler(const Editor::KeyBindingViewModel& /*senderVM*/, const Control::KeyChord& keys) { - return _filteredActions; + // Update the settings model + _State.Settings().ActionMap().DeleteKeyBinding(keys); + + // Find the current container in our list and remove it. + // This is much faster than rebuilding the entire ActionMap. + if (const auto index{ _GetContainerIndexByKeyChord(keys) }) + { + _KeyBindingList.RemoveAt(*index); + + // Focus the new item at this index + if (_KeyBindingList.Size() != 0) + { + const auto newFocusedIndex{ std::clamp(*index, 0u, _KeyBindingList.Size() - 1) }; + KeyBindingsListView().ContainerFromIndex(newFocusedIndex).as().Focus(FocusState::Programmatic); + } + } } - void Actions::_OpenSettingsClick(const IInspectable& /*sender*/, - const Windows::UI::Xaml::RoutedEventArgs& /*eventArgs*/) + void Actions::_ViewModelRebindKeysHandler(const Editor::KeyBindingViewModel& senderVM, const Editor::RebindKeysEventArgs& args) { - const CoreWindow window = CoreWindow::GetForCurrentThread(); - const auto rAltState = window.GetKeyState(VirtualKey::RightMenu); - const auto lAltState = window.GetKeyState(VirtualKey::LeftMenu); - const bool altPressed = WI_IsFlagSet(lAltState, CoreVirtualKeyStates::Down) || - WI_IsFlagSet(rAltState, CoreVirtualKeyStates::Down); + if (args.OldKeys().Modifiers() != args.NewKeys().Modifiers() || args.OldKeys().Vkey() != args.NewKeys().Vkey()) + { + // We're actually changing the key chord + const auto senderVMImpl{ get_self(senderVM) }; + const auto& conflictingCmd{ _State.Settings().ActionMap().GetActionByKeyChord(args.NewKeys()) }; + if (conflictingCmd) + { + // We're about to overwrite another key chord. + // Display a confirmation dialog. + const auto& conflictingCmdName{ conflictingCmd.Name() }; + if (!conflictingCmdName.empty()) + { + TextBlock errorMessageTB{}; + errorMessageTB.Text(RS_(L"Actions_RenameConflictConfirmationMessage")); + + TextBlock conflictingCommandNameTB{}; + conflictingCommandNameTB.Text(fmt::format(L"\"{}\"", conflictingCmdName)); + + TextBlock confirmationQuestionTB{}; + confirmationQuestionTB.Text(RS_(L"Actions_RenameConflictConfirmationQuestion")); + + Button acceptBtn{}; + acceptBtn.Content(box_value(RS_(L"Actions_RenameConflictConfirmationAcceptButton"))); + acceptBtn.Click([=](auto&, auto&) { + // remove conflicting key binding from list view + const auto containerIndex{ _GetContainerIndexByKeyChord(args.NewKeys()) }; + _KeyBindingList.RemoveAt(*containerIndex); + + // remove flyout + senderVM.AcceptChangesFlyout().Hide(); + senderVM.AcceptChangesFlyout(nullptr); + + // update settings model and view model + _State.Settings().ActionMap().RebindKeys(args.OldKeys(), args.NewKeys()); + senderVMImpl->Keys(args.NewKeys()); + senderVM.ToggleEditMode(); + }); - const auto target = altPressed ? SettingsTarget::DefaultsFile : SettingsTarget::SettingsFile; + StackPanel flyoutStack{}; + flyoutStack.Children().Append(errorMessageTB); + flyoutStack.Children().Append(conflictingCommandNameTB); + flyoutStack.Children().Append(confirmationQuestionTB); + flyoutStack.Children().Append(acceptBtn); - _State.RequestOpenJson(target); + Flyout acceptChangesFlyout{}; + acceptChangesFlyout.Content(flyoutStack); + senderVM.AcceptChangesFlyout(acceptChangesFlyout); + return; + } + else + { + // TODO CARLOS: this command doesn't have a name. Not sure what to display here. + } + } + else + { + // update settings model + _State.Settings().ActionMap().RebindKeys(args.OldKeys(), args.NewKeys()); + + // update view model (keys) + senderVMImpl->Keys(args.NewKeys()); + } + } + + // update view model (exit edit mode) + senderVM.ToggleEditMode(); } + // Method Desctiption: + // - performs a search on KeyBindingList by key chord. + // Arguments: + // - keys - the associated key chord of the command we're looking for + // Return Value: + // - the index of the view model referencing the command. If the command doesn't exist, nullopt + std::optional Actions::_GetContainerIndexByKeyChord(const Control::KeyChord& keys) + { + for (uint32_t i = 0; i < _KeyBindingList.Size(); ++i) + { + const auto kbdVM{ get_self(_KeyBindingList.GetAt(i)) }; + const auto& otherKeys{ kbdVM->Keys() }; + if (keys.Modifiers() == otherKeys.Modifiers() && keys.Vkey() == otherKeys.Vkey()) + { + return i; + } + } + + // TODO CARLOS: + // an expedited search can be done if we use cmd.Name() + // to quickly search through the sorted list. + return std::nullopt; + } } diff --git a/src/cascadia/TerminalSettingsEditor/Actions.h b/src/cascadia/TerminalSettingsEditor/Actions.h index e49c1aebc3f..8abe05b5993 100644 --- a/src/cascadia/TerminalSettingsEditor/Actions.h +++ b/src/cascadia/TerminalSettingsEditor/Actions.h @@ -4,32 +4,65 @@ #pragma once #include "Actions.g.h" +#include "KeyBindingViewModel.g.h" #include "ActionsPageNavigationState.g.h" +#include "RebindKeysEventArgs.g.h" #include "Utils.h" +#include "ViewModelHelpers.h" namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { - struct CommandComparator + struct KeyBindingViewModelComparator { - bool operator()(const Model::Command& lhs, const Model::Command& rhs) const + bool operator()(const Editor::KeyBindingViewModel& lhs, const Editor::KeyBindingViewModel& rhs) const { return lhs.Name() < rhs.Name(); } }; + struct RebindKeysEventArgs : RebindKeysEventArgsT + { + public: + RebindKeysEventArgs(const Control::KeyChord& oldKeys, const Control::KeyChord& newKeys) : + _OldKeys{ oldKeys }, + _NewKeys{ newKeys } {} + + WINRT_PROPERTY(Control::KeyChord, OldKeys, nullptr); + WINRT_PROPERTY(Control::KeyChord, NewKeys, nullptr); + }; + + struct KeyBindingViewModel : KeyBindingViewModelT, ViewModelHelper + { + public: + KeyBindingViewModel(const Control::KeyChord& keys, const Settings::Model::Command& cmd); + + hstring Name() const { return _Command.Name(); } + hstring KeyChordText() const { return _KeyChordText; } + + void ToggleEditMode(); + void DisableEditMode() { IsInEditMode(false); } + void AttemptAcceptChanges(); + void DeleteKeyBinding() { _DeleteKeyBindingRequestedHandlers(*this, _Keys); } + + VIEW_MODEL_OBSERVABLE_PROPERTY(bool, IsInEditMode, false); + VIEW_MODEL_OBSERVABLE_PROPERTY(hstring, ProposedKeys); + VIEW_MODEL_OBSERVABLE_PROPERTY(Control::KeyChord, Keys, nullptr); + VIEW_MODEL_OBSERVABLE_PROPERTY(Windows::UI::Xaml::Controls::Flyout, AcceptChangesFlyout, nullptr); + TYPED_EVENT(RebindKeysRequested, Editor::KeyBindingViewModel, Editor::RebindKeysEventArgs); + TYPED_EVENT(DeleteKeyBindingRequested, Editor::KeyBindingViewModel, Terminal::Control::KeyChord); + + private: + Settings::Model::Command _Command{ nullptr }; + hstring _KeyChordText {}; + }; + struct ActionsPageNavigationState : ActionsPageNavigationStateT { public: ActionsPageNavigationState(const Model::CascadiaSettings& settings) : _Settings{ settings } {} - void RequestOpenJson(const Model::SettingsTarget target) - { - _OpenJsonHandlers(nullptr, target); - } - WINRT_PROPERTY(Model::CascadiaSettings, Settings, nullptr) - TYPED_EVENT(OpenJson, Windows::Foundation::IInspectable, Model::SettingsTarget); }; struct Actions : ActionsT @@ -39,15 +72,16 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation void OnNavigatedTo(const winrt::Windows::UI::Xaml::Navigation::NavigationEventArgs& e); - Windows::Foundation::Collections::IObservableVector FilteredActions(); - + WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler); WINRT_PROPERTY(Editor::ActionsPageNavigationState, State, nullptr); + WINRT_PROPERTY(Windows::Foundation::Collections::IObservableVector, KeyBindingList); private: - friend struct ActionsT; // for Xaml to bind events - Windows::Foundation::Collections::IObservableVector _filteredActions{ nullptr }; + void _ViewModelPropertyChangedHandler(const Windows::Foundation::IInspectable& senderVM, const Windows::UI::Xaml::Data::PropertyChangedEventArgs& args); + void _ViewModelDeleteKeyBindingHandler(const Editor::KeyBindingViewModel& senderVM, const Control::KeyChord& args); + void _ViewModelRebindKeysHandler(const Editor::KeyBindingViewModel& senderVM, const Editor::RebindKeysEventArgs& args); - void _OpenSettingsClick(const IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& eventArgs); + std::optional _GetContainerIndexByKeyChord(const Control::KeyChord& keys); }; } diff --git a/src/cascadia/TerminalSettingsEditor/Actions.idl b/src/cascadia/TerminalSettingsEditor/Actions.idl index 2b1ea22aab5..65c51d643a6 100644 --- a/src/cascadia/TerminalSettingsEditor/Actions.idl +++ b/src/cascadia/TerminalSettingsEditor/Actions.idl @@ -5,11 +5,33 @@ import "EnumEntry.idl"; namespace Microsoft.Terminal.Settings.Editor { + runtimeclass RebindKeysEventArgs + { + Microsoft.Terminal.Control.KeyChord OldKeys { get; }; + Microsoft.Terminal.Control.KeyChord NewKeys { get; }; + } + + runtimeclass KeyBindingViewModel : Windows.UI.Xaml.Data.INotifyPropertyChanged + { + // Settings Model side + String Name { get; }; + String KeyChordText { get; }; + + // UI side + Boolean IsInEditMode { get; }; + String ProposedKeys; + Windows.UI.Xaml.Controls.Flyout AcceptChangesFlyout; + void ToggleEditMode(); + void AttemptAcceptChanges(); + void DeleteKeyBinding(); + + event Windows.Foundation.TypedEventHandler RebindKeysRequested; + event Windows.Foundation.TypedEventHandler DeleteKeyBindingRequested; + } + runtimeclass ActionsPageNavigationState { Microsoft.Terminal.Settings.Model.CascadiaSettings Settings; - void RequestOpenJson(Microsoft.Terminal.Settings.Model.SettingsTarget target); - event Windows.Foundation.TypedEventHandler OpenJson; }; [default_interface] runtimeclass Actions : Windows.UI.Xaml.Controls.Page @@ -17,7 +39,6 @@ namespace Microsoft.Terminal.Settings.Editor Actions(); ActionsPageNavigationState State { get; }; - IObservableVector FilteredActions { get; }; - + IObservableVector KeyBindingList { get; }; } } diff --git a/src/cascadia/TerminalSettingsEditor/Actions.xaml b/src/cascadia/TerminalSettingsEditor/Actions.xaml index 3167229a987..6cde042d473 100644 --- a/src/cascadia/TerminalSettingsEditor/Actions.xaml +++ b/src/cascadia/TerminalSettingsEditor/Actions.xaml @@ -5,11 +5,11 @@ @@ -18,69 +18,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -166,6 +104,7 @@ @@ -181,43 +120,185 @@ + + - - + + + - - - + + - + + - + + + + + + + + + + - + + + + + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/cascadia/TerminalSettingsEditor/CommonResources.xaml b/src/cascadia/TerminalSettingsEditor/CommonResources.xaml index 741c2d4a600..eed98eaa17a 100644 --- a/src/cascadia/TerminalSettingsEditor/CommonResources.xaml +++ b/src/cascadia/TerminalSettingsEditor/CommonResources.xaml @@ -29,7 +29,7 @@ diff --git a/src/cascadia/TerminalSettingsEditor/MainPage.cpp b/src/cascadia/TerminalSettingsEditor/MainPage.cpp index 99f692fcff7..08b93b45eda 100644 --- a/src/cascadia/TerminalSettingsEditor/MainPage.cpp +++ b/src/cascadia/TerminalSettingsEditor/MainPage.cpp @@ -264,14 +264,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation } else if (clickedItemTag == actionsTag) { - auto actionsState{ winrt::make(_settingsClone) }; - actionsState.OpenJson([weakThis = get_weak()](auto&&, auto&& arg) { - if (auto self{ weakThis.get() }) - { - self->_OpenJsonHandlers(nullptr, arg); - } - }); - contentFrame().Navigate(xaml_typename(), actionsState); + contentFrame().Navigate(xaml_typename(), winrt::make(_settingsClone)); } else if (clickedItemTag == colorSchemesTag) { diff --git a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw index 505d56fbc20..88a5bc0a8c4 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw @@ -1065,4 +1065,22 @@ If checked, show all installed fonts in the list above. Otherwise, only show the list of monospace fonts. A description for what the supplementary "show all fonts" setting does. Presented near "Profile_FontFaceShowAllFonts". + + Yes, delete key binding + + + Are you sure you want to delete this key binding? + + + Invalid key chord. Please enter a valid key chord. + + + Yes + + + The provided key chord is already being used by the following action: + + + Would you like to overwrite that key binding? + \ No newline at end of file diff --git a/src/cascadia/TerminalSettingsEditor/SettingContainer.h b/src/cascadia/TerminalSettingsEditor/SettingContainer.h index bdca684b6d2..490792c99d9 100644 --- a/src/cascadia/TerminalSettingsEditor/SettingContainer.h +++ b/src/cascadia/TerminalSettingsEditor/SettingContainer.h @@ -20,27 +20,6 @@ Author(s): #include "SettingContainer.g.h" #include "Utils.h" -// This macro defines a dependency property for a WinRT class. -// Use this in your class' header file after declaring it in the idl. -// Remember to register your dependency property in the respective cpp file. -#define DEPENDENCY_PROPERTY(type, name) \ -public: \ - static winrt::Windows::UI::Xaml::DependencyProperty name##Property() \ - { \ - return _##name##Property; \ - } \ - type name() const \ - { \ - return winrt::unbox_value(GetValue(_##name##Property)); \ - } \ - void name(type const& value) \ - { \ - SetValue(_##name##Property, winrt::box_value(value)); \ - } \ - \ -private: \ - static winrt::Windows::UI::Xaml::DependencyProperty _##name##Property; - namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { struct SettingContainer : SettingContainerT diff --git a/src/cascadia/TerminalSettingsEditor/Utils.h b/src/cascadia/TerminalSettingsEditor/Utils.h index cac11180674..0d1804bd534 100644 --- a/src/cascadia/TerminalSettingsEditor/Utils.h +++ b/src/cascadia/TerminalSettingsEditor/Utils.h @@ -70,6 +70,27 @@ private: winrt::Windows::Foundation::Collections::IObservableVector _##name##List; \ winrt::Windows::Foundation::Collections::IMap _##name##Map; +// This macro defines a dependency property for a WinRT class. +// Use this in your class' header file after declaring it in the idl. +// Remember to register your dependency property in the respective cpp file. +#define DEPENDENCY_PROPERTY(type, name) \ +public: \ + static winrt::Windows::UI::Xaml::DependencyProperty name##Property() \ + { \ + return _##name##Property; \ + } \ + type name() const \ + { \ + return winrt::unbox_value(GetValue(_##name##Property)); \ + } \ + void name(type const& value) \ + { \ + SetValue(_##name##Property, winrt::box_value(value)); \ + } \ + \ +private: \ + static winrt::Windows::UI::Xaml::DependencyProperty _##name##Property; + namespace winrt::Microsoft::Terminal::Settings { winrt::hstring GetSelectedItemTag(winrt::Windows::Foundation::IInspectable const& comboBoxAsInspectable); diff --git a/src/cascadia/TerminalSettingsEditor/ViewModelHelpers.h b/src/cascadia/TerminalSettingsEditor/ViewModelHelpers.h index 48b9ff2bd10..8994e368ec2 100644 --- a/src/cascadia/TerminalSettingsEditor/ViewModelHelpers.h +++ b/src/cascadia/TerminalSettingsEditor/ViewModelHelpers.h @@ -72,3 +72,20 @@ public: \ // setting, but which cannot be erased. #define PERMANENT_OBSERVABLE_PROJECTED_SETTING(target, name) \ _BASE_OBSERVABLE_PROJECTED_SETTING(target, name) + +// Defines a basic observable property that uses the _NotifyChanges +// system from ViewModelHelper. +#define VIEW_MODEL_OBSERVABLE_PROPERTY(type, name, ...) \ +public: \ + type name() const noexcept { return _##name; }; \ + void name(const type& value) \ + { \ + if (_##name != value) \ + { \ + _##name = value; \ + _NotifyChanges(L#name); \ + } \ + }; \ + \ +private: \ + type _##name{ __VA_ARGS__ }; diff --git a/src/cascadia/TerminalSettingsModel/ActionMap.cpp b/src/cascadia/TerminalSettingsModel/ActionMap.cpp index d8acfa2e3d7..0ded6ba1c04 100644 --- a/src/cascadia/TerminalSettingsModel/ActionMap.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionMap.cpp @@ -135,6 +135,49 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation } } + Windows::Foundation::Collections::IMapView ActionMap::KeyBindings() + { + if (!_KeyBindingMapCache) + { + // populate _KeyBindingMapCache + std::unordered_map keyBindingsMap{}; + _PopulateKeyBindingMapWithStandardCommands(keyBindingsMap); + + _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. + void ActionMap::_PopulateKeyBindingMapWithStandardCommands(std::unordered_map& keyBindingsMap) const + { + // Update KeyBindingsMap with our current layer + for (const auto& [_, cmd] : _ActionMap) + { + // iterate over all of the commands' bound keys + const auto cmdImpl{ get_self(cmd) }; + for (const auto& keys : cmdImpl->KeyMappings()) + { + // Only populate KeyBindingsMap with actions that haven't been visited already. + if (keyBindingsMap.find(keys) == keyBindingsMap.end()) + { + keyBindingsMap.insert({ keys, cmd }); + } + } + } + + // Update KeyBindingsMap and visitedKeyChords with our parents + for (const auto& parent : _parents) + { + parent->_PopulateKeyBindingMapWithStandardCommands(keyBindingsMap); + } + } + com_ptr ActionMap::Copy() const { auto actionMap{ make_self() }; @@ -177,6 +220,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation return; } + // Reset the caches because they are no longer valid + _NameMapCache = nullptr; + _KeyBindingMapCache = nullptr; + // Handle nested commands const auto cmdImpl{ get_self(cmd) }; if (cmdImpl->IsNestedCommand()) @@ -367,4 +414,36 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // This key binding does not exist return nullptr; } + + void 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. + throw hresult_invalid_argument(); + } + + if (newKeys) + { + // Bind newKeys + const auto newCmd{ make_self() }; + newCmd->ActionAndArgs(cmd.ActionAndArgs()); + newCmd->RegisterKey(newKeys); + AddAction(*newCmd); + } + + // unbind oldKeys + DeleteKeyBinding(oldKeys); + } + + 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); + } } diff --git a/src/cascadia/TerminalSettingsModel/ActionMap.h b/src/cascadia/TerminalSettingsModel/ActionMap.h index 917abec5428..8825bf534c6 100644 --- a/src/cascadia/TerminalSettingsModel/ActionMap.h +++ b/src/cascadia/TerminalSettingsModel/ActionMap.h @@ -75,6 +75,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // views Windows::Foundation::Collections::IMapView NameMap(); + Windows::Foundation::Collections::IMapView KeyBindings(); com_ptr Copy() const; // queries @@ -86,6 +87,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation void AddAction(const Model::Command& cmd); std::vector LayerJson(const Json::Value& json); + // modification + void RebindKeys(Control::KeyChord const& oldKeys, Control::KeyChord const& newKeys); + void DeleteKeyBinding(Control::KeyChord const& keys); + static Windows::System::VirtualKeyModifiers ConvertVKModifiers(Control::KeyModifiers modifiers); private: @@ -94,8 +99,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation void _PopulateNameMapWithNestedCommands(std::unordered_map& nameMap) const; void _PopulateNameMapWithStandardCommands(std::unordered_map& nameMap, std::set& visitedActionIDs) const; + void _PopulateKeyBindingMapWithStandardCommands(std::unordered_map& keyBindingsMap) const; Windows::Foundation::Collections::IMap _NameMapCache{ nullptr }; + Windows::Foundation::Collections::IMap _KeyBindingMapCache{ nullptr }; Windows::Foundation::Collections::IMap _NestedCommands{ nullptr }; std::unordered_map _KeyMap; std::unordered_map _ActionMap; diff --git a/src/cascadia/TerminalSettingsModel/ActionMap.idl b/src/cascadia/TerminalSettingsModel/ActionMap.idl index f4a75d04ffa..5f5c23c0b15 100644 --- a/src/cascadia/TerminalSettingsModel/ActionMap.idl +++ b/src/cascadia/TerminalSettingsModel/ActionMap.idl @@ -11,6 +11,10 @@ namespace Microsoft.Terminal.Settings.Model Microsoft.Terminal.Control.KeyChord GetKeyBindingForAction(ShortcutAction action); Microsoft.Terminal.Control.KeyChord GetKeyBindingForAction(ShortcutAction action, IActionArgs actionArgs); + void RebindKeys(Microsoft.Terminal.Control.KeyChord oldKeys, Microsoft.Terminal.Control.KeyChord newKeys); + void DeleteKeyBinding(Microsoft.Terminal.Control.KeyChord keys); + Windows.Foundation.Collections.IMapView NameMap { get; }; + Windows.Foundation.Collections.IMapView KeyBindings { get; }; } } diff --git a/src/cascadia/TerminalSettingsModel/Command.cpp b/src/cascadia/TerminalSettingsModel/Command.cpp index f16188c8b4f..6ae3a376928 100644 --- a/src/cascadia/TerminalSettingsModel/Command.cpp +++ b/src/cascadia/TerminalSettingsModel/Command.cpp @@ -42,11 +42,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { auto command{ winrt::make_self() }; command->_name = _name; - - // TODO GH#6900: We probably want ActionAndArgs::Copy here - // This is fine for now because SUI can't actually - // modify the copy yet. - command->_ActionAndArgs = _ActionAndArgs; + command->_ActionAndArgs = *(get_self(_ActionAndArgs)->Copy()); for (const auto& keys : _keyMappings) { command->_keyMappings.emplace_back(keys.Modifiers(), keys.Vkey());