Compare commits

..

2 Commits

Author SHA1 Message Date
Leonard Hecker
fa81e700ff Use native sRGB support for blending 2024-02-18 16:02:34 +01:00
PankajBhojwani
f30cbef34d Implement MVVM for the Actions page (#14292)
## Summary of the Pull Request
Implements an `ActionsViewModel` for the `Actions` page in the SUI

## References

## PR Checklist
* [ ] Closes #xxx
* [x] CLA signed. If not, go over
[here](https://cla.opensource.microsoft.com/microsoft/Terminal) and sign
the CLA
* [ ] Tests added/passed
* [ ] Documentation updated. If checked, please file a pull request on
[our docs repo](https://github.com/MicrosoftDocs/terminal) and link it
here: #xxx
* [ ] Schema updated.
* [x] I've discussed this with core contributors already. If not
checked, I'm ready to accept this work might be rejected in favor of a
different grand plan. Issue number where discussion took place: #xxx

## Detailed Description of the Pull Request / Additional comments
A few annoyances:
- Because of the shifts in the UI when switching between edit
mode/regular mode on the `KeyBindingViewModel`s, the page used to
manually handle moving focus accordingly so that focus does not get
lost. However, the page no longer owns the `KeyBindingViewModel`,
instead the `ActionsViewModel` owns them but the `ActionsViewModel`
cannot manually move focus around since it does not own the UI Element.
So, the `ActionsViewModel` emits an event for the page to catch that
tells the page to move focus (`FocusContainer`).
- Similarly, the page used to manually update the `ContainerBackground`
of the `KeyBindingViewModel` when the kbdVM enters `EditMode`. The
`ActionsViewModel` does not have access to the page's resources though
(to determine the correct background brush to use). So, the
`ActionsViewModel` emits another event for the page to catch
(`UpdateBackground`).

## Validation Steps Performed
Actions page still works as before

---------

Co-authored-by: Dustin L. Howett <duhowett@microsoft.com>
2024-02-13 19:25:09 +00:00
23 changed files with 662 additions and 754 deletions

View File

@@ -8,7 +8,6 @@
#include <LibraryResources.h>
#include "SuggestionsControl.g.cpp"
#include "../../types/inc/utils.hpp"
using namespace winrt;
using namespace winrt::TerminalApp;
@@ -20,8 +19,6 @@ using namespace winrt::Windows::Foundation;
using namespace winrt::Windows::Foundation::Collections;
using namespace winrt::Microsoft::Terminal::Settings::Model;
using namespace std::chrono_literals;
namespace winrt::TerminalApp::implementation
{
SuggestionsControl::SuggestionsControl()
@@ -265,8 +262,8 @@ namespace winrt::TerminalApp::implementation
// - <unused>
// Return Value:
// - <none>
winrt::fire_and_forget SuggestionsControl::_selectedCommandChanged(const IInspectable& /*sender*/,
const Windows::UI::Xaml::RoutedEventArgs& /*args*/)
void SuggestionsControl::_selectedCommandChanged(const IInspectable& /*sender*/,
const Windows::UI::Xaml::RoutedEventArgs& /*args*/)
{
const auto selectedCommand = _filteredActionsView().SelectedItem();
const auto filteredCommand{ selectedCommand.try_as<winrt::TerminalApp::FilteredCommand>() };
@@ -284,106 +281,11 @@ namespace winrt::TerminalApp::implementation
{
if (const auto actionPaletteItem{ filteredCommand.Item().try_as<winrt::TerminalApp::ActionPaletteItem>() })
{
const auto& cmd = actionPaletteItem.Command();
PreviewAction.raise(*this, cmd);
const auto description{ cmd.Description() };
if (SelectedItem())
SelectedItem().SetValue(Automation::AutomationProperties::FullDescriptionProperty(), winrt::box_value(description));
if (!description.empty())
{
// If it's already open, then just re-target it and update the content immediately.
if (_descriptionsView().Visibility() == Visibility::Visible)
{
_openTooltip(cmd);
}
else
{
// Otherwise, wait a bit before opening it.
co_await winrt::resume_after(200ms);
co_await wil::resume_foreground(Dispatcher());
_openTooltip(cmd);
// DescriptionTip().IsOpen(true);
}
}
else
{
// If there's no description, then just close the tooltip.
// DescriptionTip().IsOpen(false);
_descriptionsView().Visibility(Visibility::Collapsed);
_descriptionsBackdrop().Visibility(Visibility::Collapsed);
}
PreviewAction.raise(*this, actionPaletteItem.Command());
}
}
}
winrt::fire_and_forget SuggestionsControl::_openTooltip(Command cmd)
{
const auto description{ cmd.Description() };
if (!description.empty())
{
// DescriptionTip().Target(SelectedItem());
// DescriptionTip().Title(cmd.Name());
{
// The Title
_descriptionTitle().Inlines().Clear();
Documents::Run titleRun;
titleRun.Text(cmd.Name());
_descriptionTitle().Inlines().Append(titleRun);
}
// TODO! NOT REALLY TRUE ANYMORE
// If you try to put a newline in the Subtitle, it'll _immediately
// close the tooltip_. Instead, we'll need to build up the text as a
// series of runs, and put them in the content.
_descriptionComment().Inlines().Clear();
// First, replace all "\r\n" with "\n"
std::wstring filtered = description.c_str();
// replace all "\r\n" with "\n" in `filtered`
std::wstring::size_type pos = 0;
while ((pos = filtered.find(L"\r\n", pos)) != std::wstring::npos)
{
filtered.erase(pos, 1);
}
// Split the filtered description on '\n`
const auto lines = ::Microsoft::Console::Utils::SplitString(filtered.c_str(), L'\n');
// For each line, build a Run + LineBreak, and add them to the text
// block
for (const auto& line : lines)
{
if (line.empty())
{
continue;
}
Documents::Run textRun;
textRun.Text(winrt::hstring{ line });
_descriptionComment().Inlines().Append(textRun);
_descriptionComment().Inlines().Append(Documents::LineBreak{});
}
// // TODO! These were all feigned attempts to allow us to focus the content of the teachingtip.
// // We may want to keep IsTextSelectionEnabled in the XAML.
// //
// // I also have no idea if the FullDescriptionProperty thing worked at all.
// _toolTipContent().AllowFocusOnInteraction(true);
// _toolTipContent().IsTextSelectionEnabled(true);
// DescriptionTip().SetValue(Automation::AutomationProperties::FullDescriptionProperty(), winrt::box_value(description));
_descriptionsView().Visibility(Visibility::Visible);
_descriptionsBackdrop().Visibility(Visibility::Visible);
_recalculateTopMargin();
}
co_return;
}
void SuggestionsControl::_previewKeyDownHandler(const IInspectable& /*sender*/,
const Windows::UI::Xaml::Input::KeyRoutedEventArgs& e)
{
@@ -1125,43 +1027,13 @@ namespace winrt::TerminalApp::implementation
if (_direction == TerminalApp::SuggestionsDirection::TopDown)
{
Controls::Grid::SetRow(_searchBox(), 0);
Controls::Grid::SetRow(_descriptionsBackdrop(), 2);
}
else // BottomUp
{
Controls::Grid::SetRow(_searchBox(), 4);
Controls::Grid::SetRow(_descriptionsBackdrop(), 0);
}
}
void SuggestionsControl::_recalculateTopMargin()
{
auto currentMargin = Margin();
const til::size actualSize{ til::math::rounding, ActualWidth(), ActualHeight() };
const til::size descriptionSize{ til::math::rounding, _descriptionsBackdrop().ActualWidth(), _descriptionsBackdrop().ActualHeight() };
// Now, position vertically.
if (_direction == TerminalApp::SuggestionsDirection::TopDown)
{
// The control should open right below the cursor, with the list
// extending below. This is easy, we can just use the cursor as the
// origin (more or less)
currentMargin.Top = (_anchor.Y /* - descriptionSize.height*/);
}
else
{
// Bottom Up.
// TODO! This is all wrong. It just jumps around randomly.
// Position at the cursor. The suggestions UI itself will maintain
// its own offset such that it's always above its origin
currentMargin.Top = (_anchor.Y - actualSize.height /*+ descriptionSize.height*/);
}
Margin(currentMargin);
}
void SuggestionsControl::Open(TerminalApp::SuggestionsMode mode,
const Windows::Foundation::Collections::IVector<Microsoft::Terminal::Settings::Model::Command>& commands,
winrt::hstring filter,
@@ -1180,7 +1052,6 @@ namespace winrt::TerminalApp::implementation
_space = space;
const til::size actualSize{ til::math::rounding, ActualWidth(), ActualHeight() };
const til::size descriptionSize{ til::math::rounding, _descriptionsBackdrop().ActualWidth(), _descriptionsBackdrop().ActualHeight() };
// Is there space in the window below the cursor to open the menu downwards?
const bool canOpenDownwards = (_anchor.Y + characterHeight + actualSize.height) < space.Height;
_setDirection(canOpenDownwards ? TerminalApp::SuggestionsDirection::TopDown :
@@ -1207,13 +1078,13 @@ namespace winrt::TerminalApp::implementation
// The control should open right below the cursor, with the list
// extending below. This is easy, we can just use the cursor as the
// origin (more or less)
newMargin.Top = (_anchor.Y /* - descriptionSize.height*/);
newMargin.Top = (_anchor.Y);
}
else
{
// Position at the cursor. The suggestions UI itself will maintain
// its own offset such that it's always above its origin
newMargin.Top = (_anchor.Y - actualSize.height /* - descriptionSize.height*/);
newMargin.Top = (_anchor.Y - actualSize.height);
}
Margin(newMargin);

View File

@@ -99,8 +99,6 @@ namespace winrt::TerminalApp::implementation
void _close();
void _dismissPalette();
void _recalculateTopMargin();
void _filterTextChanged(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& args);
void _previewKeyDownHandler(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::Input::KeyRoutedEventArgs& e);
void _keyUpHandler(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::Input::KeyRoutedEventArgs& e);
@@ -112,8 +110,7 @@ namespace winrt::TerminalApp::implementation
void _listItemClicked(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::Controls::ItemClickEventArgs& e);
void _listItemSelectionChanged(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::Controls::SelectionChangedEventArgs& e);
winrt::fire_and_forget _selectedCommandChanged(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& args);
winrt::fire_and_forget _openTooltip(Microsoft::Terminal::Settings::Model::Command cmd);
void _selectedCommandChanged(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& args);
void _moveBackButtonClicked(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs&);
void _updateCurrentNestedCommands(const winrt::Microsoft::Terminal::Settings::Model::Command& parentCommand);
@@ -125,6 +122,7 @@ namespace winrt::TerminalApp::implementation
Windows::Foundation::Collections::IVector<winrt::TerminalApp::FilteredCommand> _commandsToFilter();
std::wstring _getTrimmedInput();
uint32_t _getNumVisibleItems();
friend class TerminalAppLocalTests::TabTests;
};
}

View File

@@ -112,17 +112,15 @@
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="2*" />
<RowDefinition Height="7*" />
<RowDefinition Height="8*" />
<RowDefinition Height="2*" />
</Grid.RowDefinitions>
<Grid x:Name="_backdrop"
Grid.Row="1"
Grid.RowSpan="1"
Grid.Row="0"
Grid.RowSpan="2"
Grid.Column="0"
Grid.ColumnSpan="3"
MinWidth="300"
MaxWidth="300"
MaxHeight="300"
Margin="0"
@@ -138,16 +136,16 @@
Translation="0,0,32">
<Grid.RowDefinitions>
<!-- 0: Top-down _searchBox -->
<RowDefinition Height="Auto" />
<!-- 1: Top-down ParentCommandName -->
<!-- Top-down _searchBox -->
<RowDefinition Height="Auto" />
<!-- 2: Top-down UNUSED???????? -->
<!-- Top-down ParentCommandName -->
<RowDefinition Height="Auto" />
<!-- 3: _filteredActionsView -->
<!-- Top-down UNUSED???????? -->
<RowDefinition Height="*" />
<!-- 4: bottom-up _searchBox -->
<!-- _filteredActionsView -->
<RowDefinition Height="Auto" />
<!-- bottom-up _searchBox -->
</Grid.RowDefinitions>
<TextBox x:Name="_searchBox"
@@ -209,55 +207,8 @@
SelectionChanged="_listItemSelectionChanged"
SelectionMode="Single" />
</Grid>
<!-- <Border Height="1" Grid.Row="1" /> -->
<Grid x:Name="_descriptionsBackdrop"
Grid.Row="0"
Grid.RowSpan="1"
Grid.Column="0"
Grid.ColumnSpan="3"
MaxWidth="300"
Margin="0,6,0,0"
Padding="0,4,0,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="{ThemeResource FlyoutPresenterBackground}"
BorderBrush="{ThemeResource FlyoutBorderThemeBrush}"
BorderThickness="{ThemeResource FlyoutBorderThemeThickness}"
CornerRadius="{ThemeResource OverlayCornerRadius}"
PointerPressed="_backdropPointerPressed"
Shadow="{StaticResource SharedShadow}"
Translation="0,0,32">
<Grid.RowDefinitions>
<!-- 0: descriptions view -->
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel x:Name="_descriptionsView"
Grid.Row="0"
Margin="8,0,8,8"
Orientation="Vertical"
Visibility="Collapsed">
<TextBlock x:Name="_descriptionTitle"
FontSize="14"
FontWeight="Bold"
IsTextSelectionEnabled="True"
TextWrapping="WrapWholeWords" />
<ScrollViewer MaxHeight="64"
VerticalScrollBarVisibility="Visible"
VerticalScrollMode="Enabled"
Visibility="Visible">
<!-- <TextBlock Text="Foo bar bax"
TextWrapping="WrapWholeWords" />-->
<TextBlock x:Name="_descriptionComment"
IsTextSelectionEnabled="True"
TextWrapping="WrapWholeWords" />
</ScrollViewer>
</StackPanel>
</Grid>
</Grid>
</UserControl>

View File

@@ -4,108 +4,14 @@
#include "pch.h"
#include "Actions.h"
#include "Actions.g.cpp"
#include "KeyBindingViewModel.g.cpp"
#include "ActionsPageNavigationState.g.cpp"
#include "LibraryResources.h"
#include "../TerminalSettingsModel/AllShortcutActions.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 Windows::Foundation::Collections::IObservableVector<hstring>& availableActions) :
KeyBindingViewModel(nullptr, availableActions.First().Current(), availableActions) {}
KeyBindingViewModel::KeyBindingViewModel(const Control::KeyChord& keys, const hstring& actionName, const IObservableVector<hstring>& availableActions) :
_CurrentKeys{ keys },
_KeyChordText{ KeyChordSerialization::ToString(keys) },
_CurrentAction{ actionName },
_ProposedAction{ box_value(actionName) },
_AvailableActions{ availableActions }
{
// 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"CurrentKeys")
{
_KeyChordText = KeyChordSerialization::ToString(_CurrentKeys);
_NotifyChanges(L"KeyChordText");
}
else if (viewModelProperty == L"IsContainerFocused" ||
viewModelProperty == L"IsEditButtonFocused" ||
viewModelProperty == L"IsHovered" ||
viewModelProperty == L"IsAutomationPeerAttached" ||
viewModelProperty == L"IsInEditMode")
{
_NotifyChanges(L"ShowEditButton");
}
else if (viewModelProperty == L"CurrentAction")
{
_NotifyChanges(L"Name");
}
});
}
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
// - reset the combo box with the current action
ProposedKeys(_CurrentKeys);
ProposedAction(box_value(_CurrentAction));
}
}
void KeyBindingViewModel::AttemptAcceptChanges()
{
AttemptAcceptChanges(_ProposedKeys);
}
void KeyBindingViewModel::AttemptAcceptChanges(const Control::KeyChord newKeys)
{
const auto args{ make_self<ModifyKeyBindingEventArgs>(_CurrentKeys, // OldKeys
newKeys, // NewKeys
_IsNewlyAdded ? hstring{} : _CurrentAction, // OldAction
unbox_value<hstring>(_ProposedAction)) }; // NewAction
_ModifyKeyBindingRequestedHandlers(*this, *args);
}
void KeyBindingViewModel::CancelChanges()
{
if (_IsNewlyAdded)
{
_DeleteNewlyAddedKeyBindingHandlers(*this, nullptr);
}
else
{
ToggleEditMode();
}
}
Actions::Actions()
{
InitializeComponent();
@@ -115,277 +21,55 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
Automation::Peers::AutomationPeer Actions::OnCreateAutomationPeer()
{
_AutomationPeerAttached = true;
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);
}
_ViewModel.OnAutomationPeerAttached();
return nullptr;
}
void Actions::OnNavigatedTo(const NavigationEventArgs& e)
{
_State = e.Parameter().as<Editor::ActionsPageNavigationState>();
_ViewModel = e.Parameter().as<Editor::ActionsViewModel>();
// Populate AvailableActionAndArgs
_AvailableActionMap = single_threaded_map<hstring, Model::ActionAndArgs>();
std::vector<hstring> availableActionAndArgs;
for (const auto& [name, actionAndArgs] : _State.Settings().ActionMap().AvailableActions())
{
availableActionAndArgs.push_back(name);
_AvailableActionMap.Insert(name, actionAndArgs);
}
std::sort(begin(availableActionAndArgs), end(availableActionAndArgs));
_AvailableActionAndArgs = single_threaded_observable_vector(std::move(availableActionAndArgs));
// Subscribe to the view model's FocusContainer event.
// Use the KeyBindingViewModel or index provided in the event to focus the corresponding container
_ViewModel.FocusContainer([this](const auto& /*sender*/, const auto& args) {
if (auto kbdVM{ args.try_as<KeyBindingViewModel>() })
{
if (const auto& container = KeyBindingsListView().ContainerFromItem(*kbdVM))
{
container.as<Controls::ListViewItem>().Focus(FocusState::Programmatic);
}
}
else if (const auto& index = args.try_as<uint32_t>())
{
if (const auto& container = KeyBindingsListView().ContainerFromIndex(*index))
{
container.as<Controls::ListViewItem>().Focus(FocusState::Programmatic);
}
}
});
// 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)
{
// convert the cmd into a KeyBindingViewModel
auto container{ make_self<KeyBindingViewModel>(keys, cmd.Name(), _AvailableActionAndArgs) };
_RegisterEvents(container);
keyBindingList.push_back(*container);
}
std::sort(begin(keyBindingList), end(keyBindingList), KeyBindingViewModelComparator{});
_KeyBindingList = single_threaded_observable_vector(std::move(keyBindingList));
// Subscribe to the view model's UpdateBackground event.
// The view model does not have access to the page resources, so it asks us
// to update the key binding's container background
_ViewModel.UpdateBackground([this](const auto& /*sender*/, const auto& args) {
if (auto kbdVM{ args.try_as<KeyBindingViewModel>() })
{
if (kbdVM->IsInEditMode())
{
const auto& containerBackground{ Resources().Lookup(box_value(L"ActionContainerBackgroundEditing")).as<Windows::UI::Xaml::Media::Brush>() };
kbdVM->ContainerBackground(containerBackground);
}
else
{
const auto& containerBackground{ Resources().Lookup(box_value(L"ActionContainerBackground")).as<Windows::UI::Xaml::Media::Brush>() };
kbdVM->ContainerBackground(containerBackground);
}
}
});
}
void Actions::AddNew_Click(const IInspectable& /*sender*/, const RoutedEventArgs& /*eventArgs*/)
{
// Create the new key binding and register all of the event handlers.
auto kbdVM{ make_self<KeyBindingViewModel>(_AvailableActionAndArgs) };
_RegisterEvents(kbdVM);
kbdVM->DeleteNewlyAddedKeyBinding({ this, &Actions::_ViewModelDeleteNewlyAddedKeyBindingHandler });
// Manually add the editing background. This needs to be done in Actions not the view model.
// We also have to do this manually because it hasn't been added to the list yet.
kbdVM->IsInEditMode(true);
const auto& containerBackground{ Resources().Lookup(box_value(L"ActionContainerBackgroundEditing")).as<Windows::UI::Xaml::Media::Brush>() };
kbdVM->ContainerBackground(containerBackground);
// IMPORTANT: do this _after_ setting IsInEditMode. Otherwise, it'll get deleted immediately
// by the PropertyChangedHandler below (where we delete any IsNewlyAdded items)
kbdVM->IsNewlyAdded(true);
_KeyBindingList.InsertAt(0, *kbdVM);
}
void Actions::_ViewModelPropertyChangedHandler(const IInspectable& sender, const Windows::UI::Xaml::Data::PropertyChangedEventArgs& args)
{
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. any actions that were newly added are removed
// 3. this is the only entry that is in edit mode
for (int32_t i = _KeyBindingList.Size() - 1; i >= 0; --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 if (kbdVM.IsNewlyAdded())
{
// Remove any actions that were newly added
_KeyBindingList.RemoveAt(i);
}
else
{
// Exit edit mode for all other containers
get_self<KeyBindingViewModel>(kbdVM)->DisableEditMode();
}
}
const auto& containerBackground{ Resources().Lookup(box_value(L"ActionContainerBackgroundEditing")).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);
const auto& containerBackground{ Resources().Lookup(box_value(L"ActionContainerBackground")).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.
uint32_t index;
if (_KeyBindingList.IndexOf(senderVM, index))
{
_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::_ViewModelModifyKeyBindingHandler(const Editor::KeyBindingViewModel& senderVM, const Editor::ModifyKeyBindingEventArgs& args)
{
const auto isNewAction{ !args.OldKeys() && args.OldActionName().empty() };
auto applyChangesToSettingsModel = [=]() {
// If the key chord was changed,
// update the settings model and view model appropriately
// NOTE: we still need to update the view model if we're working with a newly added action
if (isNewAction || args.OldKeys().Modifiers() != args.NewKeys().Modifiers() || args.OldKeys().Vkey() != args.NewKeys().Vkey())
{
if (!isNewAction)
{
// update settings model
_State.Settings().ActionMap().RebindKeys(args.OldKeys(), args.NewKeys());
}
// update view model
auto senderVMImpl{ get_self<KeyBindingViewModel>(senderVM) };
senderVMImpl->CurrentKeys(args.NewKeys());
}
// If the action was changed,
// update the settings model and view model appropriately
// NOTE: no need to check for "isNewAction" here. <empty_string> != <action name> already.
if (args.OldActionName() != args.NewActionName())
{
// convert the action's name into a view model.
const auto& newAction{ _AvailableActionMap.Lookup(args.NewActionName()) };
// update settings model
_State.Settings().ActionMap().RegisterKeyBinding(args.NewKeys(), newAction);
// update view model
auto senderVMImpl{ get_self<KeyBindingViewModel>(senderVM) };
senderVMImpl->CurrentAction(args.NewActionName());
senderVMImpl->IsNewlyAdded(false);
}
};
// Check for this special case:
// we're changing the key chord,
// but the new key chord is already in use
if (isNewAction || args.OldKeys().Modifiers() != args.NewKeys().Modifiers() || args.OldKeys().Vkey() != args.NewKeys().Vkey())
{
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
applyChangesToSettingsModel();
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;
}
}
// update settings model and view model
applyChangesToSettingsModel();
// We NEED to toggle the edit mode here,
// so that if nothing changed, we still exit
// edit mode.
senderVM.ToggleEditMode();
}
void Actions::_ViewModelDeleteNewlyAddedKeyBindingHandler(const Editor::KeyBindingViewModel& senderVM, const IInspectable& /*args*/)
{
for (uint32_t i = 0; i < _KeyBindingList.Size(); ++i)
{
const auto& kbdVM{ _KeyBindingList.GetAt(i) };
if (kbdVM == senderVM)
{
_KeyBindingList.RemoveAt(i);
return;
}
}
}
// 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->CurrentKeys() };
if (otherKeys && 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;
}
void Actions::_RegisterEvents(com_ptr<KeyBindingViewModel>& kbdVM)
{
kbdVM->PropertyChanged({ this, &Actions::_ViewModelPropertyChangedHandler });
kbdVM->DeleteKeyBindingRequested({ this, &Actions::_ViewModelDeleteKeyBindingHandler });
kbdVM->ModifyKeyBindingRequested({ this, &Actions::_ViewModelModifyKeyBindingHandler });
kbdVM->IsAutomationPeerAttached(_AutomationPeerAttached);
_ViewModel.AddNewKeybinding();
}
}

View File

@@ -4,109 +4,12 @@
#pragma once
#include "Actions.g.h"
#include "KeyBindingViewModel.g.h"
#include "ActionsPageNavigationState.g.h"
#include "ModifyKeyBindingEventArgs.g.h"
#include "ActionsViewModel.h"
#include "Utils.h"
#include "ViewModelHelpers.h"
namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
struct KeyBindingViewModelComparator
{
bool operator()(const Editor::KeyBindingViewModel& lhs, const Editor::KeyBindingViewModel& rhs) const
{
return lhs.Name() < rhs.Name();
}
};
struct ModifyKeyBindingEventArgs : ModifyKeyBindingEventArgsT<ModifyKeyBindingEventArgs>
{
public:
ModifyKeyBindingEventArgs(const Control::KeyChord& oldKeys, const Control::KeyChord& newKeys, const hstring oldActionName, const hstring newActionName) :
_OldKeys{ oldKeys },
_NewKeys{ newKeys },
_OldActionName{ std::move(oldActionName) },
_NewActionName{ std::move(newActionName) } {}
WINRT_PROPERTY(Control::KeyChord, OldKeys, nullptr);
WINRT_PROPERTY(Control::KeyChord, NewKeys, nullptr);
WINRT_PROPERTY(hstring, OldActionName);
WINRT_PROPERTY(hstring, NewActionName);
};
struct KeyBindingViewModel : KeyBindingViewModelT<KeyBindingViewModel>, ViewModelHelper<KeyBindingViewModel>
{
public:
KeyBindingViewModel(const Windows::Foundation::Collections::IObservableVector<hstring>& availableActions);
KeyBindingViewModel(const Control::KeyChord& keys, const hstring& name, const Windows::Foundation::Collections::IObservableVector<hstring>& availableActions);
hstring Name() const { return _CurrentAction; }
hstring KeyChordText() const { return _KeyChordText; }
// 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 ActionGotFocus() { IsContainerFocused(true); };
void ActionLostFocus() { IsContainerFocused(false); };
void EditButtonGettingFocus() { IsEditButtonFocused(true); };
void EditButtonLosingFocus() { IsEditButtonFocused(false); };
bool ShowEditButton() const noexcept;
void ToggleEditMode();
void DisableEditMode() { IsInEditMode(false); }
void AttemptAcceptChanges();
void AttemptAcceptChanges(const Control::KeyChord newKeys);
void CancelChanges();
void DeleteKeyBinding() { _DeleteKeyBindingRequestedHandlers(*this, _CurrentKeys); }
// ProposedAction: the entry selected by the combo box; may disagree with the settings model.
// CurrentAction: the combo box item that maps to the settings model value.
// AvailableActions: the list of options in the combo box; both actions above must be in this list.
// NOTE: ProposedAction and CurrentAction may disagree mainly due to the "edit mode" system in place.
// Current Action serves as...
// 1 - a record of what to set ProposedAction to on a cancellation
// 2 - a form of translation between ProposedAction and the settings model
// We would also need an ActionMap reference to remove this, but this is a better separation
// of responsibilities.
VIEW_MODEL_OBSERVABLE_PROPERTY(IInspectable, ProposedAction);
VIEW_MODEL_OBSERVABLE_PROPERTY(hstring, CurrentAction);
WINRT_PROPERTY(Windows::Foundation::Collections::IObservableVector<hstring>, AvailableActions, nullptr);
// ProposedKeys: the keys proposed by the control; may disagree with the settings model.
// CurrentKeys: the key chord bound in the settings model.
VIEW_MODEL_OBSERVABLE_PROPERTY(Control::KeyChord, ProposedKeys);
VIEW_MODEL_OBSERVABLE_PROPERTY(Control::KeyChord, CurrentKeys, nullptr);
VIEW_MODEL_OBSERVABLE_PROPERTY(bool, IsInEditMode, false);
VIEW_MODEL_OBSERVABLE_PROPERTY(bool, IsNewlyAdded, false);
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(ModifyKeyBindingRequested, Editor::KeyBindingViewModel, Editor::ModifyKeyBindingEventArgs);
TYPED_EVENT(DeleteKeyBindingRequested, Editor::KeyBindingViewModel, Terminal::Control::KeyChord);
TYPED_EVENT(DeleteNewlyAddedKeyBinding, Editor::KeyBindingViewModel, IInspectable);
private:
hstring _KeyChordText{};
};
struct ActionsPageNavigationState : ActionsPageNavigationStateT<ActionsPageNavigationState>
{
public:
ActionsPageNavigationState(const Model::CascadiaSettings& settings) :
_Settings{ settings } {}
WINRT_PROPERTY(Model::CascadiaSettings, Settings, nullptr)
};
struct Actions : public HasScrollViewer<Actions>, ActionsT<Actions>
{
public:
@@ -114,24 +17,11 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
void OnNavigatedTo(const winrt::Windows::UI::Xaml::Navigation::NavigationEventArgs& e);
Windows::UI::Xaml::Automation::Peers::AutomationPeer OnCreateAutomationPeer();
void AddNew_Click(const IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& eventArgs);
WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler);
WINRT_PROPERTY(Editor::ActionsPageNavigationState, State, nullptr);
WINRT_PROPERTY(Windows::Foundation::Collections::IObservableVector<Editor::KeyBindingViewModel>, KeyBindingList);
private:
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 _ViewModelModifyKeyBindingHandler(const Editor::KeyBindingViewModel& senderVM, const Editor::ModifyKeyBindingEventArgs& args);
void _ViewModelDeleteNewlyAddedKeyBindingHandler(const Editor::KeyBindingViewModel& senderVM, const IInspectable& args);
std::optional<uint32_t> _GetContainerIndexByKeyChord(const Control::KeyChord& keys);
void _RegisterEvents(com_ptr<implementation::KeyBindingViewModel>& kbdVM);
bool _AutomationPeerAttached{ false };
Windows::Foundation::Collections::IObservableVector<hstring> _AvailableActionAndArgs;
Windows::Foundation::Collections::IMap<hstring, Model::ActionAndArgs> _AvailableActionMap;
WINRT_OBSERVABLE_PROPERTY(Editor::ActionsViewModel, ViewModel, _PropertyChangedHandlers, nullptr);
};
}

View File

@@ -1,63 +1,13 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import "EnumEntry.idl";
import "ActionsViewModel.idl";
namespace Microsoft.Terminal.Settings.Editor
{
runtimeclass ModifyKeyBindingEventArgs
{
Microsoft.Terminal.Control.KeyChord OldKeys { get; };
Microsoft.Terminal.Control.KeyChord NewKeys { get; };
String OldActionName { get; };
String NewActionName { 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; };
Boolean IsNewlyAdded { get; };
Microsoft.Terminal.Control.KeyChord ProposedKeys;
Object ProposedAction;
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 ActionGotFocus();
void ActionLostFocus();
void EditButtonGettingFocus();
void EditButtonLosingFocus();
IObservableVector<String> AvailableActions { get; };
void ToggleEditMode();
void AttemptAcceptChanges();
void CancelChanges();
void DeleteKeyBinding();
event Windows.Foundation.TypedEventHandler<KeyBindingViewModel, ModifyKeyBindingEventArgs> ModifyKeyBindingRequested;
event Windows.Foundation.TypedEventHandler<KeyBindingViewModel, Microsoft.Terminal.Control.KeyChord> DeleteKeyBindingRequested;
}
runtimeclass ActionsPageNavigationState
{
Microsoft.Terminal.Settings.Model.CascadiaSettings Settings;
};
[default_interface] runtimeclass Actions : Windows.UI.Xaml.Controls.Page
{
Actions();
ActionsPageNavigationState State { get; };
IObservableVector<KeyBindingViewModel> KeyBindingList { get; };
ActionsViewModel ViewModel { get; };
}
}

View File

@@ -347,7 +347,7 @@
<!-- Keybindings -->
<ListView x:Name="KeyBindingsListView"
ItemTemplate="{StaticResource KeyBindingTemplate}"
ItemsSource="{x:Bind KeyBindingList, Mode=OneWay}"
ItemsSource="{x:Bind ViewModel.KeyBindingList, Mode=OneWay}"
SelectionMode="None" />
</StackPanel>
</Border>

View File

@@ -0,0 +1,380 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "ActionsViewModel.h"
#include "ActionsViewModel.g.cpp"
#include "KeyBindingViewModel.g.cpp"
#include "LibraryResources.h"
#include "../TerminalSettingsModel/AllShortcutActions.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 IObservableVector<hstring>& availableActions) :
KeyBindingViewModel(nullptr, availableActions.First().Current(), availableActions) {}
KeyBindingViewModel::KeyBindingViewModel(const Control::KeyChord& keys, const hstring& actionName, const IObservableVector<hstring>& availableActions) :
_CurrentKeys{ keys },
_KeyChordText{ KeyChordSerialization::ToString(keys) },
_CurrentAction{ actionName },
_ProposedAction{ box_value(actionName) },
_AvailableActions{ availableActions }
{
// 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"CurrentKeys")
{
_KeyChordText = KeyChordSerialization::ToString(_CurrentKeys);
_NotifyChanges(L"KeyChordText");
}
else if (viewModelProperty == L"IsContainerFocused" ||
viewModelProperty == L"IsEditButtonFocused" ||
viewModelProperty == L"IsHovered" ||
viewModelProperty == L"IsAutomationPeerAttached" ||
viewModelProperty == L"IsInEditMode")
{
_NotifyChanges(L"ShowEditButton");
}
else if (viewModelProperty == L"CurrentAction")
{
_NotifyChanges(L"Name");
}
});
}
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
// - reset the combo box with the current action
ProposedKeys(_CurrentKeys);
ProposedAction(box_value(_CurrentAction));
}
}
void KeyBindingViewModel::AttemptAcceptChanges()
{
AttemptAcceptChanges(_ProposedKeys);
}
void KeyBindingViewModel::AttemptAcceptChanges(const Control::KeyChord newKeys)
{
const auto args{ make_self<ModifyKeyBindingEventArgs>(_CurrentKeys, // OldKeys
newKeys, // NewKeys
_IsNewlyAdded ? hstring{} : _CurrentAction, // OldAction
unbox_value<hstring>(_ProposedAction)) }; // NewAction
_ModifyKeyBindingRequestedHandlers(*this, *args);
}
void KeyBindingViewModel::CancelChanges()
{
if (_IsNewlyAdded)
{
_DeleteNewlyAddedKeyBindingHandlers(*this, nullptr);
}
else
{
ToggleEditMode();
}
}
ActionsViewModel::ActionsViewModel(Model::CascadiaSettings settings) :
_Settings{ settings }
{
// Populate AvailableActionAndArgs
_AvailableActionMap = single_threaded_map<hstring, Model::ActionAndArgs>();
std::vector<hstring> availableActionAndArgs;
for (const auto& [name, actionAndArgs] : _Settings.ActionMap().AvailableActions())
{
availableActionAndArgs.push_back(name);
_AvailableActionMap.Insert(name, actionAndArgs);
}
std::sort(begin(availableActionAndArgs), end(availableActionAndArgs));
_AvailableActionAndArgs = single_threaded_observable_vector(std::move(availableActionAndArgs));
// Convert the key bindings from our settings into a view model representation
const auto& keyBindingMap{ _Settings.ActionMap().KeyBindings() };
std::vector<Editor::KeyBindingViewModel> keyBindingList;
keyBindingList.reserve(keyBindingMap.Size());
for (const auto& [keys, cmd] : keyBindingMap)
{
// convert the cmd into a KeyBindingViewModel
auto container{ make_self<KeyBindingViewModel>(keys, cmd.Name(), _AvailableActionAndArgs) };
_RegisterEvents(container);
keyBindingList.push_back(*container);
}
std::sort(begin(keyBindingList), end(keyBindingList), KeyBindingViewModelComparator{});
_KeyBindingList = single_threaded_observable_vector(std::move(keyBindingList));
}
void ActionsViewModel::OnAutomationPeerAttached()
{
_AutomationPeerAttached = true;
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);
}
}
void ActionsViewModel::AddNewKeybinding()
{
// Create the new key binding and register all of the event handlers.
auto kbdVM{ make_self<KeyBindingViewModel>(_AvailableActionAndArgs) };
_RegisterEvents(kbdVM);
kbdVM->DeleteNewlyAddedKeyBinding({ this, &ActionsViewModel::_KeyBindingViewModelDeleteNewlyAddedKeyBindingHandler });
// Manually add the editing background. This needs to be done in Actions not the view model.
// We also have to do this manually because it hasn't been added to the list yet.
kbdVM->IsInEditMode(true);
// Emit an event to let the page know to update the background of this key binding VM
_UpdateBackgroundHandlers(*this, *kbdVM);
// IMPORTANT: do this _after_ setting IsInEditMode. Otherwise, it'll get deleted immediately
// by the PropertyChangedHandler below (where we delete any IsNewlyAdded items)
kbdVM->IsNewlyAdded(true);
_KeyBindingList.InsertAt(0, *kbdVM);
_FocusContainerHandlers(*this, *kbdVM);
}
void ActionsViewModel::_KeyBindingViewModelPropertyChangedHandler(const IInspectable& sender, const Windows::UI::Xaml::Data::PropertyChangedEventArgs& args)
{
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. any actions that were newly added are removed
// 3. this is the only entry that is in edit mode
for (int32_t i = _KeyBindingList.Size() - 1; i >= 0; --i)
{
const auto& kbdVM{ _KeyBindingList.GetAt(i) };
if (senderVM == kbdVM)
{
// This is the view model entry that went into edit mode.
// Emit an event to let the page know to move focus to
// this VM's container.
_FocusContainerHandlers(*this, senderVM);
}
else if (kbdVM.IsNewlyAdded())
{
// Remove any actions that were newly added
_KeyBindingList.RemoveAt(i);
}
else
{
// Exit edit mode for all other containers
get_self<KeyBindingViewModel>(kbdVM)->DisableEditMode();
}
}
}
else
{
// Emit an event to let the page know to move focus to
// this VM's container.
_FocusContainerHandlers(*this, senderVM);
}
// Emit an event to let the page know to update the background of this key binding VM
_UpdateBackgroundHandlers(*this, senderVM);
}
}
void ActionsViewModel::_KeyBindingViewModelDeleteKeyBindingHandler(const Editor::KeyBindingViewModel& senderVM, const Control::KeyChord& keys)
{
// Update the settings model
_Settings.ActionMap().DeleteKeyBinding(keys);
// Find the current container in our list and remove it.
// This is much faster than rebuilding the entire ActionMap.
uint32_t index;
if (_KeyBindingList.IndexOf(senderVM, index))
{
_KeyBindingList.RemoveAt(index);
// Focus the new item at this index
if (_KeyBindingList.Size() != 0)
{
const auto newFocusedIndex{ std::clamp(index, 0u, _KeyBindingList.Size() - 1) };
// Emit an event to let the page know to move focus to
// this VM's container.
_FocusContainerHandlers(*this, winrt::box_value(newFocusedIndex));
}
}
}
void ActionsViewModel::_KeyBindingViewModelModifyKeyBindingHandler(const Editor::KeyBindingViewModel& senderVM, const Editor::ModifyKeyBindingEventArgs& args)
{
const auto isNewAction{ !args.OldKeys() && args.OldActionName().empty() };
auto applyChangesToSettingsModel = [=]() {
// If the key chord was changed,
// update the settings model and view model appropriately
// NOTE: we still need to update the view model if we're working with a newly added action
if (isNewAction || args.OldKeys().Modifiers() != args.NewKeys().Modifiers() || args.OldKeys().Vkey() != args.NewKeys().Vkey())
{
if (!isNewAction)
{
// update settings model
_Settings.ActionMap().RebindKeys(args.OldKeys(), args.NewKeys());
}
// update view model
auto senderVMImpl{ get_self<KeyBindingViewModel>(senderVM) };
senderVMImpl->CurrentKeys(args.NewKeys());
}
// If the action was changed,
// update the settings model and view model appropriately
// NOTE: no need to check for "isNewAction" here. <empty_string> != <action name> already.
if (args.OldActionName() != args.NewActionName())
{
// convert the action's name into a view model.
const auto& newAction{ _AvailableActionMap.Lookup(args.NewActionName()) };
// update settings model
_Settings.ActionMap().RegisterKeyBinding(args.NewKeys(), newAction);
// update view model
auto senderVMImpl{ get_self<KeyBindingViewModel>(senderVM) };
senderVMImpl->CurrentAction(args.NewActionName());
senderVMImpl->IsNewlyAdded(false);
}
};
// Check for this special case:
// we're changing the key chord,
// but the new key chord is already in use
if (isNewAction || args.OldKeys().Modifiers() != args.NewKeys().Modifiers() || args.OldKeys().Vkey() != args.NewKeys().Vkey())
{
const auto& conflictingCmd{ _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
applyChangesToSettingsModel();
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);
}
}
// update settings model and view model
applyChangesToSettingsModel();
// We NEED to toggle the edit mode here,
// so that if nothing changed, we still exit
// edit mode.
senderVM.ToggleEditMode();
}
void ActionsViewModel::_KeyBindingViewModelDeleteNewlyAddedKeyBindingHandler(const Editor::KeyBindingViewModel& senderVM, const IInspectable& /*args*/)
{
for (uint32_t i = 0; i < _KeyBindingList.Size(); ++i)
{
const auto& kbdVM{ _KeyBindingList.GetAt(i) };
if (kbdVM == senderVM)
{
_KeyBindingList.RemoveAt(i);
return;
}
}
}
// 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> ActionsViewModel::_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->CurrentKeys() };
if (otherKeys && 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;
}
void ActionsViewModel::_RegisterEvents(com_ptr<KeyBindingViewModel>& kbdVM)
{
kbdVM->PropertyChanged({ this, &ActionsViewModel::_KeyBindingViewModelPropertyChangedHandler });
kbdVM->DeleteKeyBindingRequested({ this, &ActionsViewModel::_KeyBindingViewModelDeleteKeyBindingHandler });
kbdVM->ModifyKeyBindingRequested({ this, &ActionsViewModel::_KeyBindingViewModelModifyKeyBindingHandler });
kbdVM->IsAutomationPeerAttached(_AutomationPeerAttached);
}
}

View File

@@ -0,0 +1,131 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#include "ActionsViewModel.g.h"
#include "KeyBindingViewModel.g.h"
#include "ModifyKeyBindingEventArgs.g.h"
#include "Utils.h"
#include "ViewModelHelpers.h"
namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
struct KeyBindingViewModelComparator
{
bool operator()(const Editor::KeyBindingViewModel& lhs, const Editor::KeyBindingViewModel& rhs) const
{
return lhs.Name() < rhs.Name();
}
};
struct ModifyKeyBindingEventArgs : ModifyKeyBindingEventArgsT<ModifyKeyBindingEventArgs>
{
public:
ModifyKeyBindingEventArgs(const Control::KeyChord& oldKeys, const Control::KeyChord& newKeys, const hstring oldActionName, const hstring newActionName) :
_OldKeys{ oldKeys },
_NewKeys{ newKeys },
_OldActionName{ std::move(oldActionName) },
_NewActionName{ std::move(newActionName) } {}
WINRT_PROPERTY(Control::KeyChord, OldKeys, nullptr);
WINRT_PROPERTY(Control::KeyChord, NewKeys, nullptr);
WINRT_PROPERTY(hstring, OldActionName);
WINRT_PROPERTY(hstring, NewActionName);
};
struct KeyBindingViewModel : KeyBindingViewModelT<KeyBindingViewModel>, ViewModelHelper<KeyBindingViewModel>
{
public:
KeyBindingViewModel(const Windows::Foundation::Collections::IObservableVector<hstring>& availableActions);
KeyBindingViewModel(const Control::KeyChord& keys, const hstring& name, const Windows::Foundation::Collections::IObservableVector<hstring>& availableActions);
hstring Name() const { return _CurrentAction; }
hstring KeyChordText() const { return _KeyChordText; }
// 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 ActionGotFocus() { IsContainerFocused(true); };
void ActionLostFocus() { IsContainerFocused(false); };
void EditButtonGettingFocus() { IsEditButtonFocused(true); };
void EditButtonLosingFocus() { IsEditButtonFocused(false); };
bool ShowEditButton() const noexcept;
void ToggleEditMode();
void DisableEditMode() { IsInEditMode(false); }
void AttemptAcceptChanges();
void AttemptAcceptChanges(const Control::KeyChord newKeys);
void CancelChanges();
void DeleteKeyBinding() { _DeleteKeyBindingRequestedHandlers(*this, _CurrentKeys); }
// ProposedAction: the entry selected by the combo box; may disagree with the settings model.
// CurrentAction: the combo box item that maps to the settings model value.
// AvailableActions: the list of options in the combo box; both actions above must be in this list.
// NOTE: ProposedAction and CurrentAction may disagree mainly due to the "edit mode" system in place.
// Current Action serves as...
// 1 - a record of what to set ProposedAction to on a cancellation
// 2 - a form of translation between ProposedAction and the settings model
// We would also need an ActionMap reference to remove this, but this is a better separation
// of responsibilities.
VIEW_MODEL_OBSERVABLE_PROPERTY(IInspectable, ProposedAction);
VIEW_MODEL_OBSERVABLE_PROPERTY(hstring, CurrentAction);
WINRT_PROPERTY(Windows::Foundation::Collections::IObservableVector<hstring>, AvailableActions, nullptr);
// ProposedKeys: the keys proposed by the control; may disagree with the settings model.
// CurrentKeys: the key chord bound in the settings model.
VIEW_MODEL_OBSERVABLE_PROPERTY(Control::KeyChord, ProposedKeys);
VIEW_MODEL_OBSERVABLE_PROPERTY(Control::KeyChord, CurrentKeys, nullptr);
VIEW_MODEL_OBSERVABLE_PROPERTY(bool, IsInEditMode, false);
VIEW_MODEL_OBSERVABLE_PROPERTY(bool, IsNewlyAdded, false);
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(ModifyKeyBindingRequested, Editor::KeyBindingViewModel, Editor::ModifyKeyBindingEventArgs);
TYPED_EVENT(DeleteKeyBindingRequested, Editor::KeyBindingViewModel, Terminal::Control::KeyChord);
TYPED_EVENT(DeleteNewlyAddedKeyBinding, Editor::KeyBindingViewModel, IInspectable);
private:
hstring _KeyChordText{};
};
struct ActionsViewModel : ActionsViewModelT<ActionsViewModel>, ViewModelHelper<ActionsViewModel>
{
public:
ActionsViewModel(Model::CascadiaSettings settings);
void OnAutomationPeerAttached();
void AddNewKeybinding();
WINRT_PROPERTY(Windows::Foundation::Collections::IObservableVector<Editor::KeyBindingViewModel>, KeyBindingList);
TYPED_EVENT(FocusContainer, IInspectable, IInspectable);
TYPED_EVENT(UpdateBackground, IInspectable, IInspectable);
private:
bool _AutomationPeerAttached{ false };
Model::CascadiaSettings _Settings;
Windows::Foundation::Collections::IObservableVector<hstring> _AvailableActionAndArgs;
Windows::Foundation::Collections::IMap<hstring, Model::ActionAndArgs> _AvailableActionMap;
std::optional<uint32_t> _GetContainerIndexByKeyChord(const Control::KeyChord& keys);
void _RegisterEvents(com_ptr<implementation::KeyBindingViewModel>& kbdVM);
void _KeyBindingViewModelPropertyChangedHandler(const Windows::Foundation::IInspectable& senderVM, const Windows::UI::Xaml::Data::PropertyChangedEventArgs& args);
void _KeyBindingViewModelDeleteKeyBindingHandler(const Editor::KeyBindingViewModel& senderVM, const Control::KeyChord& args);
void _KeyBindingViewModelModifyKeyBindingHandler(const Editor::KeyBindingViewModel& senderVM, const Editor::ModifyKeyBindingEventArgs& args);
void _KeyBindingViewModelDeleteNewlyAddedKeyBindingHandler(const Editor::KeyBindingViewModel& senderVM, const IInspectable& args);
};
}
namespace winrt::Microsoft::Terminal::Settings::Editor::factory_implementation
{
BASIC_FACTORY(ActionsViewModel);
}

View File

@@ -0,0 +1,60 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
namespace Microsoft.Terminal.Settings.Editor
{
runtimeclass ModifyKeyBindingEventArgs
{
Microsoft.Terminal.Control.KeyChord OldKeys { get; };
Microsoft.Terminal.Control.KeyChord NewKeys { get; };
String OldActionName { get; };
String NewActionName { 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; };
Boolean IsNewlyAdded { get; };
Microsoft.Terminal.Control.KeyChord ProposedKeys;
Object ProposedAction;
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 ActionGotFocus();
void ActionLostFocus();
void EditButtonGettingFocus();
void EditButtonLosingFocus();
IObservableVector<String> AvailableActions { get; };
void ToggleEditMode();
void AttemptAcceptChanges();
void CancelChanges();
void DeleteKeyBinding();
event Windows.Foundation.TypedEventHandler<KeyBindingViewModel, ModifyKeyBindingEventArgs> ModifyKeyBindingRequested;
event Windows.Foundation.TypedEventHandler<KeyBindingViewModel, Microsoft.Terminal.Control.KeyChord> DeleteKeyBindingRequested;
}
runtimeclass ActionsViewModel : Windows.UI.Xaml.Data.INotifyPropertyChanged
{
ActionsViewModel(Microsoft.Terminal.Settings.Model.CascadiaSettings settings);
void OnAutomationPeerAttached();
void AddNewKeybinding();
IObservableVector<KeyBindingViewModel> KeyBindingList { get; };
event Windows.Foundation.TypedEventHandler<Object, Object> FocusContainer;
event Windows.Foundation.TypedEventHandler<Object, Object> UpdateBackground;
}
}

View File

@@ -374,7 +374,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
}
else if (clickedItemTag == actionsTag)
{
contentFrame().Navigate(xaml_typename<Editor::Actions>(), winrt::make<ActionsPageNavigationState>(_settingsClone));
contentFrame().Navigate(xaml_typename<Editor::Actions>(), winrt::make<ActionsViewModel>(_settingsClone));
const auto crumb = winrt::make<Breadcrumb>(box_value(clickedItemTag), RS_(L"Nav_Actions/Content"), BreadcrumbSubPage::None);
_breadcrumbs.Append(crumb);
}

View File

@@ -82,6 +82,10 @@
<DependentUpon>ProfileViewModel.idl</DependentUpon>
<SubType>Code</SubType>
</ClInclude>
<ClInclude Include="ActionsViewModel.h">
<DependentUpon>ActionsViewModel.idl</DependentUpon>
<SubType>Code</SubType>
</ClInclude>
<ClInclude Include="ColorSchemeViewModel.h">
<DependentUpon>ColorSchemeViewModel.idl</DependentUpon>
<SubType>Code</SubType>
@@ -225,6 +229,10 @@
<DependentUpon>ProfileViewModel.idl</DependentUpon>
<SubType>Code</SubType>
</ClCompile>
<ClCompile Include="ActionsViewModel.cpp">
<DependentUpon>ActionsViewModel.idl</DependentUpon>
<SubType>Code</SubType>
</ClCompile>
<ClCompile Include="ColorSchemeViewModel.cpp">
<DependentUpon>ColorSchemeViewModel.idl</DependentUpon>
<SubType>Code</SubType>
@@ -320,6 +328,7 @@
<DependentUpon>MainPage.xaml</DependentUpon>
</Midl>
<Midl Include="ProfileViewModel.idl" />
<Midl Include="ActionsViewModel.idl" />
<Midl Include="ColorSchemeViewModel.idl" />
<Midl Include="ColorSchemesPageViewModel.idl" />
<Midl Include="RenderingViewModel.idl" />

View File

@@ -18,6 +18,7 @@
</ItemGroup>
<ItemGroup>
<Midl Include="ProfileViewModel.idl" />
<Midl Include="ActionsViewModel.idl" />
<Midl Include="ColorSchemeViewModel.idl" />
<Midl Include="ColorSchemesPageViewModel.idl" />
<Midl Include="RenderingViewModel.idl" />

View File

@@ -27,7 +27,6 @@ static constexpr std::string_view ArgsKey{ "args" };
static constexpr std::string_view IterateOnKey{ "iterateOn" };
static constexpr std::string_view CommandsKey{ "commands" };
static constexpr std::string_view KeysKey{ "keys" };
static constexpr std::string_view DescriptionKey{ "description" };
static constexpr std::string_view ProfileNameToken{ "${profile.name}" };
static constexpr std::string_view ProfileIconToken{ "${profile.icon}" };
@@ -45,7 +44,6 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
command->_keyMappings = _keyMappings;
command->_iconPath = _iconPath;
command->_IterateOn = _IterateOn;
command->_Description = _Description;
command->_originalJson = _originalJson;
command->_nestedCommand = _nestedCommand;
@@ -266,7 +264,6 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
auto nested = false;
JsonUtils::GetValueForKey(json, IterateOnKey, result->_IterateOn);
JsonUtils::GetValueForKey(json, DescriptionKey, result->_Description);
// For iterable commands, we'll make another pass at parsing them once
// the json is patched. So ignore parsing sub-commands for now. Commands
@@ -419,7 +416,6 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
Json::Value cmdJson{ Json::ValueType::objectValue };
JsonUtils::SetValueForKey(cmdJson, IconKey, _iconPath);
JsonUtils::SetValueForKey(cmdJson, NameKey, _name);
JsonUtils::SetValueForKey(cmdJson, DescriptionKey, _Description);
if (_ActionAndArgs)
{
@@ -440,7 +436,6 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
// First iteration also writes icon and name
JsonUtils::SetValueForKey(cmdJson, IconKey, _iconPath);
JsonUtils::SetValueForKey(cmdJson, NameKey, _name);
JsonUtils::SetValueForKey(cmdJson, DescriptionKey, _Description);
}
if (_ActionAndArgs)
@@ -669,10 +664,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
const auto parseElement = [&](const auto& element) {
winrt::hstring completionText;
winrt::hstring listText;
winrt::hstring tooltipText;
JsonUtils::GetValueForKey(element, "CompletionText", completionText);
JsonUtils::GetValueForKey(element, "ListItemText", listText);
JsonUtils::GetValueForKey(element, "ToolTip", tooltipText);
auto args = winrt::make_self<SendInputArgs>(
winrt::hstring{ fmt::format(FMT_COMPILE(L"{:\x7f^{}}{}"),
@@ -684,8 +677,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
auto c = winrt::make_self<Command>();
c->_name = listText;
c->_Description = tooltipText;
c->_ActionAndArgs = actionAndArgs;
// Try to assign a sensible icon based on the result type. These are
// roughly chosen to align with the icons in
// https://github.com/PowerShell/PowerShellEditorServices/pull/1738

View File

@@ -74,7 +74,6 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
WINRT_PROPERTY(ExpandCommandType, IterateOn, ExpandCommandType::None);
WINRT_PROPERTY(Model::ActionAndArgs, ActionAndArgs);
WINRT_PROPERTY(winrt::hstring, Description, L"");
private:
Json::Value _originalJson;

View File

@@ -36,7 +36,6 @@ namespace Microsoft.Terminal.Settings.Model
Command();
String Name { get; };
String Description { get; };
ActionAndArgs ActionAndArgs { get; };
Microsoft.Terminal.Control.KeyChord Keys { get; };
void RegisterKey(Microsoft.Terminal.Control.KeyChord keys);

View File

@@ -265,7 +265,12 @@ void BackendD3D::_handleSettingsUpdate(const RenderingPayload& p)
{
wil::com_ptr<ID3D11Texture2D> buffer;
THROW_IF_FAILED(p.swapChain.swapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), reinterpret_cast<void**>(buffer.addressof())));
THROW_IF_FAILED(p.device->CreateRenderTargetView(buffer.get(), nullptr, _renderTargetView.put()));
static constexpr D3D11_RENDER_TARGET_VIEW_DESC desc{
.Format = DXGI_FORMAT_B8G8R8A8_UNORM_SRGB,
.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D,
};
THROW_IF_FAILED(p.device->CreateRenderTargetView(buffer.get(), &desc, _renderTargetView.put()));
}
const auto fontChanged = _fontGeneration != p.s->font.generation();
@@ -547,7 +552,7 @@ void BackendD3D::_recreateBackgroundColorBitmap(const RenderingPayload& p)
.Height = p.s->viewportCellCount.y,
.MipLevels = 1,
.ArraySize = 1,
.Format = DXGI_FORMAT_R8G8B8A8_UNORM,
.Format = DXGI_FORMAT_R8G8B8A8_UNORM_SRGB,
.SampleDesc = { 1, 0 },
.Usage = D3D11_USAGE_DYNAMIC,
.BindFlags = D3D11_BIND_SHADER_RESOURCE,

View File

@@ -35,6 +35,7 @@ float3 DWrite_EnhanceContrast3(float3 alpha, float k)
return alpha * (k + 1.0f) / (alpha * k + 1.0f);
}
// dwrite vs. gamma 1.8: https://www.desmos.com/calculator/6wocsr6vcq
float DWrite_ApplyAlphaCorrection(float a, float f, float4 g)
{
return a + a * (1 - a) * ((g.x * f + g.y) * a + (g.z * f + g.w));

View File

@@ -27,7 +27,7 @@ struct PSData
float4 position : SV_Position;
float2 texcoord : texcoord;
nointerpolation uint shadingType : shadingType;
nointerpolation uint2 renditionScale : renditionScale;
nointerpolation float2 renditionScale : renditionScale;
nointerpolation float4 color : color;
};

View File

@@ -44,27 +44,13 @@ Output main(PSData data) : SV_Target
}
case SHADING_TYPE_TEXT_GRAYSCALE:
{
// These are independent of the glyph texture and could be moved to the vertex shader or CPU side of things.
const float4 foreground = premultiplyColor(data.color);
const float blendEnhancedContrast = DWrite_ApplyLightOnDarkContrastAdjustment(enhancedContrast, data.color.rgb);
const float intensity = DWrite_CalcColorIntensity(data.color.rgb);
// These aren't.
const float4 glyph = glyphAtlas[data.texcoord];
const float contrasted = DWrite_EnhanceContrast(glyph.a, blendEnhancedContrast);
const float alphaCorrected = DWrite_ApplyAlphaCorrection(contrasted, intensity, gammaRatios);
color = alphaCorrected * foreground;
color = glyphAtlas[data.texcoord].a * data.color;
weights = color.aaaa;
break;
}
case SHADING_TYPE_TEXT_CLEARTYPE:
{
// These are independent of the glyph texture and could be moved to the vertex shader or CPU side of things.
const float blendEnhancedContrast = DWrite_ApplyLightOnDarkContrastAdjustment(enhancedContrast, data.color.rgb);
// These aren't.
const float4 glyph = glyphAtlas[data.texcoord];
const float3 contrasted = DWrite_EnhanceContrast3(glyph.rgb, blendEnhancedContrast);
const float3 alphaCorrected = DWrite_ApplyAlphaCorrection3(contrasted, data.color.rgb, gammaRatios);
weights = float4(alphaCorrected * data.color.a, 1);
weights = float4(glyphAtlas[data.texcoord].rgb * data.color.a, 1);
color = weights * data.color;
break;
}
@@ -77,14 +63,14 @@ Output main(PSData data) : SV_Target
case SHADING_TYPE_DOTTED_LINE:
{
const bool on = frac(data.position.x / (2.0f * underlineWidth * data.renditionScale.x)) < 0.5f;
color = on * premultiplyColor(data.color);
color = on * data.color;
weights = color.aaaa;
break;
}
case SHADING_TYPE_DASHED_LINE:
{
const bool on = frac(data.position.x / (backgroundCellSize.x * data.renditionScale.x)) < 0.5f;
color = on * premultiplyColor(data.color);
color = on * data.color;
weights = color.aaaa;
break;
}
@@ -102,13 +88,13 @@ Output main(PSData data) : SV_Target
const float s = sin(data.position.x * freq);
const float d = abs(centerY - (s * amp) - data.position.y);
const float a = 1 - saturate(d - strokeWidthHalf);
color = a * premultiplyColor(data.color);
color = a * data.color;
weights = color.aaaa;
break;
}
default:
{
color = premultiplyColor(data.color);
color = data.color;
weights = color.aaaa;
break;
}

View File

@@ -3,6 +3,8 @@
#include "shader_common.hlsl"
#pragma warning(disable: 3571) // pow(f, e) will not work for negative f, use abs(f) or conditionally handle negative values if you expect them
cbuffer ConstBuffer : register(b0)
{
float2 positionScale;
@@ -13,7 +15,7 @@ PSData main(VSData data)
// clang-format on
{
PSData output;
output.color = data.color;
output.color = float4(pow(data.color.rgb * data.color.a, 2.2), data.color.a);
output.shadingType = data.shadingType;
output.renditionScale = data.renditionScale;
// positionScale is expected to be float2(2.0f / sizeInPixel.x, -2.0f / sizeInPixel.y). Together with the

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net461</TargetFramework>
<TargetFramework>net472</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<LangVersion>latest</LangVersion>
</PropertyGroup>

View File

@@ -24,14 +24,12 @@ try
_pProvider = pProvider;
_pData = pData;
_pData->LockConsole();
_start = pData->GetViewport().Origin();
_end = pData->GetViewport().Origin();
_blockRange = false;
_wordDelimiters = wordDelimiters;
UiaTracing::TextRange::Constructor(*this);
_pData->UnlockConsole();
return S_OK;
}
CATCH_RETURN();