Make Actions page editable (#9949)

## Summary of the Pull Request

This PR lays the foundation for a new Actions page in the Settings UI as designed in #6900. The Actions page now leverages the `ActionMap` to display all of the key bindings and allow the user to modify the associated key chord or delete the key binding entirely.

## References

#9621 - ActionMap
#9926 - ActionMap serialization
#9428 - ActionMap Spec
#6900 - Actions page
#9427 - Actions page design doc

## Detailed Description of the Pull Request / Additional comments

### Settings Model Changes

- `Command::Copy()` now copies the `ActionAndArgs`
- `ActionMap::RebindKeys()` handles changing the key chord of a key binding. If a conflict occurs, the conflicting key chord is overwritten.
- `ActionMap::DeleteKeyBinding()` "deletes" a key binding by binding "unbound" to the given key chord.
- `ActionMap::KeyBindings()` presents another view (similar to `NameMap`) of the `ActionMap`. It specifically presents a map of key chords to commands. It is generated similar to how `NameMap` is generated.

### Editor Changes

- `Actions.xaml` is mainly split into two parts:
   - `ListView` (as before) holds the list of key bindings. We _could_ explore the idea of an items repeater, but the `ListView` seems to provide some niceties with regards to navigating the list via the key board (though none are selectable).
   - `DataTemplate` is used to represent each key binding inside the `ListView`. This is tricky because it is bound to a `KeyBindingViewModel` which must provide _all_ context necessary to modify the UI and the settings model. We cannot use names to target UI elements inside this template, so we must make the view model smart and force updates to the UI via changes in the view model.
- `KeyBindingViewModel` is a view model object that controls the UI and the settings model. 

There are a number of TODOs in Actions.cpp will be long-term follow-ups and would be nice to have. This includes...
- a binary search by name on `Actions::KeyBindingList`
- presenting an error when the provided key chord is invalid.

## Demo
![Actions Page Demo](https://user-images.githubusercontent.com/11050425/116034988-131d1b80-a619-11eb-8df2-c7e57c6fad86.gif)
This commit is contained in:
Carlos Zamora 2021-05-18 17:37:16 -04:00 committed by GitHub
parent 24f80bd9ba
commit c66910b685
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 853 additions and 193 deletions

View file

@ -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<RebindKeysEventArgs>(_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<Command>();
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<KeyBindingViewModel>(kbdVM)->IsAutomationPeerAttached(_AutomationPeerAttached);
}
return nullptr;
}
void Actions::OnNavigatedTo(const NavigationEventArgs& e)
{
_State = e.Parameter().as<Editor::ActionsPageNavigationState>();
std::vector<Command> 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<Editor::KeyBindingViewModel> 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())
{
continue;
}
keyBindingList.push_back(command);
auto container{ make_self<KeyBindingViewModel>(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), CommandComparator{});
_filteredActions = single_threaded_observable_vector<Command>(std::move(keyBindingList));
std::sort(begin(keyBindingList), end(keyBindingList), KeyBindingViewModelComparator{});
_KeyBindingList = single_threaded_observable_vector(std::move(keyBindingList));
}
Collections::IObservableVector<Command> Actions::FilteredActions()
void Actions::KeyChordEditor_PreviewKeyDown(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e)
{
return _filteredActions;
const auto& senderTB{ sender.as<TextBox>() };
const auto& kbdVM{ senderTB.DataContext().as<Editor::KeyBindingViewModel>() };
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<KeyBindingViewModel>(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::_OpenSettingsClick(const IInspectable& /*sender*/,
const Windows::UI::Xaml::RoutedEventArgs& /*eventArgs*/)
void Actions::_ViewModelPropertyChangedHandler(const IInspectable& sender, const Windows::UI::Xaml::Data::PropertyChangedEventArgs& 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);
const auto senderVM{ sender.as<Editor::KeyBindingViewModel>() };
const auto propertyName{ args.PropertyName() };
if (propertyName == L"IsInEditMode")
{
if (senderVM.IsInEditMode())
{
// 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<ListViewItem>() };
container.Focus(FocusState::Programmatic);
}
else
{
// Exit edit mode for all other containers
get_self<KeyBindingViewModel>(kbdVM)->DisableEditMode();
}
}
const auto target = altPressed ? SettingsTarget::DefaultsFile : SettingsTarget::SettingsFile;
const auto& containerBackground{ Resources().Lookup(box_value(L"EditModeContainerBackground")).as<Windows::UI::Xaml::Media::Brush>() };
get_self<KeyBindingViewModel>(senderVM)->ContainerBackground(containerBackground);
}
else
{
// Focus on the list view item
KeyBindingsListView().ContainerFromItem(senderVM).as<Controls::Control>().Focus(FocusState::Programmatic);
_State.RequestOpenJson(target);
const auto& containerBackground{ Resources().Lookup(box_value(L"NonEditModeContainerBackground")).as<Windows::UI::Xaml::Media::Brush>() };
get_self<KeyBindingViewModel>(senderVM)->ContainerBackground(containerBackground);
}
}
}
void Actions::_ViewModelDeleteKeyBindingHandler(const Editor::KeyBindingViewModel& /*senderVM*/, const Control::KeyChord& keys)
{
// 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<Controls::Control>().Focus(FocusState::Programmatic);
}
}
}
void Actions::_ViewModelRebindKeysHandler(const Editor::KeyBindingViewModel& senderVM, const Editor::RebindKeysEventArgs& args)
{
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<KeyBindingViewModel>(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);
// update settings model and view model
_State.Settings().ActionMap().RebindKeys(args.OldKeys(), args.NewKeys());
senderVMImpl->Keys(args.NewKeys());
senderVM.ToggleEditMode();
});
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<uint32_t> Actions::_GetContainerIndexByKeyChord(const Control::KeyChord& keys)
{
for (uint32_t i = 0; i < _KeyBindingList.Size(); ++i)
{
const auto kbdVM{ get_self<KeyBindingViewModel>(_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;
}
}

View file

@ -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<RebindKeysEventArgs>
{
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<KeyBindingViewModel>, ViewModelHelper<KeyBindingViewModel>
{
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<ActionsPageNavigationState>
{
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<Actions>
@ -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<winrt::Microsoft::Terminal::Settings::Model::Command> FilteredActions();
WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler);
WINRT_PROPERTY(Editor::ActionsPageNavigationState, State, nullptr);
WINRT_PROPERTY(Windows::Foundation::Collections::IObservableVector<Editor::KeyBindingViewModel>, KeyBindingList);
private:
friend struct ActionsT<Actions>; // for Xaml to bind events
Windows::Foundation::Collections::IObservableVector<winrt::Microsoft::Terminal::Settings::Model::Command> _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<uint32_t> _GetContainerIndexByKeyChord(const Control::KeyChord& keys);
bool _AutomationPeerAttached{ false };
};
}

View file

@ -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<KeyBindingViewModel, RebindKeysEventArgs> RebindKeysRequested;
event Windows.Foundation.TypedEventHandler<KeyBindingViewModel, Microsoft.Terminal.Control.KeyChord> DeleteKeyBindingRequested;
}
runtimeclass ActionsPageNavigationState
{
Microsoft.Terminal.Settings.Model.CascadiaSettings Settings;
void RequestOpenJson(Microsoft.Terminal.Settings.Model.SettingsTarget target);
event Windows.Foundation.TypedEventHandler<Object, Microsoft.Terminal.Settings.Model.SettingsTarget> OpenJson;
};
[default_interface] runtimeclass Actions : Windows.UI.Xaml.Controls.Page
@ -17,7 +52,6 @@ namespace Microsoft.Terminal.Settings.Editor
Actions();
ActionsPageNavigationState State { get; };
IObservableVector<Microsoft.Terminal.Settings.Model.Command> FilteredActions { get; };
IObservableVector<KeyBindingViewModel> KeyBindingList { get; };
}
}

View file

@ -5,10 +5,10 @@
<Page x:Class="Microsoft.Terminal.Settings.Editor.Actions"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:SettingsModel="using:Microsoft.Terminal.Settings.Model"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Microsoft.Terminal.Settings.Editor"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:model="using:Microsoft.Terminal.Settings.Model"
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
mc:Ignorable="d">
@ -18,69 +18,7 @@
<ResourceDictionary Source="CommonResources.xaml" />
</ResourceDictionary.MergedDictionaries>
<local:StringIsEmptyConverter x:Key="CommandKeyChordVisibilityConverter" />
<!--
Template for actions. This is _heavily_ copied from the command
palette, with modifications:
* We don't need to use a HighlightedTextControl, because we're
not filtering this list
* We don't need the chevron for nested commands
* We're not displaying the icon
* We're binding directly to a Command, not a FilteredCommand
If we wanted to reuse the command palette's list more directly,
that's theoretically possible, but then it would need to be
lifted out of TerminalApp and either moved into the
TerminalSettingsEditor or moved to it's own project consumed by
both TSE and TerminalApp.
-->
<DataTemplate x:Key="GeneralItemTemplate"
x:DataType="SettingsModel:Command">
<!--
This HorizontalContentAlignment="Stretch" is important
to make sure it takes the entire width of the line
-->
<ListViewItem HorizontalContentAlignment="Stretch"
AutomationProperties.AcceleratorKey="{x:Bind KeyChordText, Mode=OneWay}"
AutomationProperties.Name="{x:Bind Name, Mode=OneWay}">
<Grid HorizontalAlignment="Stretch"
ColumnSpacing="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<!-- command label -->
<ColumnDefinition Width="*" />
<!-- key chord -->
<ColumnDefinition Width="32" />
<!-- gutter for scrollbar -->
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
HorizontalAlignment="Left"
Text="{x:Bind Name, Mode=OneWay}" />
<!--
Inexplicably, we don't need to set the
AutomationProperties to Raw here, unlike in the
CommandPalette. We're not quite sure why.
-->
<Border Grid.Column="1"
Padding="2,0,2,0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Style="{ThemeResource KeyChordBorderStyle}">
<TextBlock FontSize="12"
Style="{ThemeResource KeyChordTextBlockStyle}"
Text="{x:Bind KeyChordText, Mode=OneWay}" />
</Border>
</Grid>
</ListViewItem>
</DataTemplate>
<!-- These resources again, HEAVILY copied from the command palette -->
<!-- Theme Dictionary -->
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Dark">
<!-- TextBox colors ! -->
@ -181,44 +119,271 @@
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
<!-- Styles -->
<Style x:Key="KeyBindingContainerStyle"
BasedOn="{StaticResource DefaultListViewItemStyle}"
TargetType="ListViewItem">
<Setter Property="Padding" Value="4" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="XYFocusKeyboardNavigation" Value="Enabled" />
</Style>
<Style x:Key="KeyBindingNameTextBlockStyle"
BasedOn="{StaticResource BaseTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="TextWrapping" Value="WrapWholeWords" />
</Style>
<Style x:Key="KeyChordEditorStyle"
BasedOn="{StaticResource DefaultTextBoxStyle}"
TargetType="TextBox">
<Setter Property="HorizontalAlignment" Value="Right" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="TextAlignment" Value="Right" />
<Setter Property="TextWrapping" Value="Wrap" />
<Setter Property="IsSpellCheckEnabled" Value="False" />
</Style>
<x:Int32 x:Key="EditButtonSize">32</x:Int32>
<x:Double x:Key="EditButtonIconSize">15</x:Double>
<Style x:Key="EditButtonStyle"
BasedOn="{StaticResource DefaultButtonStyle}"
TargetType="Button">
<Setter Property="Padding" Value="0" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Height" Value="{StaticResource EditButtonSize}" />
<Setter Property="Width" Value="{StaticResource EditButtonSize}" />
</Style>
<Style x:Key="AccentEditButtonStyle"
BasedOn="{StaticResource AccentButtonStyle}"
TargetType="Button">
<Setter Property="Padding" Value="3" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Height" Value="{StaticResource EditButtonSize}" />
<Setter Property="Width" Value="{StaticResource EditButtonSize}" />
</Style>
<!-- Converters & Misc. -->
<model:IconPathConverter x:Key="IconSourceConverter" />
<local:InvertedBooleanToVisibilityConverter x:Key="InvertedBooleanToVisibilityConverter" />
<SolidColorBrush x:Key="EditModeContainerBackground"
Color="{ThemeResource SystemListMediumColor}" />
<SolidColorBrush x:Key="NonEditModeContainerBackground"
Color="Transparent" />
<!-- Templates -->
<DataTemplate x:Key="KeyBindingTemplate"
x:DataType="local:KeyBindingViewModel">
<ListViewItem AutomationProperties.AcceleratorKey="{x:Bind KeyChordText, Mode=OneWay}"
AutomationProperties.Name="{x:Bind Name, Mode=OneWay}"
Background="{x:Bind ContainerBackground, Mode=OneWay}"
GotFocus="{x:Bind FocusContainer}"
LostFocus="{x:Bind UnfocusContainer}"
PointerEntered="{x:Bind EnterHoverMode}"
PointerExited="{x:Bind ExitHoverMode}"
Style="{StaticResource KeyBindingContainerStyle}">
<Grid ColumnSpacing="8">
<Grid.ColumnDefinitions>
<!-- command name -->
<ColumnDefinition Width="*" />
<!-- key chord -->
<ColumnDefinition Width="150" />
<!-- edit buttons -->
<!--
This needs to be 112 because that is the width of the row of buttons in edit mode + padding.
3 buttons: 32+32+32
Padding: 8+ 8
This allows the "edit" button to align with the "cancel" button seamlessly
-->
<ColumnDefinition Width="112" />
</Grid.ColumnDefinitions>
<!-- Command Name -->
<TextBlock Grid.Column="0"
Style="{StaticResource KeyBindingNameTextBlockStyle}"
Text="{x:Bind Name, Mode=OneWay}" />
<!-- Key Chord Text -->
<Border Grid.Column="1"
Padding="2,0,2,0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Style="{ThemeResource KeyChordBorderStyle}"
Visibility="{x:Bind IsInEditMode, Mode=OneWay, Converter={StaticResource InvertedBooleanToVisibilityConverter}}">
<TextBlock FontSize="14"
Style="{ThemeResource KeyChordTextBlockStyle}"
Text="{x:Bind KeyChordText, Mode=OneWay}"
TextWrapping="WrapWholeWords" />
</Border>
<!-- Edit Mode: Key Chord Text Box -->
<TextBox Grid.Column="1"
DataContext="{x:Bind Mode=OneWay}"
PreviewKeyDown="KeyChordEditor_PreviewKeyDown"
Style="{StaticResource KeyChordEditorStyle}"
Text="{x:Bind ProposedKeys, Mode=TwoWay}"
Visibility="{x:Bind IsInEditMode, Mode=OneWay}" />
<!-- Edit Button -->
<Button x:Uid="Actions_EditButton"
Grid.Column="2"
AutomationProperties.Name="{x:Bind EditButtonName}"
Background="Transparent"
Click="{x:Bind ToggleEditMode}"
GettingFocus="{x:Bind FocusEditButton}"
LosingFocus="{x:Bind UnfocusEditButton}"
Style="{StaticResource EditButtonStyle}"
Visibility="{x:Bind ShowEditButton, Mode=OneWay}">
<Button.Content>
<FontIcon FontSize="{StaticResource EditButtonIconSize}"
Glyph="&#xE70F;" />
</Button.Content>
<Button.Resources>
<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<SolidColorBrush x:Key="ButtonForeground"
Color="White" />
<SolidColorBrush x:Key="ButtonForegroundPointerOver"
Color="{StaticResource SystemAccentColor}" />
<SolidColorBrush x:Key="ButtonForegroundPressed"
Color="{StaticResource SystemAccentColor}" />
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<SolidColorBrush x:Key="ButtonForeground"
Color="White" />
<SolidColorBrush x:Key="ButtonForegroundPointerOver"
Color="{StaticResource SystemAccentColor}" />
<SolidColorBrush x:Key="ButtonForegroundPressed"
Color="{StaticResource SystemAccentColor}" />
</ResourceDictionary>
<ResourceDictionary x:Key="HighContrast">
<SolidColorBrush x:Key="ButtonBackground"
Color="{ThemeResource SystemColorButtonFaceColor}" />
<SolidColorBrush x:Key="ButtonBackgroundPointerOver"
Color="{ThemeResource SystemColorHighlightColor}" />
<SolidColorBrush x:Key="ButtonBackgroundPressed"
Color="{ThemeResource SystemColorHighlightColor}" />
<SolidColorBrush x:Key="ButtonForeground"
Color="{ThemeResource SystemColorButtonTextColor}" />
<SolidColorBrush x:Key="ButtonForegroundPointerOver"
Color="{ThemeResource SystemColorHighlightTextColor}" />
<SolidColorBrush x:Key="ButtonForegroundPressed"
Color="{ThemeResource SystemColorHighlightTextColor}" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
</Button.Resources>
</Button>
<!-- Edit Mode: Buttons -->
<StackPanel Grid.Column="2"
Orientation="Horizontal"
Visibility="{x:Bind IsInEditMode, Mode=OneWay}">
<!-- Cancel editing the action -->
<Button x:Uid="Actions_CancelButton"
AutomationProperties.Name="{x:Bind CancelButtonName}"
Click="{x:Bind ToggleEditMode}"
Style="{StaticResource EditButtonStyle}">
<FontIcon FontSize="{StaticResource EditButtonIconSize}"
Glyph="&#xE711;" />
</Button>
<!-- Accept changes -->
<Button x:Uid="Actions_AcceptButton"
Margin="8,0,0,0"
AutomationProperties.Name="{x:Bind AcceptButtonName}"
Click="{x:Bind AttemptAcceptChanges}"
Flyout="{x:Bind AcceptChangesFlyout, Mode=OneWay}"
Style="{StaticResource AccentEditButtonStyle}">
<FontIcon FontSize="{StaticResource EditButtonIconSize}"
Glyph="&#xE8FB;" />
</Button>
<!-- Delete the current key binding -->
<Button x:Uid="Actions_DeleteButton"
Margin="8,0,0,0"
AutomationProperties.Name="{x:Bind DeleteButtonName}"
Style="{StaticResource EditButtonStyle}">
<Button.Content>
<FontIcon FontSize="{StaticResource EditButtonIconSize}"
Glyph="&#xE74D;" />
</Button.Content>
<Button.Flyout>
<Flyout>
<StackPanel>
<TextBlock x:Uid="Actions_DeleteConfirmationMessage"
Style="{StaticResource CustomFlyoutTextStyle}" />
<Button x:Uid="Actions_DeleteConfirmationButton"
Click="{x:Bind DeleteKeyBinding}" />
</StackPanel>
</Flyout>
</Button.Flyout>
<Button.Resources>
<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<SolidColorBrush x:Key="ButtonBackground"
Color="Firebrick" />
<SolidColorBrush x:Key="ButtonBackgroundPointerOver"
Color="#C23232" />
<SolidColorBrush x:Key="ButtonBackgroundPressed"
Color="#A21212" />
<SolidColorBrush x:Key="ButtonForeground"
Color="White" />
<SolidColorBrush x:Key="ButtonForegroundPointerOver"
Color="White" />
<SolidColorBrush x:Key="ButtonForegroundPressed"
Color="White" />
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<SolidColorBrush x:Key="ButtonBackground"
Color="Firebrick" />
<SolidColorBrush x:Key="ButtonBackgroundPointerOver"
Color="#C23232" />
<SolidColorBrush x:Key="ButtonBackgroundPressed"
Color="#A21212" />
<SolidColorBrush x:Key="ButtonForeground"
Color="White" />
<SolidColorBrush x:Key="ButtonForegroundPointerOver"
Color="White" />
<SolidColorBrush x:Key="ButtonForegroundPressed"
Color="White" />
</ResourceDictionary>
<ResourceDictionary x:Key="HighContrast">
<SolidColorBrush x:Key="ButtonBackground"
Color="{ThemeResource SystemColorButtonFaceColor}" />
<SolidColorBrush x:Key="ButtonBackgroundPointerOver"
Color="{ThemeResource SystemColorHighlightColor}" />
<SolidColorBrush x:Key="ButtonBackgroundPressed"
Color="{ThemeResource SystemColorHighlightColor}" />
<SolidColorBrush x:Key="ButtonForeground"
Color="{ThemeResource SystemColorButtonTextColor}" />
<SolidColorBrush x:Key="ButtonForegroundPointerOver"
Color="{ThemeResource SystemColorHighlightTextColor}" />
<SolidColorBrush x:Key="ButtonForegroundPressed"
Color="{ThemeResource SystemColorHighlightTextColor}" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
</Button.Resources>
</Button>
</StackPanel>
</Grid>
</ListViewItem>
</DataTemplate>
</ResourceDictionary>
</Page.Resources>
<ScrollViewer>
<StackPanel Style="{StaticResource SettingsStackStyle}">
<TextBlock x:Uid="Globals_KeybindingsDisclaimer"
Style="{StaticResource DisclaimerStyle}" />
<!--
The Nav_OpenJSON resource just so happens to have a .Content
and .Tooltip that are _exactly_ what we're looking for here.
-->
<HyperlinkButton x:Uid="Nav_OpenJSON"
Click="_OpenSettingsClick" />
<StackPanel MaxWidth="600"
Style="{StaticResource SettingsStackStyle}">
<!-- Keybindings -->
<!--
NOTE: Globals_Keybindings.Header is not defined, because that
would result in the page having "Keybindings" displayed twice, which
looks quite redundant
-->
<ContentPresenter x:Uid="Globals_Keybindings"
Margin="0">
<ListView HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
AllowDrop="False"
CanReorderItems="False"
IsItemClickEnabled="False"
ItemTemplate="{StaticResource GeneralItemTemplate}"
ItemsSource="{x:Bind FilteredActions, Mode=OneWay}"
SelectionMode="None" />
</ContentPresenter>
<ListView x:Name="KeyBindingsListView"
ItemTemplate="{StaticResource KeyBindingTemplate}"
ItemsSource="{x:Bind KeyBindingList, Mode=OneWay}"
SelectionMode="None" />
</StackPanel>
</ScrollViewer>

View file

@ -29,7 +29,7 @@
<Style x:Key="SettingsStackStyle"
TargetType="StackPanel">
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="Margin" Value="13,0,0,48" />
<Setter Property="Margin" Value="13,0,13,48" />
</Style>
<!-- Used to stack a group of settings inside a pivot -->

View file

@ -286,14 +286,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
}
else if (clickedItemTag == actionsTag)
{
auto actionsState{ winrt::make<ActionsPageNavigationState>(_settingsClone) };
actionsState.OpenJson([weakThis = get_weak()](auto&&, auto&& arg) {
if (auto self{ weakThis.get() })
{
self->_OpenJsonHandlers(nullptr, arg);
}
});
contentFrame().Navigate(xaml_typename<Editor::Actions>(), actionsState);
contentFrame().Navigate(xaml_typename<Editor::Actions>(), winrt::make<ActionsPageNavigationState>(_settingsClone));
}
else if (clickedItemTag == colorSchemesTag)
{

View file

@ -1091,4 +1091,48 @@
<value>If checked, show all installed fonts in the list above. Otherwise, only show the list of monospace fonts.</value>
<comment>A description for what the supplementary "show all fonts" setting does. Presented near "Profile_FontFaceShowAllFonts".</comment>
</data>
</root>
<data name="Actions_DeleteConfirmationButton.Content" xml:space="preserve">
<value>Yes, delete key binding</value>
<comment>Button label that confirms deletion of a key binding entry.</comment>
</data>
<data name="Actions_DeleteConfirmationMessage.Text" xml:space="preserve">
<value>Are you sure you want to delete this key binding?</value>
<comment>Confirmation message displayed when the user attempts to delete a key binding entry.</comment>
</data>
<data name="Actions_InvalidKeyChordMessage" xml:space="preserve">
<value>Invalid key chord. Please enter a valid key chord.</value>
<comment>Error message displayed when an invalid key chord is input by the user.</comment>
</data>
<data name="Actions_RenameConflictConfirmationAcceptButton" xml:space="preserve">
<value>Yes</value>
<comment>Button label that confirms the deletion of a conflicting key binding to allow the current key binding to be registered.</comment>
</data>
<data name="Actions_RenameConflictConfirmationMessage" xml:space="preserve">
<value>The provided key chord is already being used by the following action:</value>
<comment>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.</comment>
</data>
<data name="Actions_RenameConflictConfirmationQuestion" xml:space="preserve">
<value>Would you like to overwrite that key binding?</value>
<comment>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.</comment>
</data>
<data name="Actions_UnnamedCommandName" xml:space="preserve">
<value>&lt;unnamed command&gt;</value>
<comment>{Locked="&lt;"} {Locked="&gt;"} The text shown when referring to a command that is unnamed.</comment>
</data>
<data name="Actions_AcceptButton.[using:Windows.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>Accept</value>
<comment>Text label for a button that can be used to accept changes to a key binding entry.</comment>
</data>
<data name="Actions_CancelButton.[using:Windows.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>Cancel</value>
<comment>Text label for a button that can be used to cancel changes to a key binding entry</comment>
</data>
<data name="Actions_DeleteButton.[using:Windows.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>Delete</value>
<comment>Text label for a button that can be used to delete a key binding entry.</comment>
</data>
<data name="Actions_EditButton.[using:Windows.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>Edit</value>
<comment>Text label for a button that can be used to begin making changes to a key binding entry.</comment>
</data>
</root>

View file

@ -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<type>(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<SettingContainer>

View file

@ -70,6 +70,27 @@ private:
winrt::Windows::Foundation::Collections::IObservableVector<winrt::Microsoft::Terminal::Settings::Editor::EnumEntry> _##name##List; \
winrt::Windows::Foundation::Collections::IMap<enumType, winrt::Microsoft::Terminal::Settings::Editor::EnumEntry> _##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<type>(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);

View file

@ -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__ };

View file

@ -193,32 +193,14 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{
if (!_GlobalHotkeysCache)
{
std::unordered_set<InternalActionID> visitedActionIDs{};
std::unordered_map<Control::KeyChord, Model::Command, KeyChordHash, KeyChordEquality> 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<Command>(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<Control::KeyChord, Model::Command>(std::move(globalHotkeys));
@ -226,6 +208,64 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
return _GlobalHotkeysCache.GetView();
}
Windows::Foundation::Collections::IMapView<Control::KeyChord, Model::Command> ActionMap::KeyBindings()
{
if (!_KeyBindingMapCache)
{
// populate _KeyBindingMapCache
std::unordered_map<KeyChord, Model::Command, KeyChordHash, KeyChordEquality> keyBindingsMap;
std::unordered_set<KeyChord, KeyChordHash, KeyChordEquality> unboundKeys;
_PopulateKeyBindingMapWithStandardCommands(keyBindingsMap, unboundKeys);
_KeyBindingMapCache = single_threaded_map<KeyChord, Model::Command>(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<KeyChord, Model::Command, KeyChordHash, KeyChordEquality>& keyBindingsMap, std::unordered_set<Control::KeyChord, KeyChordHash, KeyChordEquality>& 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<Command>(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> ActionMap::Copy() const
{
auto actionMap{ make_self<ActionMap>() };
@ -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<Command>(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<Command>() };
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:
// - <none>
void ActionMap::DeleteKeyBinding(KeyChord const& keys)
{
// create an "unbound" command
// { "command": "unbound", "keys": <keys> }
const auto cmd{ make_self<Command>() };
cmd->ActionAndArgs(make<ActionAndArgs>());
cmd->RegisterKey(keys);
AddAction(*cmd);
}
}

View file

@ -55,6 +55,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
// views
Windows::Foundation::Collections::IMapView<hstring, Model::Command> NameMap();
Windows::Foundation::Collections::IMapView<Control::KeyChord, Model::Command> GlobalHotkeys();
Windows::Foundation::Collections::IMapView<Control::KeyChord, Model::Command> KeyBindings();
com_ptr<ActionMap> Copy() const;
// queries
@ -66,6 +67,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
void AddAction(const Model::Command& cmd);
std::vector<SettingsLoadWarnings> 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<hstring, Model::Command>& nameMap) const;
void _PopulateNameMapWithStandardCommands(std::unordered_map<hstring, Model::Command>& nameMap) const;
void _PopulateKeyBindingMapWithStandardCommands(std::unordered_map<Control::KeyChord, Model::Command, KeyChordHash, KeyChordEquality>& keyBindingsMap, std::unordered_set<Control::KeyChord, KeyChordHash, KeyChordEquality>& unboundKeys) const;
std::vector<Model::Command> _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<hstring, Model::Command> _NameMapCache{ nullptr };
Windows::Foundation::Collections::IMap<Control::KeyChord, Model::Command> _GlobalHotkeysCache{ nullptr };
Windows::Foundation::Collections::IMap<Control::KeyChord, Model::Command> _KeyBindingMapCache{ nullptr };
Windows::Foundation::Collections::IMap<hstring, Model::Command> _NestedCommands{ nullptr };
std::unordered_map<Control::KeyChord, InternalActionID, KeyChordHash, KeyChordEquality> _KeyMap;
std::unordered_map<InternalActionID, Model::Command> _ActionMap;

View file

@ -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<String, Command> NameMap { get; };
Windows.Foundation.Collections.IMapView<Microsoft.Terminal.Control.KeyChord, Command> GlobalHotkeys();
Windows.Foundation.Collections.IMapView<Microsoft.Terminal.Control.KeyChord, Command> KeyBindings { get; };
Windows.Foundation.Collections.IMapView<Microsoft.Terminal.Control.KeyChord, Command> 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);
}
}