-
Notifications
You must be signed in to change notification settings - Fork 8.6k
Allow editing actions in the settings UI #18917
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev/pabhoj/settings_model_reflection
Are you sure you want to change the base?
Allow editing actions in the settings UI #18917
Conversation
…ettings_actions_editor
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
…ettings_actions_editor
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Other feedback:
- "Clear buffer" action is being shown as "Clear mark" (binding to ctrl+shift+k shown correctly)
Style="{StaticResource KeyBindingContainerStyle}"> | ||
<Grid ColumnSpacing="8"> | ||
<Grid.ColumnDefinitions> | ||
<!-- command name --> | ||
<ColumnDefinition Width="*" /> | ||
<!-- key chord --> | ||
<ColumnDefinition Width="auto" /> | ||
<ColumnDefinition Width="150" /> | ||
<!-- edit buttons --> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't need the third column anymore since we don't have the edit buttons
<!-- Key Chord Text --> | ||
<Border Grid.Column="1" | ||
Padding="2,0,2,0" | ||
HorizontalAlignment="Right" | ||
VerticalAlignment="Center" | ||
Style="{ThemeResource KeyChordBorderStyle}" | ||
Visibility="{x:Bind mtu:Converters.InvertedBooleanToVisibility(IsInEditMode), Mode=OneWay}"> | ||
|
||
<TextBlock FontSize="14" | ||
Style="{ThemeResource KeyChordTextBlockStyle}" | ||
Text="{x:Bind KeyChordText, Mode=OneWay}" | ||
TextWrapping="WrapWholeWords" /> | ||
</Border> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Key bindings aren't shown anymore
<TextBlock Style="{StaticResource DisclaimerStyle}" | ||
TextWrapping="WrapWholeWords"> | ||
<Hyperlink NavigateUri="https://learn.microsoft.com/en-us/windows/terminal/customize-settings/actions" | ||
TextDecorations="None"> | ||
<Run x:Uid="Actions_Disclaimer" /> | ||
</Hyperlink> | ||
</TextBlock> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you do a HyperlinkButton
, you won't need a TextBlock and Run anymore.
<!-- Cancel editing the action --> | ||
<Button x:Uid="Actions_CancelButton" | ||
Grid.Column="1" | ||
Click="{x:Bind CancelChanges}" | ||
Style="{StaticResource EditButtonStyle}"> | ||
<FontIcon FontSize="{StaticResource EditButtonIconSize}" | ||
Glyph="" /> | ||
</Button> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Keyboard navigation] invoking "cancel" button when editing a key binding yeets focus
<!-- Accept changes --> | ||
<Button x:Uid="Actions_AcceptButton" | ||
Grid.Column="2" | ||
Click="{x:Bind AttemptAcceptChanges}" | ||
Flyout="{x:Bind AcceptChangesFlyout, Mode=OneWay}" | ||
Style="{StaticResource AccentEditButtonStyle}"> | ||
<FontIcon FontSize="{StaticResource EditButtonIconSize}" | ||
Glyph="" /> | ||
</Button> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Keyboard navigation] invoking "accept" button when editing a key binding yeets focus
<!-- Templates --> | ||
<DataTemplate x:Key="KeyChordTemplate" | ||
x:DataType="local:KeyChordViewModel"> | ||
<ListViewItem Style="{StaticResource KeyBindingContainerStyle}"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Keyboard navigation] So right now, the keybindings list has 3 tabbable UI elements: the container, the key chord, and the delete button.
If we set IsTabStop
to False
on the ListViewItem
(aka, the container), we can get rid of that first one. It also doesn't have an automation property, but if we just have it not be a focusable element, we're good on that one.
The only thing I'm cautious about (and you'll have to test this out), is how you navigate from key binding to key binding. I suspect that after this change, you may have to tab in this order: (1) 1st key chord -> (2) delete -> (3) 2nd key chord... etc.
Ideally, if you press the up/down arrows, you should be able to go from (1) directly to (3) and vice versa. If that doesn't work right off the bat, try using XYFocusKeyboardNavigation
(probably on the button below) to see if that fixes it. I think you should be good though.
Style="{StaticResource KeyChordEditorStyle}" /> | ||
|
||
<!-- Cancel editing the action --> | ||
<Button x:Uid="Actions_CancelButton" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Screen reader reads this as just "button" (aka needs an AP.name)
</Button> | ||
|
||
<!-- Accept changes --> | ||
<Button x:Uid="Actions_AcceptButton" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Screen reader reads this as just "button" (aka needs an AP.name)
Glyph="" /> | ||
</Button> | ||
</Grid> | ||
<Button Grid.Column="1" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Screen reader reads this as just "button" (aka needs an AP.name)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It'd be really nice if you added a comment by each of the ArgWrapper data templates saying what's a good action type and arg combo to test this out. I've been manually going through ActionArgs.h to figure this out. Not the end of the world, but it'd be a nice QOL improvement 😁
Almost done reviewing this. Just need to go over ActionsViewModel
<DataTemplate x:Key="ListItemTemplate" | ||
x:DataType="local:ArgWrapper"> | ||
<ListViewItem HorizontalContentAlignment="Stretch" /> | ||
</DataTemplate> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like ListItemTemplate
is not being used anywhere?
<DataTemplate x:Key="NoArgTemplate" | ||
x:DataType="local:ArgWrapper"> | ||
<ListViewItem /> | ||
</DataTemplate> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, how is this used? Like, I get that it's passed into the ArgsTemplateSelectors
and if we can't find a good template, we use this one. But from that point forward, what's done? Is there an example of a setting that uses this? Any reason we can't just return nullptr
instead of a blank ListViewItem?
I'm also wondering if we should add an assert in the template selector for when we use this template, because that means that we have an arg that we don't know how to represent, right?
<ListViewItem /> | ||
</DataTemplate> | ||
|
||
<DataTemplate x:Key="Int32Template" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📝 type = "Adjust Opacity" is a good example for this
|
||
<DataTemplate x:Key="Int32Template" | ||
x:DataType="local:ArgWrapper"> | ||
<ListViewItem> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Keyboard navigation] Tabbing lets us move our focus into 3 spots here (in order): (1) 1st list view item -> (2) 2nd list view item -> (3) number box
I suspect that the redundant list view item is from the first ListViewItem
in the data template. I think you should be able to remove it because the data template controls what's inside the ListViewItem already. You can repeat this for all the other data templates here.
As for the other list view item, you'll want to unset it as a tab stop. I think you can do that with ItemContainerStyle
on the ListView
itself. Create a style with IsTabStop
= False
and pass it in. That way, the user can tab directly into the number box (or whatever control is being used).
It's worth checking the UIA tree with Accessibility Insights too. Idk if you'll need to change the AccessibilityView
on these to hide them from the tree, of if just removing the tab stop is enough.
<TextBlock Grid.Column="0" | ||
VerticalAlignment="Center" | ||
Text="{x:Bind Name}" /> | ||
<muxc:NumberBox Grid.Column="1" | ||
LargeChange="1" | ||
Maximum="999" | ||
Minimum="0" | ||
SmallChange="1" | ||
Style="{StaticResource NumberBoxSettingStyle}" | ||
Value="{x:Bind UnboxInt32(Value), Mode=TwoWay, BindBack=Int32BindBack}" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah! Common trap for screen readers! So, visually, looks great! It's clear to a visual user that the text block is the header for the control. But with a screen reader, there isn't an association at all. So when I go over the control with a screen reader, it'll just say "number box" and not tell me what the number box actually represents.
To fix this, you're going to have to set the AutomationProperties.Name
to {x:Bind Name}
.
Don't forget to do this to all the other controls
</ListViewItem> | ||
</DataTemplate> | ||
|
||
<DataTemplate x:Key="WindowsUIColorOptionalTemplate" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📝 type = "Set tab color" is a good example for this
<TextBlock Grid.Column="0" | ||
VerticalAlignment="Center" | ||
Text="{x:Bind Name}" /> | ||
<local:NullableColorPicker x:Uid="Actions_NullableColorPicker" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Null color is "No color"? Hmm... but I'm not sure how to handle this one exactly either. For Tab color, it opens the color picker. But for new terminal args, it... what, uses the theme color?
<TextBlock x:Uid="Actions_Name" | ||
Grid.Row="0" | ||
Grid.Column="0" | ||
VerticalAlignment="Center" /> | ||
<TextBox Grid.Row="0" | ||
Grid.Column="1" | ||
Width="300" | ||
HorizontalAlignment="Left" | ||
PlaceholderText="{x:Bind ViewModel.DisplayName, Mode=OneWay}" | ||
Text="{x:Bind ViewModel.Name, Mode=TwoWay}" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Each of these pairs has the same problem I mentioned earlier with a screen reader. Visually, we can tell that the text block is the header for the control. With a screen reader, we don't have that, so we need to set the AutomationProperty.Name manually
Grid.Row="3" | ||
Grid.Column="0" | ||
VerticalAlignment="Center" /> | ||
<ListView Grid.Row="3" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider ItemsRepeater instead of ListView. We're not using the functionality of the ListView, really. We just want a stack panel layout of our own controls.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ItemsControl
works too. I used it over in my extensions page:
<ItemsControl x:Name="ActiveExtensionsList" |
<ListView Grid.Row="3" | ||
Grid.Column="1" | ||
ItemTemplate="{StaticResource KeyChordTemplate}" | ||
ItemsSource="{x:Bind ViewModel.KeyChordViewModelList, Mode=OneWay}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright! Finally finished reviewing everything. Left a bunch of comments all around. Tried to test things out with the bug bash build as I went and did some accessibility testing too. I recommend that after you get the accessibility stuff in, you run it through Accessibility Insights' FastPass to check for common issues. That said, I think once you fix the comments I added in, you should be pretty much good to go there.
There's a bit more cacheing that can be done. I think it's worth it since we've got A TON of commands/actions. There was also a concern I had about null-checking somewhere in there so we probably want to fix that.
The ArgsWrapper stuff is really cool. Good work all-around dude 😊. Excited to see this land when you make the changes. We're close!
@@ -18,383 +24,1194 @@ using namespace winrt::Windows::UI::Xaml::Data; | |||
using namespace winrt::Windows::UI::Xaml::Navigation; | |||
using namespace winrt::Microsoft::Terminal::Settings::Model; | |||
|
|||
// todo: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair. We should file an issue and link it here for tracking.
I wonder if in the near term, we could add support for them, but just expose a text box. It wouldn't be ideal, but we'd still show the actions in the main list and users would be able to view/modify them. Then a cleaner UI could be added in later (possibly even by the community).
@@ -4,127 +4,248 @@ | |||
#pragma once |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we have a ton of structs here and they all work closely together, mind adding an abstract to the top of this file explaining the relationships and responsibilities of each of them? It'll go a long way 😊
} \ | ||
std::sort(enumList.begin(), enumList.end(), EnumEntryReverseComparator<enumType>()); \ | ||
_EnumList = winrt::single_threaded_observable_vector<winrt::Microsoft::Terminal::Settings::Editor::EnumEntry>(std::move(enumList)); \ | ||
_NotifyChanges(L"EnumList", L"EnumValue"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Regarding this _NotifyChanges()
and all the other macros that have a similar one)
Who's listening to these? It looks like we don't have a PropertyChanged
handler for specifically these values.
Conversely, shouldn't the property changed events be more specific? Like, shouldn't it notify L## #enumMappingsName L"EnumList"
to pass over the specific one?
std::vector<winrt::Microsoft::Terminal::Settings::Editor::EnumEntry> enumList; \ | ||
const auto mappings = winrt::Microsoft::Terminal::Settings::Model::EnumMappings::enumMappingsName(); \ | ||
enumType unboxedValue; \ | ||
auto nullEntry = winrt::make<winrt::Microsoft::Terminal::Settings::Editor::implementation::EnumEntry>(RS_(L"Actions_NullEnumValue"), nullptr); \ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Somewhere I mentioned that we should provide a more specific string for the null value. Looks like this is the spot to do it. You can probably just move the resource ID into the macro definition
std::vector<winrt::Microsoft::Terminal::Settings::Editor::FlagEntry> flagList; \ | ||
const auto mappings = winrt::Microsoft::Terminal::Settings::Model::EnumMappings::enumMappingsName(); \ | ||
enumType unboxedValue{ 0 }; \ | ||
auto nullEntry = winrt::make<winrt::Microsoft::Terminal::Settings::Editor::implementation::FlagEntry>(RS_(L"Actions_NullEnumValue"), nullptr, true); \ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here. You can probably move this up into the macro definition to have it be a custom string
// even though the arg type is technically a string, we want an enum list for color schemes specifically | ||
std::vector<winrt::Microsoft::Terminal::Settings::Editor::EnumEntry> namesList; | ||
const auto currentSchemeName = unbox_value<winrt::hstring>(_Value); | ||
auto nullEntry = winrt::make<winrt::Microsoft::Terminal::Settings::Editor::implementation::EnumEntry>(RS_(L"Actions_NullEnumValue"), nullptr); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This may be where you can add a custom value for the null
enum value. Maybe hardcode a Name
check so that we do a different one depending on the setting?
// unboxing functions | ||
winrt::hstring UnboxString(const Windows::Foundation::IInspectable& value); | ||
winrt::hstring UnboxGuid(const Windows::Foundation::IInspectable& value); | ||
int32_t UnboxInt32(const Windows::Foundation::IInspectable& value); | ||
float UnboxInt32Optional(const Windows::Foundation::IInspectable& value); | ||
uint32_t UnboxUInt32(const Windows::Foundation::IInspectable& value); | ||
float UnboxUInt32Optional(const Windows::Foundation::IInspectable& value); | ||
float UnboxUInt64(const Windows::Foundation::IInspectable& value); | ||
float UnboxFloat(const Windows::Foundation::IInspectable& value); | ||
bool UnboxBool(const Windows::Foundation::IInspectable& value); | ||
winrt::Windows::Foundation::IReference<bool> UnboxBoolOptional(const Windows::Foundation::IInspectable& value); | ||
winrt::Windows::Foundation::IReference<Microsoft::Terminal::Core::Color> UnboxTerminalCoreColorOptional(const Windows::Foundation::IInspectable& value); | ||
winrt::Windows::Foundation::IReference<Microsoft::Terminal::Core::Color> UnboxWindowsUIColorOptional(const Windows::Foundation::IInspectable& value); | ||
|
||
// bind back functions | ||
void StringBindBack(const winrt::hstring& newValue); | ||
void GuidBindBack(const winrt::hstring& newValue); | ||
void Int32BindBack(const double newValue); | ||
void Int32OptionalBindBack(const double newValue); | ||
void UInt32BindBack(const double newValue); | ||
void UInt32OptionalBindBack(const double newValue); | ||
void UInt64BindBack(const double newValue); | ||
void FloatBindBack(const double newValue); | ||
void BoolOptionalBindBack(const Windows::Foundation::IReference<bool> newValue); | ||
void TerminalCoreColorBindBack(const winrt::Windows::Foundation::IReference<Microsoft::Terminal::Core::Color> newValue); | ||
void WindowsUIColorBindBack(const winrt::Windows::Foundation::IReference<Microsoft::Terminal::Core::Color> newValue); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are all pretty simple functions, and that's great. Would there be any benefit to moving these over to be converters so they can be used throughout the project? Or am I overengineering it? 😅
// Populate AvailableActionsAndNames | ||
_AvailableActionsAndNamesMap = Model::CascadiaSettings::AvailableShortcutActionsAndNames(); | ||
for (const auto unimplemented : UnimplementedShortcutActions) | ||
{ | ||
_AvailableActionsAndNamesMap.Remove(unimplemented); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can probably cache this, right? Looks like CascadiaSettings::AvailableShortcutActionsAndNames
does the work each time we call it, then we'll remove the same 2 unimplemented shortcut actions. Maybe we should just store it as a static so we don't regenerate it? It shouldn't change at runtime.
|
||
void ActionsViewModel::AttemptDeleteKeyChord(const Control::KeyChord& keys) | ||
{ | ||
// Update the settings model |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// Update the settings model | |
// Update the settings model | |
assert(keys); |
Thoughts on adding an assert
here? If somebody is calling this with an empty keys
, they're probably holding it wrong, right? So maybe we should notify the developer that they're not using it right.
if (propertyName == L"ProposedAction") | ||
{ | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't do anything? Something seems to be missing here.
…ettings_actions_editor
Oh! Also add in the "New" badge. You should be able to add it like the same way I did for the Extensions page. Just look at |
Summary of the Pull Request
Targets #18915
The actions page now has a list of all the commands (default, user, fragments etc) and clicking a command from that page brings you to an "Edit action" page where you can fully view and edit both the action type and any additional arguments.
One todo remaining that shouldn't block reviews:
Detailed Description of the Pull Request / Additional comments
Actions View Model
CommandViewModel
(view model for aCommand
), a list of these is created and managed byActionsViewModel
ActionArgsViewModel
(view model for anActionArgs
), created and managed byCommandViewModel
ArgWrapper
(view model for each individual argument inside anActionArgs
), created and managed byActionArgsViewModel
Actions page
EditAction page
CommandViewModel
Validation Steps Performed
Bug bashed
PR Checklist