Skip to content

Commit e332c67

Browse files
Add an Extensions page to the Settings UI (#18559)
This pull request adds an Extensions page to the Settings UI, which lets you enable/disable extensions and see how they affect your settings (i.e. adding/modifying profiles and adding color schemes). This page is specifically designed for fragment extensions and dynamic profile generators, but can be expanded on in the future as we develop a more advanced extensions model. App extensions extract the name and icon from the extension package and display it in the UI. Dynamic profile generators extract the name and icon from the generator and display it in the UI. We prefer to use the display name for breadcrumbs when possible. A "NEW" badge was added to the Extensions page's `NavigationViewItem` to highlight that it's new. It goes away once the user visits it. ## Detailed Description of the Pull Request / Additional comments - Settings Model changes: - `FragmentSettings` represents a parsed json fragment extension. - `FragmentProfileEntry` and `FragmentColorSchemeEntry` are used to track profiles and color schemes added/modified - `ExtensionPackage` bundles the `FragmentSettings` together. This is how we represent multiple JSON files in one extension. - `IDynamicProfileGenerator` exposes a `DisplayName` and `Icon` - `ExtensionPackage`s created from app extensions extract the `DisplayName` and `Icon` from the extension - `ApplicationState` is used to track which badges have been dismissed and prevent them from appearing again - a `std::unordered_set` is used to keep track of the dismissed badges, but we only expose a get and append function via the IDL to interact with it - Editor changes - view models: - `ExtensionsViewModel` operates as the main view model for the page. - `FragmentProfileViewModel` and `FragmentColorSchemeViewModel` are used to reference specific components of fragments. They also provide support for navigating to the linked profile or color scheme via the settings UI! - `ExtensionPackageViewModel` is a VM for a group of extensions exposed by a single source. This is mainly needed because a single source can have multiple JSON fragments in it. This is used for the navigators on the main page. Can be extended to provide additional information (i.e. package logo, package name, etc.) - `CurrentExtensionPackage` is used to track which extension package is currently in view, if applicable (similar to how the new tab menu page works) - Editor changes - views: - `Extensions.xaml` uses _a lot_ of data templates. These are reused in `ItemsControl`s to display extension components. - `ExtensionPackageTemplateSelector` is used to display `ExtensionPackage`s with metadata vs simple ones that just have a source (i.e. Git) - Added a `NewInfoBadge` style that is just an InfoBadge with "New" in it instead of a number or an icon. Based on microsoft/PowerToys#36939 - The visibility is bound to a `get` call to the `ApplicationState` conducted via the `ExtensionsPageViewModel`. The VM is also responsible for updating the state. - Lazy loading extension objects - Since most instances of Terminal won't actually open the settings UI, it doesn't make sense to create all the extension objects upon startup. Instead, we defer creating those objects until the user actually navigates to the Extensions page. This is most of the work that happened in `CascadiaSettingsSerialization.cpp`. The `SettingsLoader` can be used specifically to load and create the extension objects. ## Validation Steps ✅ Keyboard navigation feels right ✅ Screen reader reads all info on screen properly ✅ Accessibility Insights FastPass found no issues ✅ "Discard changes" retains subpage, but removes any changes ✅ Extensions page nav item displays a badge if page hasn't been visited ✅ The badge is dismissed when the user visits the page ## Follow-ups - Streamline a process for adding extensions from the new page - Long-term, we can reuse the InfoBadge system and make the following minor changes: - `SettingContainer`: display the badge and add logic to read/write `ApplicationState` appropriately (similarly to above) - `XPageViewModel`: - count all the badges that will be displayed and expose/bind that to `InfoBadge.Value` - If a whole page is new, we can just style the badge using the `NewInfoBadge` style
1 parent 3acb3d5 commit e332c67

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+2123
-115
lines changed
Loading
Loading
Loading
Loading
Loading

src/cascadia/CascadiaResources.build.items

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@
2121
<DeploymentContent>true</DeploymentContent>
2222
<Link>ProfileIcons\%(RecursiveDir)%(FileName)%(Extension)</Link>
2323
</Content>
24+
<!-- Profile Generator Icons -->
25+
<Content Include="$(OpenConsoleDir)src\cascadia\CascadiaPackage\ProfileGeneratorIcons\**\*">
26+
<DeploymentContent>true</DeploymentContent>
27+
<Link>ProfileGeneratorIcons\%(RecursiveDir)%(FileName)%(Extension)</Link>
28+
</Content>
2429
<!-- Default Settings -->
2530
<Content Include="$(OpenConsoleDir)src\cascadia\TerminalSettingsModel\defaults.json">
2631
<DeploymentContent>true</DeploymentContent>

src/cascadia/TerminalSettingsEditor/AddProfile.xaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959
<IconSourceElement Grid.Column="0"
6060
Width="16"
6161
Height="16"
62-
IconSource="{x:Bind mtu:IconPathConverter.IconSourceWUX(Icon), Mode=OneTime}" />
62+
IconSource="{x:Bind mtu:IconPathConverter.IconSourceWUX(EvaluatedIcon), Mode=OneTime}" />
6363

6464
<TextBlock Grid.Column="1"
6565
Text="{x:Bind Name}" />

src/cascadia/TerminalSettingsEditor/CommonResources.xaml

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1204,7 +1204,7 @@
12041204
<Setter Property="HorizontalAlignment" Value="Stretch" />
12051205
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}" />
12061206
<Setter Property="VerticalAlignment" Value="Stretch" />
1207-
<Setter Property="HorizontalContentAlignment" Value="Left" />
1207+
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
12081208
<Setter Property="VerticalContentAlignment" Value="Center" />
12091209
<Setter Property="Template">
12101210
<Setter.Value>
@@ -1227,7 +1227,8 @@
12271227
Content="{TemplateBinding Content}"
12281228
ContentTemplate="{TemplateBinding ContentTemplate}"
12291229
ContentTransitions="{TemplateBinding ContentTransitions}" />
1230-
<FontIcon Margin="20,0,8,0"
1230+
<FontIcon Grid.Column="1"
1231+
Margin="20,0,8,0"
12311232
HorizontalAlignment="Right"
12321233
FontSize="10"
12331234
FontWeight="Black"
@@ -1271,4 +1272,24 @@
12711272
</Setter.Value>
12721273
</Setter>
12731274
</Style>
1275+
1276+
<Style x:Key="NewInfoBadge"
1277+
TargetType="muxc:InfoBadge">
1278+
<Setter Property="Padding" Value="5,1,5,2" />
1279+
<Setter Property="Template">
1280+
<Setter.Value>
1281+
<ControlTemplate TargetType="muxc:InfoBadge">
1282+
<Border x:Name="RootGrid"
1283+
Padding="{TemplateBinding Padding}"
1284+
Background="{TemplateBinding Background}"
1285+
CornerRadius="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.InfoBadgeCornerRadius}">
1286+
<TextBlock x:Uid="NewInfoBadgeTextBlock"
1287+
HorizontalAlignment="Center"
1288+
VerticalAlignment="Center"
1289+
FontSize="10" />
1290+
</Border>
1291+
</ControlTemplate>
1292+
</Setter.Value>
1293+
</Setter>
1294+
</Style>
12741295
</ResourceDictionary>

src/cascadia/TerminalSettingsEditor/Extensions.cpp

Lines changed: 509 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
#pragma once
5+
6+
#include "Extensions.g.h"
7+
#include "ExtensionsViewModel.g.h"
8+
#include "ExtensionPackageViewModel.g.h"
9+
#include "FragmentExtensionViewModel.g.h"
10+
#include "FragmentProfileViewModel.g.h"
11+
#include "FragmentColorSchemeViewModel.g.h"
12+
#include "ExtensionPackageTemplateSelector.g.h"
13+
#include "ViewModelHelpers.h"
14+
#include "Utils.h"
15+
16+
namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
17+
{
18+
struct Extensions : public HasScrollViewer<Extensions>, ExtensionsT<Extensions>
19+
{
20+
public:
21+
Windows::UI::Xaml::Thickness CalculateMargin(bool hidden);
22+
23+
Extensions();
24+
25+
void OnNavigatedTo(const Windows::UI::Xaml::Navigation::NavigationEventArgs& e);
26+
27+
void ExtensionNavigator_Click(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& args);
28+
void NavigateToProfile_Click(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& args);
29+
void NavigateToColorScheme_Click(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& args);
30+
31+
WINRT_PROPERTY(Editor::ExtensionsViewModel, ViewModel, nullptr);
32+
33+
private:
34+
Editor::ExtensionPackageTemplateSelector _extensionPackageIdentifierTemplateSelector;
35+
};
36+
37+
struct ExtensionsViewModel : ExtensionsViewModelT<ExtensionsViewModel>, ViewModelHelper<ExtensionsViewModel>
38+
{
39+
public:
40+
ExtensionsViewModel(const Model::CascadiaSettings& settings, const Editor::ColorSchemesPageViewModel& colorSchemesPageVM);
41+
42+
// Properties
43+
Windows::UI::Xaml::DataTemplate CurrentExtensionPackageIdentifierTemplate() const;
44+
bool IsExtensionView() const noexcept { return _CurrentExtensionPackage != nullptr; }
45+
bool NoExtensionPackages() const noexcept { return _extensionPackages.Size() == 0; }
46+
bool NoProfilesModified() const noexcept { return _profilesModifiedView.Size() == 0; }
47+
bool NoProfilesAdded() const noexcept { return _profilesAddedView.Size() == 0; }
48+
bool NoSchemesAdded() const noexcept { return _colorSchemesAddedView.Size() == 0; }
49+
bool DisplayBadge() const noexcept;
50+
51+
// Views
52+
Windows::Foundation::Collections::IObservableVector<Editor::ExtensionPackageViewModel> ExtensionPackages() const noexcept { return _extensionPackages; }
53+
Windows::Foundation::Collections::IObservableVector<Editor::FragmentProfileViewModel> ProfilesModified() const noexcept { return _profilesModifiedView; }
54+
Windows::Foundation::Collections::IObservableVector<Editor::FragmentProfileViewModel> ProfilesAdded() const noexcept { return _profilesAddedView; }
55+
Windows::Foundation::Collections::IObservableVector<Editor::FragmentColorSchemeViewModel> ColorSchemesAdded() const noexcept { return _colorSchemesAddedView; }
56+
57+
// Methods
58+
void LazyLoadExtensions();
59+
void UpdateSettings(const Model::CascadiaSettings& settings, const Editor::ColorSchemesPageViewModel& colorSchemesPageVM);
60+
void NavigateToProfile(const guid profileGuid);
61+
void NavigateToColorScheme(const Editor::ColorSchemeViewModel& schemeVM);
62+
void MarkAsVisited();
63+
64+
static bool GetExtensionState(hstring extensionSource, const Model::CascadiaSettings& settings);
65+
static void SetExtensionState(hstring extensionSource, const Model::CascadiaSettings& settings, bool enableExt);
66+
67+
til::typed_event<IInspectable, guid> NavigateToProfileRequested;
68+
til::typed_event<IInspectable, Editor::ColorSchemeViewModel> NavigateToColorSchemeRequested;
69+
70+
VIEW_MODEL_OBSERVABLE_PROPERTY(Editor::ExtensionPackageViewModel, CurrentExtensionPackage, nullptr);
71+
WINRT_PROPERTY(Editor::ExtensionPackageTemplateSelector, ExtensionPackageIdentifierTemplateSelector, nullptr);
72+
73+
private:
74+
Model::CascadiaSettings _settings;
75+
Editor::ColorSchemesPageViewModel _colorSchemesPageVM;
76+
Windows::Foundation::Collections::IObservableVector<Editor::ExtensionPackageViewModel> _extensionPackages;
77+
Windows::Foundation::Collections::IObservableVector<Editor::FragmentProfileViewModel> _profilesModifiedView;
78+
Windows::Foundation::Collections::IObservableVector<Editor::FragmentProfileViewModel> _profilesAddedView;
79+
Windows::Foundation::Collections::IObservableVector<Editor::FragmentColorSchemeViewModel> _colorSchemesAddedView;
80+
bool _extensionsLoaded;
81+
82+
void _UpdateListViews(bool updateProfilesModified, bool updateProfilesAdded, bool updateColorSchemesAdded);
83+
};
84+
85+
struct ExtensionPackageViewModel : ExtensionPackageViewModelT<ExtensionPackageViewModel>, ViewModelHelper<ExtensionPackageViewModel>
86+
{
87+
public:
88+
ExtensionPackageViewModel(const Model::ExtensionPackage& pkg, const Model::CascadiaSettings& settings) :
89+
_package{ pkg },
90+
_settings{ settings },
91+
_fragmentExtensions{ single_threaded_observable_vector<Editor::FragmentExtensionViewModel>() } {}
92+
93+
static bool SortAscending(const Editor::ExtensionPackageViewModel& lhs, const Editor::ExtensionPackageViewModel& rhs);
94+
95+
void UpdateSettings(const Model::CascadiaSettings& settings);
96+
97+
Model::ExtensionPackage Package() const noexcept { return _package; }
98+
hstring Scope() const noexcept;
99+
bool Enabled() const;
100+
void Enabled(bool val);
101+
hstring AccessibleName() const noexcept;
102+
Windows::Foundation::Collections::IObservableVector<Editor::FragmentExtensionViewModel> FragmentExtensions() { return _fragmentExtensions; }
103+
104+
private:
105+
Model::ExtensionPackage _package;
106+
Model::CascadiaSettings _settings;
107+
Windows::Foundation::Collections::IObservableVector<Editor::FragmentExtensionViewModel> _fragmentExtensions;
108+
};
109+
110+
struct FragmentExtensionViewModel : FragmentExtensionViewModelT<FragmentExtensionViewModel>, ViewModelHelper<FragmentExtensionViewModel>
111+
{
112+
public:
113+
FragmentExtensionViewModel(const Model::FragmentSettings& fragment,
114+
std::vector<FragmentProfileViewModel>& profilesModified,
115+
std::vector<FragmentProfileViewModel>& profilesAdded,
116+
std::vector<FragmentColorSchemeViewModel>& colorSchemesAdded) :
117+
_fragment{ fragment },
118+
_profilesModified{ single_threaded_vector(std::move(profilesModified)) },
119+
_profilesAdded{ single_threaded_vector(std::move(profilesAdded)) },
120+
_colorSchemesAdded{ single_threaded_vector(std::move(colorSchemesAdded)) } {}
121+
122+
Model::FragmentSettings Fragment() const noexcept { return _fragment; }
123+
Windows::Foundation::Collections::IVectorView<FragmentProfileViewModel> ProfilesModified() const noexcept { return _profilesModified.GetView(); }
124+
Windows::Foundation::Collections::IVectorView<FragmentProfileViewModel> ProfilesAdded() const noexcept { return _profilesAdded.GetView(); }
125+
Windows::Foundation::Collections::IVectorView<FragmentColorSchemeViewModel> ColorSchemesAdded() const noexcept { return _colorSchemesAdded.GetView(); }
126+
127+
private:
128+
Model::FragmentSettings _fragment;
129+
Windows::Foundation::Collections::IVector<FragmentProfileViewModel> _profilesModified;
130+
Windows::Foundation::Collections::IVector<FragmentProfileViewModel> _profilesAdded;
131+
Windows::Foundation::Collections::IVector<FragmentColorSchemeViewModel> _colorSchemesAdded;
132+
};
133+
134+
struct FragmentProfileViewModel : FragmentProfileViewModelT<FragmentProfileViewModel>, ViewModelHelper<FragmentProfileViewModel>
135+
{
136+
public:
137+
FragmentProfileViewModel(const Model::FragmentProfileEntry& entry, const Model::FragmentSettings& fragment, const Model::Profile& deducedProfile) :
138+
_entry{ entry },
139+
_fragment{ fragment },
140+
_deducedProfile{ deducedProfile } {}
141+
142+
static bool SortAscending(const Editor::FragmentProfileViewModel& lhs, const Editor::FragmentProfileViewModel& rhs);
143+
144+
Model::Profile Profile() const { return _deducedProfile; };
145+
hstring SourceName() const { return _fragment.Source(); }
146+
hstring Json() const { return _entry.Json(); }
147+
hstring AccessibleName() const noexcept;
148+
149+
private:
150+
Model::FragmentProfileEntry _entry;
151+
Model::FragmentSettings _fragment;
152+
Model::Profile _deducedProfile;
153+
};
154+
155+
struct FragmentColorSchemeViewModel : FragmentColorSchemeViewModelT<FragmentColorSchemeViewModel>, ViewModelHelper<FragmentColorSchemeViewModel>
156+
{
157+
public:
158+
FragmentColorSchemeViewModel(const Model::FragmentColorSchemeEntry& entry, const Model::FragmentSettings& fragment, const Editor::ColorSchemeViewModel& deducedSchemeVM) :
159+
_entry{ entry },
160+
_fragment{ fragment },
161+
_deducedSchemeVM{ deducedSchemeVM } {}
162+
163+
static bool SortAscending(const Editor::FragmentColorSchemeViewModel& lhs, const Editor::FragmentColorSchemeViewModel& rhs);
164+
165+
Editor::ColorSchemeViewModel ColorSchemeVM() const { return _deducedSchemeVM; };
166+
hstring SourceName() const { return _fragment.Source(); }
167+
hstring Json() const { return _entry.Json(); }
168+
hstring AccessibleName() const noexcept;
169+
170+
private:
171+
Model::FragmentColorSchemeEntry _entry;
172+
Model::FragmentSettings _fragment;
173+
Editor::ColorSchemeViewModel _deducedSchemeVM;
174+
};
175+
176+
struct ExtensionPackageTemplateSelector : public ExtensionPackageTemplateSelectorT<ExtensionPackageTemplateSelector>
177+
{
178+
public:
179+
ExtensionPackageTemplateSelector() = default;
180+
181+
Windows::UI::Xaml::DataTemplate SelectTemplateCore(const Windows::Foundation::IInspectable& item, const Windows::UI::Xaml::DependencyObject& container);
182+
Windows::UI::Xaml::DataTemplate SelectTemplateCore(const Windows::Foundation::IInspectable& item);
183+
184+
WINRT_PROPERTY(Windows::UI::Xaml::DataTemplate, DefaultTemplate, nullptr);
185+
WINRT_PROPERTY(Windows::UI::Xaml::DataTemplate, ComplexTemplate, nullptr);
186+
};
187+
};
188+
189+
namespace winrt::Microsoft::Terminal::Settings::Editor::factory_implementation
190+
{
191+
BASIC_FACTORY(Extensions);
192+
BASIC_FACTORY(ExtensionPackageTemplateSelector);
193+
}

0 commit comments

Comments
 (0)