diff --git a/src/cascadia/TerminalSettingsEditor/Actions.cpp b/src/cascadia/TerminalSettingsEditor/Actions.cpp index bd9403cece6..49260547a32 100644 --- a/src/cascadia/TerminalSettingsEditor/Actions.cpp +++ b/src/cascadia/TerminalSettingsEditor/Actions.cpp @@ -4,61 +4,313 @@ #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 Model::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"); + } + else if (viewModelProperty == L"IsContainerFocused" || + viewModelProperty == L"IsEditButtonFocused" || + viewModelProperty == L"IsHovered" || + viewModelProperty == L"IsAutomationPeerAttached" || + viewModelProperty == L"IsInEditMode") + { + _NotifyChanges(L"ShowEditButton"); + } + }); + } + + hstring KeyBindingViewModel::EditButtonName() const noexcept { return RS_(L"Actions_EditButton/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip"); } + hstring KeyBindingViewModel::CancelButtonName() const noexcept { return RS_(L"Actions_CancelButton/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip"); } + hstring KeyBindingViewModel::AcceptButtonName() const noexcept { return RS_(L"Actions_AcceptButton/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip"); } + hstring KeyBindingViewModel::DeleteButtonName() const noexcept { return RS_(L"Actions_DeleteButton/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip"); } + + bool KeyBindingViewModel::ShowEditButton() const noexcept + { + return (IsContainerFocused() || IsEditButtonFocused() || IsHovered() || IsAutomationPeerAttached()) && !IsInEditMode(); + } + + 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()); + } + } + + void KeyBindingViewModel::AttemptAcceptChanges() + { + AttemptAcceptChanges(_ProposedKeys); + } + + void KeyBindingViewModel::AttemptAcceptChanges(hstring newKeyChordText) + { + auto args{ make_self(_Keys, _Keys) }; + try + { + // Attempt to convert the provided key chord text + const auto newKeyChord{ KeyChordSerialization::FromString(newKeyChordText) }; + args->NewKeys(newKeyChord); + _RebindKeysRequestedHandlers(*this, *args); + } + catch (hresult_invalid_argument) + { + // Converting the text into a key chord failed + // TODO GH #6900: + // 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. + // Alternatively, we want a full key chord editor/listener. + // If we implement that, we won't need this validation or error message. + } + } + Actions::Actions() { InitializeComponent(); + } - _filteredActions = winrt::single_threaded_observable_vector(); + Automation::Peers::AutomationPeer Actions::OnCreateAutomationPeer() + { + for (const auto& kbdVM : _KeyBindingList) + { + // To create a more accessible experience, we want the "edit" buttons to _always_ + // appear when a screen reader is attached. This ensures that the edit buttons are + // accessible via the UIA tree. + get_self(kbdVM)->IsAutomationPeerAttached(_AutomationPeerAttached); + } + return nullptr; } 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) + { + auto container{ make_self(keys, cmd) }; + container->PropertyChanged({ this, &Actions::_ViewModelPropertyChangedHandler }); + container->DeleteKeyBindingRequested({ this, &Actions::_ViewModelDeleteKeyBindingHandler }); + container->RebindKeysRequested({ this, &Actions::_ViewModelRebindKeysHandler }); + container->IsAutomationPeerAttached(_AutomationPeerAttached); + keyBindingList.push_back(*container); + } + + std::sort(begin(keyBindingList), end(keyBindingList), KeyBindingViewModelComparator{}); + _KeyBindingList = single_threaded_observable_vector(std::move(keyBindingList)); + } + + void Actions::KeyChordEditor_PreviewKeyDown(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e) + { + const auto& senderTB{ sender.as() }; + const auto& kbdVM{ senderTB.DataContext().as() }; + if (e.OriginalKey() == VirtualKey::Enter) + { + // Fun fact: this is happening _before_ "_ProposedKeys" gets updated + // with the two-way data binding. So we need to directly extract the text + // and tell the view model to update itself. + get_self(kbdVM)->AttemptAcceptChanges(senderTB.Text()); + + // For an unknown reason, when 'AcceptChangesFlyout' is set in the code above, + // the flyout isn't shown, forcing the 'Enter' key to do nothing. + // To get around this, detect if the flyout was set, and display it + // on the text box. + if (kbdVM.AcceptChangesFlyout() != nullptr) + { + kbdVM.AcceptChangesFlyout().ShowAt(senderTB); + } + e.Handled(true); + } + else if (e.OriginalKey() == VirtualKey::Escape) + { + kbdVM.ToggleEditMode(); + e.Handled(true); + } + } + + 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") { - // 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()) + if (senderVM.IsInEditMode()) { - continue; + // Ensure that... + // 1. we move focus to the edit mode controls + // 2. this is the only entry that is in edit mode + for (uint32_t i = 0; i < _KeyBindingList.Size(); ++i) + { + const auto& kbdVM{ _KeyBindingList.GetAt(i) }; + if (senderVM == kbdVM) + { + // This is the view model entry that went into edit mode. + // Move focus to the edit mode controls by + // extracting the list view item container. + const auto& container{ KeyBindingsListView().ContainerFromIndex(i).try_as() }; + container.Focus(FocusState::Programmatic); + } + else + { + // Exit edit mode for all other containers + get_self(kbdVM)->DisableEditMode(); + } + } + + const auto& containerBackground{ Resources().Lookup(box_value(L"EditModeContainerBackground")).as() }; + get_self(senderVM)->ContainerBackground(containerBackground); + } + else + { + // Focus on the list view item + KeyBindingsListView().ContainerFromItem(senderVM).as().Focus(FocusState::Programmatic); + + const auto& containerBackground{ Resources().Lookup(box_value(L"NonEditModeContainerBackground")).as() }; + get_self(senderVM)->ContainerBackground(containerBackground); } - 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. + TextBlock errorMessageTB{}; + errorMessageTB.Text(RS_(L"Actions_RenameConflictConfirmationMessage")); + + const auto conflictingCmdName{ conflictingCmd.Name() }; + TextBlock conflictingCommandNameTB{}; + conflictingCommandNameTB.Text(fmt::format(L"\"{}\"", conflictingCmdName.empty() ? RS_(L"Actions_UnnamedCommandName") : conflictingCmdName)); + conflictingCommandNameTB.FontStyle(Windows::UI::Text::FontStyle::Italic); + + 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); - const auto target = altPressed ? SettingsTarget::DefaultsFile : SettingsTarget::SettingsFile; + // update settings model and view model + _State.Settings().ActionMap().RebindKeys(args.OldKeys(), args.NewKeys()); + senderVMImpl->Keys(args.NewKeys()); + senderVM.ToggleEditMode(); + }); - _State.RequestOpenJson(target); + StackPanel flyoutStack{}; + flyoutStack.Children().Append(errorMessageTB); + flyoutStack.Children().Append(conflictingCommandNameTB); + flyoutStack.Children().Append(confirmationQuestionTB); + flyoutStack.Children().Append(acceptBTN); + + Flyout acceptChangesFlyout{}; + acceptChangesFlyout.Content(flyoutStack); + senderVM.AcceptChangesFlyout(acceptChangesFlyout); + return; + } + 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 Description: + // - 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 GH #6900: + // 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..0fe5898ec4f 100644 --- a/src/cascadia/TerminalSettingsEditor/Actions.h +++ b/src/cascadia/TerminalSettingsEditor/Actions.h @@ -4,32 +4,85 @@ #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; } + Settings::Model::Command Command() const { return _Command; }; + + // UIA Text + hstring EditButtonName() const noexcept; + hstring CancelButtonName() const noexcept; + hstring AcceptButtonName() const noexcept; + hstring DeleteButtonName() const noexcept; + + void EnterHoverMode() { IsHovered(true); }; + void ExitHoverMode() { IsHovered(false); }; + void FocusContainer() { IsContainerFocused(true); }; + void UnfocusContainer() { IsContainerFocused(false); }; + void FocusEditButton() { IsEditButtonFocused(true); }; + void UnfocusEditButton() { IsEditButtonFocused(false); }; + bool ShowEditButton() const noexcept; + void ToggleEditMode(); + void DisableEditMode() { IsInEditMode(false); } + void AttemptAcceptChanges(); + void AttemptAcceptChanges(hstring newKeyChordText); + 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); + VIEW_MODEL_OBSERVABLE_PROPERTY(bool, IsAutomationPeerAttached, false); + VIEW_MODEL_OBSERVABLE_PROPERTY(bool, IsHovered, false); + VIEW_MODEL_OBSERVABLE_PROPERTY(bool, IsContainerFocused, false); + VIEW_MODEL_OBSERVABLE_PROPERTY(bool, IsEditButtonFocused, false); + VIEW_MODEL_OBSERVABLE_PROPERTY(Windows::UI::Xaml::Media::Brush, ContainerBackground, 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 @@ -38,16 +91,21 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation Actions(); void OnNavigatedTo(const winrt::Windows::UI::Xaml::Navigation::NavigationEventArgs& e); + Windows::UI::Xaml::Automation::Peers::AutomationPeer OnCreateAutomationPeer(); + void KeyChordEditor_PreviewKeyDown(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::KeyRoutedEventArgs const& 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); + + std::optional _GetContainerIndexByKeyChord(const Control::KeyChord& keys); - void _OpenSettingsClick(const IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& eventArgs); + bool _AutomationPeerAttached{ false }; }; } diff --git a/src/cascadia/TerminalSettingsEditor/Actions.idl b/src/cascadia/TerminalSettingsEditor/Actions.idl index 2b1ea22aab5..950ce7dbbb8 100644 --- a/src/cascadia/TerminalSettingsEditor/Actions.idl +++ b/src/cascadia/TerminalSettingsEditor/Actions.idl @@ -5,11 +5,46 @@ 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 ShowEditButton { get; }; + Boolean IsInEditMode { get; }; + String ProposedKeys; + Windows.UI.Xaml.Controls.Flyout AcceptChangesFlyout; + String EditButtonName { get; }; + String CancelButtonName { get; }; + String AcceptButtonName { get; }; + String DeleteButtonName { get; }; + Windows.UI.Xaml.Media.Brush ContainerBackground { get; }; + + void EnterHoverMode(); + void ExitHoverMode(); + void FocusContainer(); + void UnfocusContainer(); + void FocusEditButton(); + void UnfocusEditButton(); + 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 +52,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..70f13deb7ad 100644 --- a/src/cascadia/TerminalSettingsEditor/Actions.xaml +++ b/src/cascadia/TerminalSettingsEditor/Actions.xaml @@ -5,10 +5,10 @@ @@ -18,69 +18,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -181,44 +119,271 @@ + + + + + 32 + 15 + + - - + + + + + - - - + + + + + + + + + + + + + + + + - + + - + + - + + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + 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 a7ba72e6094..30704ed3486 100644 --- a/src/cascadia/TerminalSettingsEditor/MainPage.cpp +++ b/src/cascadia/TerminalSettingsEditor/MainPage.cpp @@ -286,14 +286,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 f069235a98d..e9f68ea2ad8 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw @@ -1091,4 +1091,48 @@ 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 + Button label that confirms deletion of a key binding entry. + + + Are you sure you want to delete this key binding? + Confirmation message displayed when the user attempts to delete a key binding entry. + + + Invalid key chord. Please enter a valid key chord. + Error message displayed when an invalid key chord is input by the user. + + + Yes + Button label that confirms the deletion of a conflicting key binding to allow the current key binding to be registered. + + + The provided key chord is already being used by the following action: + Error message displayed when a key chord that is already in use is input by the user. The name of the conflicting key chord is displayed after this message. + + + Would you like to overwrite that key binding? + Confirmation message displayed when a key chord that is already in use is input by the user. This is intended to ask the user if they wish to delete the conflicting key binding, and assign the current key chord (or binding) instead. + + + <unnamed command> + {Locked="<"} {Locked=">"} The text shown when referring to a command that is unnamed. + + + Accept + Text label for a button that can be used to accept changes to a key binding entry. + + + Cancel + Text label for a button that can be used to cancel changes to a key binding entry + + + Delete + Text label for a button that can be used to delete a key binding entry. + + + Edit + Text label for a button that can be used to begin making changes to a key binding entry. + + \ 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 a2507e24f9c..63fffa7011a 100644 --- a/src/cascadia/TerminalSettingsModel/ActionMap.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionMap.cpp @@ -193,32 +193,14 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { if (!_GlobalHotkeysCache) { - std::unordered_set visitedActionIDs{}; std::unordered_map globalHotkeys; - for (const auto& cmd : _GetCumulativeActions()) + for (const auto& [keys, cmd] : KeyBindings()) { - // Only populate GlobalHotkeys with actions that... - // (1) ShortcutAction is GlobalSummon or QuakeMode - const auto& actionAndArgs{ cmd.ActionAndArgs() }; - if (actionAndArgs.Action() == ShortcutAction::GlobalSummon || actionAndArgs.Action() == ShortcutAction::QuakeMode) + // Only populate GlobalHotkeys with actions whose + // ShortcutAction is GlobalSummon or QuakeMode + if (cmd.ActionAndArgs().Action() == ShortcutAction::GlobalSummon || cmd.ActionAndArgs().Action() == ShortcutAction::QuakeMode) { - // (2) haven't been visited already - const auto actionID{ Hash(actionAndArgs) }; - if (visitedActionIDs.find(actionID) == visitedActionIDs.end()) - { - const auto& cmdImpl{ get_self(cmd) }; - for (const auto& keys : cmdImpl->KeyMappings()) - { - // (3) haven't had that key chord added yet - if (globalHotkeys.find(keys) == globalHotkeys.end()) - { - globalHotkeys.emplace(keys, cmd); - } - } - - // Record that we already handled adding this action to the NameMap. - visitedActionIDs.emplace(actionID); - } + globalHotkeys.emplace(keys, cmd); } } _GlobalHotkeysCache = single_threaded_map(std::move(globalHotkeys)); @@ -226,6 +208,64 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation 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) + { + 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 visitedKeyChords with our parents + FAIL_FAST_IF(_parents.size() > 1); + for (const auto& parent : _parents) + { + parent->_PopulateKeyBindingMapWithStandardCommands(keyBindingsMap, unboundKeys); + } + } + com_ptr ActionMap::Copy() const { auto actionMap{ make_self() }; @@ -276,6 +316,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // invalidate caches _NameMapCache = nullptr; _GlobalHotkeysCache = nullptr; + _KeyBindingMapCache = nullptr; // Handle nested commands const auto cmdImpl{ get_self(cmd) }; @@ -628,4 +669,50 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // 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); + } } diff --git a/src/cascadia/TerminalSettingsModel/ActionMap.h b/src/cascadia/TerminalSettingsModel/ActionMap.h index b968712ee42..9aef9db5110 100644 --- a/src/cascadia/TerminalSettingsModel/ActionMap.h +++ b/src/cascadia/TerminalSettingsModel/ActionMap.h @@ -55,6 +55,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // views Windows::Foundation::Collections::IMapView NameMap(); Windows::Foundation::Collections::IMapView GlobalHotkeys(); + Windows::Foundation::Collections::IMapView KeyBindings(); com_ptr Copy() const; // queries @@ -66,6 +67,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation void AddAction(const Model::Command& cmd); std::vector LayerJson(const Json::Value& json); + // modification + bool RebindKeys(Control::KeyChord const& oldKeys, Control::KeyChord const& newKeys); + void DeleteKeyBinding(Control::KeyChord const& keys); + static Windows::System::VirtualKeyModifiers ConvertVKModifiers(Control::KeyModifiers modifiers); private: @@ -74,6 +79,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation void _PopulateNameMapWithNestedCommands(std::unordered_map& nameMap) const; void _PopulateNameMapWithStandardCommands(std::unordered_map& nameMap) const; + void _PopulateKeyBindingMapWithStandardCommands(std::unordered_map& keyBindingsMap, std::unordered_set& unboundKeys) const; std::vector _GetCumulativeActions() const noexcept; void _TryUpdateActionMap(const Model::Command& cmd, Model::Command& oldCmd, Model::Command& consolidatedCmd); @@ -82,6 +88,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation Windows::Foundation::Collections::IMap _NameMapCache{ nullptr }; Windows::Foundation::Collections::IMap _GlobalHotkeysCache{ 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 11022ecbc9b..461f6b36f46 100644 --- a/src/cascadia/TerminalSettingsModel/ActionMap.idl +++ b/src/cascadia/TerminalSettingsModel/ActionMap.idl @@ -14,10 +14,13 @@ namespace Microsoft.Terminal.Settings.Model [method_name("GetKeyBindingForActionWithArgs")] Microsoft.Terminal.Control.KeyChord GetKeyBindingForAction(ShortcutAction action, IActionArgs actionArgs); Windows.Foundation.Collections.IMapView NameMap { get; }; - Windows.Foundation.Collections.IMapView GlobalHotkeys(); + Windows.Foundation.Collections.IMapView KeyBindings { get; }; + Windows.Foundation.Collections.IMapView GlobalHotkeys { get; }; }; [default_interface] runtimeclass ActionMap : IActionMapView { + Boolean RebindKeys(Microsoft.Terminal.Control.KeyChord oldKeys, Microsoft.Terminal.Control.KeyChord newKeys); + void DeleteKeyBinding(Microsoft.Terminal.Control.KeyChord keys); } }