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
https://github.com/microsoft/PowerToys/pull/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
This commit is contained in:
Carlos Zamora 2025-05-28 12:03:02 -07:00 committed by GitHub
parent 3acb3d510b
commit e332c67f51
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
42 changed files with 2123 additions and 115 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 943 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 787 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -21,6 +21,11 @@
<DeploymentContent>true</DeploymentContent>
<Link>ProfileIcons\%(RecursiveDir)%(FileName)%(Extension)</Link>
</Content>
<!-- Profile Generator Icons -->
<Content Include="$(OpenConsoleDir)src\cascadia\CascadiaPackage\ProfileGeneratorIcons\**\*">
<DeploymentContent>true</DeploymentContent>
<Link>ProfileGeneratorIcons\%(RecursiveDir)%(FileName)%(Extension)</Link>
</Content>
<!-- Default Settings -->
<Content Include="$(OpenConsoleDir)src\cascadia\TerminalSettingsModel\defaults.json">
<DeploymentContent>true</DeploymentContent>

View File

@ -59,7 +59,7 @@
<IconSourceElement Grid.Column="0"
Width="16"
Height="16"
IconSource="{x:Bind mtu:IconPathConverter.IconSourceWUX(Icon), Mode=OneTime}" />
IconSource="{x:Bind mtu:IconPathConverter.IconSourceWUX(EvaluatedIcon), Mode=OneTime}" />
<TextBlock Grid.Column="1"
Text="{x:Bind Name}" />

View File

@ -1204,7 +1204,7 @@
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}" />
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Template">
<Setter.Value>
@ -1227,7 +1227,8 @@
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
ContentTransitions="{TemplateBinding ContentTransitions}" />
<FontIcon Margin="20,0,8,0"
<FontIcon Grid.Column="1"
Margin="20,0,8,0"
HorizontalAlignment="Right"
FontSize="10"
FontWeight="Black"
@ -1271,4 +1272,24 @@
</Setter.Value>
</Setter>
</Style>
<Style x:Key="NewInfoBadge"
TargetType="muxc:InfoBadge">
<Setter Property="Padding" Value="5,1,5,2" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="muxc:InfoBadge">
<Border x:Name="RootGrid"
Padding="{TemplateBinding Padding}"
Background="{TemplateBinding Background}"
CornerRadius="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.InfoBadgeCornerRadius}">
<TextBlock x:Uid="NewInfoBadgeTextBlock"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="10" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@ -0,0 +1,509 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "Extensions.h"
#include "Extensions.g.cpp"
#include "ExtensionPackageViewModel.g.cpp"
#include "ExtensionsViewModel.g.cpp"
#include "FragmentProfileViewModel.g.cpp"
#include "ExtensionPackageTemplateSelector.g.cpp"
#include <LibraryResources.h>
#include "..\WinRTUtils\inc\Utils.h"
using namespace winrt::Windows::Foundation;
using namespace winrt::Windows::Foundation::Collections;
using namespace winrt::Windows::UI::Xaml;
using namespace winrt::Windows::UI::Xaml::Controls;
using namespace winrt::Windows::UI::Xaml::Navigation;
namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
static constexpr std::wstring_view ExtensionPageId{ L"page.extensions" };
Extensions::Extensions()
{
InitializeComponent();
_extensionPackageIdentifierTemplateSelector = Resources().Lookup(box_value(L"ExtensionPackageIdentifierTemplateSelector")).as<Editor::ExtensionPackageTemplateSelector>();
Automation::AutomationProperties::SetName(ActiveExtensionsList(), RS_(L"Extensions_ActiveExtensionsHeader/Text"));
Automation::AutomationProperties::SetName(ModifiedProfilesList(), RS_(L"Extensions_ModifiedProfilesHeader/Text"));
Automation::AutomationProperties::SetName(AddedProfilesList(), RS_(L"Extensions_AddedProfilesHeader/Text"));
Automation::AutomationProperties::SetName(AddedColorSchemesList(), RS_(L"Extensions_AddedColorSchemesHeader/Text"));
}
void Extensions::OnNavigatedTo(const NavigationEventArgs& e)
{
_ViewModel = e.Parameter().as<Editor::ExtensionsViewModel>();
auto vmImpl = get_self<ExtensionsViewModel>(_ViewModel);
vmImpl->ExtensionPackageIdentifierTemplateSelector(_extensionPackageIdentifierTemplateSelector);
vmImpl->LazyLoadExtensions();
vmImpl->MarkAsVisited();
}
void Extensions::ExtensionNavigator_Click(const IInspectable& sender, const RoutedEventArgs& /*args*/)
{
const auto extPkgVM = sender.as<Controls::Button>().Tag().as<Editor::ExtensionPackageViewModel>();
_ViewModel.CurrentExtensionPackage(extPkgVM);
}
void Extensions::NavigateToProfile_Click(const IInspectable& sender, const RoutedEventArgs& /*args*/)
{
const auto& profileGuid = sender.as<Controls::Button>().Tag().as<guid>();
get_self<ExtensionsViewModel>(_ViewModel)->NavigateToProfile(profileGuid);
}
void Extensions::NavigateToColorScheme_Click(const IInspectable& sender, const RoutedEventArgs& /*args*/)
{
const auto& schemeVM = sender.as<Controls::Button>().Tag().as<Editor::ColorSchemeViewModel>();
get_self<ExtensionsViewModel>(_ViewModel)->NavigateToColorScheme(schemeVM);
}
ExtensionsViewModel::ExtensionsViewModel(const Model::CascadiaSettings& settings, const Editor::ColorSchemesPageViewModel& colorSchemesPageVM) :
_settings{ settings },
_colorSchemesPageVM{ colorSchemesPageVM },
_extensionsLoaded{ false }
{
UpdateSettings(settings, colorSchemesPageVM);
PropertyChanged([this](auto&&, const PropertyChangedEventArgs& args) {
const auto viewModelProperty{ args.PropertyName() };
const bool extensionPackageChanged = viewModelProperty == L"CurrentExtensionPackage";
const bool profilesModifiedChanged = viewModelProperty == L"ProfilesModified";
const bool profilesAddedChanged = viewModelProperty == L"ProfilesAdded";
const bool colorSchemesAddedChanged = viewModelProperty == L"ColorSchemesAdded";
if (extensionPackageChanged || (!IsExtensionView() && (profilesModifiedChanged || profilesAddedChanged || colorSchemesAddedChanged)))
{
// Use these booleans to track which of our observable vectors need to be refreshed.
// This prevents a full refresh of the UI when enabling/disabling extensions.
// If the CurrentExtensionPackage changed, we want to update all components.
// Otherwise, just update the ones that we were notified about.
const bool updateProfilesModified = extensionPackageChanged || profilesModifiedChanged;
const bool updateProfilesAdded = extensionPackageChanged || profilesAddedChanged;
const bool updateColorSchemesAdded = extensionPackageChanged || colorSchemesAddedChanged;
_UpdateListViews(updateProfilesModified, updateProfilesAdded, updateColorSchemesAdded);
if (extensionPackageChanged)
{
_NotifyChanges(L"IsExtensionView", L"CurrentExtensionPackageIdentifierTemplate");
}
else if (profilesModifiedChanged)
{
_NotifyChanges(L"NoProfilesModified");
}
else if (profilesAddedChanged)
{
_NotifyChanges(L"NoProfilesAdded");
}
else if (colorSchemesAddedChanged)
{
_NotifyChanges(L"NoSchemesAdded");
}
}
});
}
void ExtensionsViewModel::_UpdateListViews(bool updateProfilesModified, bool updateProfilesAdded, bool updateColorSchemesAdded)
{
// STL vectors to track relevant components for extensions to display in UI
std::vector<Editor::FragmentProfileViewModel> profilesModifiedTotal;
std::vector<Editor::FragmentProfileViewModel> profilesAddedTotal;
std::vector<Editor::FragmentColorSchemeViewModel> colorSchemesAddedTotal;
// Helper lambda to add the contents of an extension package to the current view.
auto addPackageContentsToView = [&](const Editor::ExtensionPackageViewModel& extPkg) {
auto extPkgVM = get_self<ExtensionPackageViewModel>(extPkg);
for (const auto& ext : extPkgVM->FragmentExtensions())
{
if (updateProfilesModified)
{
for (const auto& profile : ext.ProfilesModified())
{
profilesModifiedTotal.push_back(profile);
}
}
if (updateProfilesAdded)
{
for (const auto& profile : ext.ProfilesAdded())
{
profilesAddedTotal.push_back(profile);
}
}
if (updateColorSchemesAdded)
{
for (const auto& scheme : ext.ColorSchemesAdded())
{
colorSchemesAddedTotal.push_back(scheme);
}
}
}
};
// Populate the STL vectors that we want to update
if (const auto currentExtensionPackage = CurrentExtensionPackage())
{
// Update all of the views to reflect the current extension package, if one is selected.
addPackageContentsToView(currentExtensionPackage);
}
else
{
// Only populate the views with components from enabled extensions
for (const auto& extPkg : _extensionPackages)
{
if (extPkg.Enabled())
{
addPackageContentsToView(extPkg);
}
}
}
// Sort the lists linguistically for nicer presentation.
// Update the WinRT lists bound to UI.
if (updateProfilesModified)
{
std::sort(profilesModifiedTotal.begin(), profilesModifiedTotal.end(), FragmentProfileViewModel::SortAscending);
_profilesModifiedView = winrt::single_threaded_observable_vector(std::move(profilesModifiedTotal));
}
if (updateProfilesAdded)
{
std::sort(profilesAddedTotal.begin(), profilesAddedTotal.end(), FragmentProfileViewModel::SortAscending);
_profilesAddedView = winrt::single_threaded_observable_vector(std::move(profilesAddedTotal));
}
if (updateColorSchemesAdded)
{
std::sort(colorSchemesAddedTotal.begin(), colorSchemesAddedTotal.end(), FragmentColorSchemeViewModel::SortAscending);
_colorSchemesAddedView = winrt::single_threaded_observable_vector(std::move(colorSchemesAddedTotal));
}
}
void ExtensionsViewModel::UpdateSettings(const Model::CascadiaSettings& settings, const Editor::ColorSchemesPageViewModel& colorSchemesPageVM)
{
_settings = settings;
_colorSchemesPageVM = colorSchemesPageVM;
_CurrentExtensionPackage = nullptr;
// The extension packages may not be loaded yet because we want to wait until we actually navigate to the page to do so.
// In that case, omit "updating" them. They'll get the proper references when we lazy load them.
if (_extensionPackages)
{
for (const auto& extPkg : _extensionPackages)
{
get_self<ExtensionPackageViewModel>(extPkg)->UpdateSettings(_settings);
}
}
}
void ExtensionsViewModel::LazyLoadExtensions()
{
if (_extensionsLoaded)
{
return;
}
std::vector<Model::ExtensionPackage> extensions = wil::to_vector(_settings.Extensions());
// these vectors track components all extensions successfully added
std::vector<Editor::ExtensionPackageViewModel> extensionPackages;
std::vector<Editor::FragmentProfileViewModel> profilesModifiedTotal;
std::vector<Editor::FragmentProfileViewModel> profilesAddedTotal;
std::vector<Editor::FragmentColorSchemeViewModel> colorSchemesAddedTotal;
for (const auto& extPkg : extensions)
{
auto extPkgVM = winrt::make_self<ExtensionPackageViewModel>(extPkg, _settings);
for (const auto& fragExt : extPkg.FragmentsView())
{
const auto extensionEnabled = GetExtensionState(fragExt.Source(), _settings);
// these vectors track everything the current extension attempted to bring in
std::vector<Editor::FragmentProfileViewModel> currentProfilesModified;
std::vector<Editor::FragmentProfileViewModel> currentProfilesAdded;
std::vector<Editor::FragmentColorSchemeViewModel> currentColorSchemesAdded;
if (fragExt.ModifiedProfilesView())
{
for (const auto&& entry : fragExt.ModifiedProfilesView())
{
// Ensure entry successfully modifies a profile before creating and registering the object
if (const auto& deducedProfile = _settings.FindProfile(entry.ProfileGuid()))
{
auto vm = winrt::make<FragmentProfileViewModel>(entry, fragExt, deducedProfile);
currentProfilesModified.push_back(vm);
if (extensionEnabled)
{
profilesModifiedTotal.push_back(vm);
}
}
}
}
if (fragExt.NewProfilesView())
{
for (const auto&& entry : fragExt.NewProfilesView())
{
// Ensure entry successfully points to a profile before creating and registering the object.
// The profile may have been removed by the user.
if (const auto& deducedProfile = _settings.FindProfile(entry.ProfileGuid()))
{
auto vm = winrt::make<FragmentProfileViewModel>(entry, fragExt, deducedProfile);
currentProfilesAdded.push_back(vm);
if (extensionEnabled)
{
profilesAddedTotal.push_back(vm);
}
}
}
}
if (fragExt.ColorSchemesView())
{
for (const auto&& entry : fragExt.ColorSchemesView())
{
for (const auto& schemeVM : _colorSchemesPageVM.AllColorSchemes())
{
if (schemeVM.Name() == entry.ColorSchemeName())
{
auto vm = winrt::make<FragmentColorSchemeViewModel>(entry, fragExt, schemeVM);
currentColorSchemesAdded.push_back(vm);
if (extensionEnabled)
{
colorSchemesAddedTotal.push_back(vm);
}
}
}
}
}
// sort the lists linguistically for nicer presentation
std::sort(currentProfilesModified.begin(), currentProfilesModified.end(), FragmentProfileViewModel::SortAscending);
std::sort(currentProfilesAdded.begin(), currentProfilesAdded.end(), FragmentProfileViewModel::SortAscending);
std::sort(currentColorSchemesAdded.begin(), currentColorSchemesAdded.end(), FragmentColorSchemeViewModel::SortAscending);
extPkgVM->FragmentExtensions().Append(winrt::make<FragmentExtensionViewModel>(fragExt, currentProfilesModified, currentProfilesAdded, currentColorSchemesAdded));
extPkgVM->PropertyChanged([&](const IInspectable& sender, const PropertyChangedEventArgs& args) {
const auto viewModelProperty{ args.PropertyName() };
if (viewModelProperty == L"Enabled")
{
// If the extension was enabled/disabled,
// check if any of its fragments modified profiles, added profiles, or added color schemes.
// Only notify what was affected!
bool hasModifiedProfiles = false;
bool hasAddedProfiles = false;
bool hasAddedColorSchemes = false;
for (const auto& fragExtVM : sender.as<ExtensionPackageViewModel>()->FragmentExtensions())
{
const auto profilesModified = fragExtVM.ProfilesModified();
const auto profilesAdded = fragExtVM.ProfilesAdded();
const auto colorSchemesAdded = fragExtVM.ColorSchemesAdded();
hasModifiedProfiles |= profilesModified && profilesModified.Size() > 0;
hasAddedProfiles |= profilesAdded && profilesAdded.Size() > 0;
hasAddedColorSchemes |= colorSchemesAdded && colorSchemesAdded.Size() > 0;
}
if (hasModifiedProfiles)
{
_NotifyChanges(L"ProfilesModified");
}
if (hasAddedProfiles)
{
_NotifyChanges(L"ProfilesAdded");
}
if (hasAddedColorSchemes)
{
_NotifyChanges(L"ColorSchemesAdded");
}
}
});
}
extensionPackages.push_back(*extPkgVM);
}
// sort the lists linguistically for nicer presentation
std::sort(extensionPackages.begin(), extensionPackages.end(), ExtensionPackageViewModel::SortAscending);
std::sort(profilesModifiedTotal.begin(), profilesModifiedTotal.end(), FragmentProfileViewModel::SortAscending);
std::sort(profilesAddedTotal.begin(), profilesAddedTotal.end(), FragmentProfileViewModel::SortAscending);
std::sort(colorSchemesAddedTotal.begin(), colorSchemesAddedTotal.end(), FragmentColorSchemeViewModel::SortAscending);
_extensionPackages = single_threaded_observable_vector<Editor::ExtensionPackageViewModel>(std::move(extensionPackages));
_profilesModifiedView = single_threaded_observable_vector<Editor::FragmentProfileViewModel>(std::move(profilesModifiedTotal));
_profilesAddedView = single_threaded_observable_vector<Editor::FragmentProfileViewModel>(std::move(profilesAddedTotal));
_colorSchemesAddedView = single_threaded_observable_vector<Editor::FragmentColorSchemeViewModel>(std::move(colorSchemesAddedTotal));
_extensionsLoaded = true;
}
Windows::UI::Xaml::DataTemplate ExtensionsViewModel::CurrentExtensionPackageIdentifierTemplate() const
{
return _ExtensionPackageIdentifierTemplateSelector.SelectTemplate(CurrentExtensionPackage());
}
bool ExtensionsViewModel::DisplayBadge() const noexcept
{
return !Model::ApplicationState::SharedInstance().BadgeDismissed(ExtensionPageId);
}
// Returns true if the extension is enabled, false otherwise
bool ExtensionsViewModel::GetExtensionState(hstring extensionSource, const Model::CascadiaSettings& settings)
{
if (const auto& disabledExtensions = settings.GlobalSettings().DisabledProfileSources())
{
uint32_t ignored;
return !disabledExtensions.IndexOf(extensionSource, ignored);
}
// "disabledProfileSources" not defined --> all extensions are enabled
return true;
}
// Enable/Disable an extension
void ExtensionsViewModel::SetExtensionState(hstring extensionSource, const Model::CascadiaSettings& settings, bool enableExt)
{
// get the current status of the extension
uint32_t idx;
bool currentlyEnabled = true;
const auto& disabledExtensions = settings.GlobalSettings().DisabledProfileSources();
if (disabledExtensions)
{
currentlyEnabled = !disabledExtensions.IndexOf(extensionSource, idx);
}
// current status mismatches the desired status,
// update the list of disabled extensions
if (currentlyEnabled != enableExt)
{
// If we're disabling an extension and we don't have "disabledProfileSources" defined,
// create it in the model directly
if (!disabledExtensions && !enableExt)
{
std::vector<hstring> disabledProfileSources{ extensionSource };
settings.GlobalSettings().DisabledProfileSources(single_threaded_vector<hstring>(std::move(disabledProfileSources)));
return;
}
// Update the list of disabled extensions
if (enableExt)
{
disabledExtensions.RemoveAt(idx);
}
else
{
disabledExtensions.Append(extensionSource);
}
}
}
Thickness Extensions::CalculateMargin(bool hidden)
{
return ThicknessHelper::FromLengths(/*left*/ 0,
/*top*/ hidden ? 0 : 20,
/*right*/ 0,
/*bottom*/ 0);
}
void ExtensionsViewModel::NavigateToProfile(const guid profileGuid)
{
NavigateToProfileRequested.raise(*this, profileGuid);
}
void ExtensionsViewModel::NavigateToColorScheme(const Editor::ColorSchemeViewModel& schemeVM)
{
_colorSchemesPageVM.CurrentScheme(schemeVM);
NavigateToColorSchemeRequested.raise(*this, nullptr);
}
void ExtensionsViewModel::MarkAsVisited()
{
Model::ApplicationState::SharedInstance().DismissBadge(ExtensionPageId);
_NotifyChanges(L"DisplayBadge");
}
bool ExtensionPackageViewModel::SortAscending(const Editor::ExtensionPackageViewModel& lhs, const Editor::ExtensionPackageViewModel& rhs)
{
auto getKey = [&](const Editor::ExtensionPackageViewModel& pkgVM) {
const auto pkg = pkgVM.Package();
const auto displayName = pkg.DisplayName();
return displayName.empty() ? pkg.Source() : displayName;
};
return til::compare_linguistic_insensitive(getKey(lhs), getKey(rhs)) < 0;
}
void ExtensionPackageViewModel::UpdateSettings(const Model::CascadiaSettings& settings)
{
const auto oldEnabled = Enabled();
_settings = settings;
if (oldEnabled != Enabled())
{
// The enabled state of the extension has changed, notify the UI
_NotifyChanges(L"Enabled");
}
}
hstring ExtensionPackageViewModel::Scope() const noexcept
{
return _package.Scope() == Model::FragmentScope::User ? RS_(L"Extensions_ScopeUser") : RS_(L"Extensions_ScopeSystem");
}
bool ExtensionPackageViewModel::Enabled() const
{
return ExtensionsViewModel::GetExtensionState(_package.Source(), _settings);
}
void ExtensionPackageViewModel::Enabled(bool val)
{
if (Enabled() != val)
{
ExtensionsViewModel::SetExtensionState(_package.Source(), _settings, val);
_NotifyChanges(L"Enabled");
}
}
// Returns the accessible name for the extension package in the following format:
// "<DisplayName?>, <Source>"
hstring ExtensionPackageViewModel::AccessibleName() const noexcept
{
hstring name;
const auto source = _package.Source();
if (const auto displayName = _package.DisplayName(); !displayName.empty())
{
return hstring{ fmt::format(FMT_COMPILE(L"{}, {}"), displayName, source) };
}
return source;
}
bool FragmentProfileViewModel::SortAscending(const Editor::FragmentProfileViewModel& lhs, const Editor::FragmentProfileViewModel& rhs)
{
return til::compare_linguistic_insensitive(lhs.Profile().Name(), rhs.Profile().Name()) < 0;
}
hstring FragmentProfileViewModel::AccessibleName() const noexcept
{
return hstring{ fmt::format(FMT_COMPILE(L"{}, {}"), Profile().Name(), SourceName()) };
}
bool FragmentColorSchemeViewModel::SortAscending(const Editor::FragmentColorSchemeViewModel& lhs, const Editor::FragmentColorSchemeViewModel& rhs)
{
return til::compare_linguistic_insensitive(lhs.ColorSchemeVM().Name(), rhs.ColorSchemeVM().Name()) < 0;
}
hstring FragmentColorSchemeViewModel::AccessibleName() const noexcept
{
return hstring{ fmt::format(FMT_COMPILE(L"{}, {}"), ColorSchemeVM().Name(), SourceName()) };
}
DataTemplate ExtensionPackageTemplateSelector::SelectTemplateCore(const IInspectable& item, const DependencyObject& /*container*/)
{
return SelectTemplateCore(item);
}
DataTemplate ExtensionPackageTemplateSelector::SelectTemplateCore(const IInspectable& item)
{
if (const auto extPkgVM = item.try_as<Editor::ExtensionPackageViewModel>())
{
if (!extPkgVM.Package().DisplayName().empty())
{
return ComplexTemplate();
}
return DefaultTemplate();
}
return nullptr;
}
}

View File

@ -0,0 +1,193 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#include "Extensions.g.h"
#include "ExtensionsViewModel.g.h"
#include "ExtensionPackageViewModel.g.h"
#include "FragmentExtensionViewModel.g.h"
#include "FragmentProfileViewModel.g.h"
#include "FragmentColorSchemeViewModel.g.h"
#include "ExtensionPackageTemplateSelector.g.h"
#include "ViewModelHelpers.h"
#include "Utils.h"
namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
struct Extensions : public HasScrollViewer<Extensions>, ExtensionsT<Extensions>
{
public:
Windows::UI::Xaml::Thickness CalculateMargin(bool hidden);
Extensions();
void OnNavigatedTo(const Windows::UI::Xaml::Navigation::NavigationEventArgs& e);
void ExtensionNavigator_Click(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& args);
void NavigateToProfile_Click(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& args);
void NavigateToColorScheme_Click(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& args);
WINRT_PROPERTY(Editor::ExtensionsViewModel, ViewModel, nullptr);
private:
Editor::ExtensionPackageTemplateSelector _extensionPackageIdentifierTemplateSelector;
};
struct ExtensionsViewModel : ExtensionsViewModelT<ExtensionsViewModel>, ViewModelHelper<ExtensionsViewModel>
{
public:
ExtensionsViewModel(const Model::CascadiaSettings& settings, const Editor::ColorSchemesPageViewModel& colorSchemesPageVM);
// Properties
Windows::UI::Xaml::DataTemplate CurrentExtensionPackageIdentifierTemplate() const;
bool IsExtensionView() const noexcept { return _CurrentExtensionPackage != nullptr; }
bool NoExtensionPackages() const noexcept { return _extensionPackages.Size() == 0; }
bool NoProfilesModified() const noexcept { return _profilesModifiedView.Size() == 0; }
bool NoProfilesAdded() const noexcept { return _profilesAddedView.Size() == 0; }
bool NoSchemesAdded() const noexcept { return _colorSchemesAddedView.Size() == 0; }
bool DisplayBadge() const noexcept;
// Views
Windows::Foundation::Collections::IObservableVector<Editor::ExtensionPackageViewModel> ExtensionPackages() const noexcept { return _extensionPackages; }
Windows::Foundation::Collections::IObservableVector<Editor::FragmentProfileViewModel> ProfilesModified() const noexcept { return _profilesModifiedView; }
Windows::Foundation::Collections::IObservableVector<Editor::FragmentProfileViewModel> ProfilesAdded() const noexcept { return _profilesAddedView; }
Windows::Foundation::Collections::IObservableVector<Editor::FragmentColorSchemeViewModel> ColorSchemesAdded() const noexcept { return _colorSchemesAddedView; }
// Methods
void LazyLoadExtensions();
void UpdateSettings(const Model::CascadiaSettings& settings, const Editor::ColorSchemesPageViewModel& colorSchemesPageVM);
void NavigateToProfile(const guid profileGuid);
void NavigateToColorScheme(const Editor::ColorSchemeViewModel& schemeVM);
void MarkAsVisited();
static bool GetExtensionState(hstring extensionSource, const Model::CascadiaSettings& settings);
static void SetExtensionState(hstring extensionSource, const Model::CascadiaSettings& settings, bool enableExt);
til::typed_event<IInspectable, guid> NavigateToProfileRequested;
til::typed_event<IInspectable, Editor::ColorSchemeViewModel> NavigateToColorSchemeRequested;
VIEW_MODEL_OBSERVABLE_PROPERTY(Editor::ExtensionPackageViewModel, CurrentExtensionPackage, nullptr);
WINRT_PROPERTY(Editor::ExtensionPackageTemplateSelector, ExtensionPackageIdentifierTemplateSelector, nullptr);
private:
Model::CascadiaSettings _settings;
Editor::ColorSchemesPageViewModel _colorSchemesPageVM;
Windows::Foundation::Collections::IObservableVector<Editor::ExtensionPackageViewModel> _extensionPackages;
Windows::Foundation::Collections::IObservableVector<Editor::FragmentProfileViewModel> _profilesModifiedView;
Windows::Foundation::Collections::IObservableVector<Editor::FragmentProfileViewModel> _profilesAddedView;
Windows::Foundation::Collections::IObservableVector<Editor::FragmentColorSchemeViewModel> _colorSchemesAddedView;
bool _extensionsLoaded;
void _UpdateListViews(bool updateProfilesModified, bool updateProfilesAdded, bool updateColorSchemesAdded);
};
struct ExtensionPackageViewModel : ExtensionPackageViewModelT<ExtensionPackageViewModel>, ViewModelHelper<ExtensionPackageViewModel>
{
public:
ExtensionPackageViewModel(const Model::ExtensionPackage& pkg, const Model::CascadiaSettings& settings) :
_package{ pkg },
_settings{ settings },
_fragmentExtensions{ single_threaded_observable_vector<Editor::FragmentExtensionViewModel>() } {}
static bool SortAscending(const Editor::ExtensionPackageViewModel& lhs, const Editor::ExtensionPackageViewModel& rhs);
void UpdateSettings(const Model::CascadiaSettings& settings);
Model::ExtensionPackage Package() const noexcept { return _package; }
hstring Scope() const noexcept;
bool Enabled() const;
void Enabled(bool val);
hstring AccessibleName() const noexcept;
Windows::Foundation::Collections::IObservableVector<Editor::FragmentExtensionViewModel> FragmentExtensions() { return _fragmentExtensions; }
private:
Model::ExtensionPackage _package;
Model::CascadiaSettings _settings;
Windows::Foundation::Collections::IObservableVector<Editor::FragmentExtensionViewModel> _fragmentExtensions;
};
struct FragmentExtensionViewModel : FragmentExtensionViewModelT<FragmentExtensionViewModel>, ViewModelHelper<FragmentExtensionViewModel>
{
public:
FragmentExtensionViewModel(const Model::FragmentSettings& fragment,
std::vector<FragmentProfileViewModel>& profilesModified,
std::vector<FragmentProfileViewModel>& profilesAdded,
std::vector<FragmentColorSchemeViewModel>& colorSchemesAdded) :
_fragment{ fragment },
_profilesModified{ single_threaded_vector(std::move(profilesModified)) },
_profilesAdded{ single_threaded_vector(std::move(profilesAdded)) },
_colorSchemesAdded{ single_threaded_vector(std::move(colorSchemesAdded)) } {}
Model::FragmentSettings Fragment() const noexcept { return _fragment; }
Windows::Foundation::Collections::IVectorView<FragmentProfileViewModel> ProfilesModified() const noexcept { return _profilesModified.GetView(); }
Windows::Foundation::Collections::IVectorView<FragmentProfileViewModel> ProfilesAdded() const noexcept { return _profilesAdded.GetView(); }
Windows::Foundation::Collections::IVectorView<FragmentColorSchemeViewModel> ColorSchemesAdded() const noexcept { return _colorSchemesAdded.GetView(); }
private:
Model::FragmentSettings _fragment;
Windows::Foundation::Collections::IVector<FragmentProfileViewModel> _profilesModified;
Windows::Foundation::Collections::IVector<FragmentProfileViewModel> _profilesAdded;
Windows::Foundation::Collections::IVector<FragmentColorSchemeViewModel> _colorSchemesAdded;
};
struct FragmentProfileViewModel : FragmentProfileViewModelT<FragmentProfileViewModel>, ViewModelHelper<FragmentProfileViewModel>
{
public:
FragmentProfileViewModel(const Model::FragmentProfileEntry& entry, const Model::FragmentSettings& fragment, const Model::Profile& deducedProfile) :
_entry{ entry },
_fragment{ fragment },
_deducedProfile{ deducedProfile } {}
static bool SortAscending(const Editor::FragmentProfileViewModel& lhs, const Editor::FragmentProfileViewModel& rhs);
Model::Profile Profile() const { return _deducedProfile; };
hstring SourceName() const { return _fragment.Source(); }
hstring Json() const { return _entry.Json(); }
hstring AccessibleName() const noexcept;
private:
Model::FragmentProfileEntry _entry;
Model::FragmentSettings _fragment;
Model::Profile _deducedProfile;
};
struct FragmentColorSchemeViewModel : FragmentColorSchemeViewModelT<FragmentColorSchemeViewModel>, ViewModelHelper<FragmentColorSchemeViewModel>
{
public:
FragmentColorSchemeViewModel(const Model::FragmentColorSchemeEntry& entry, const Model::FragmentSettings& fragment, const Editor::ColorSchemeViewModel& deducedSchemeVM) :
_entry{ entry },
_fragment{ fragment },
_deducedSchemeVM{ deducedSchemeVM } {}
static bool SortAscending(const Editor::FragmentColorSchemeViewModel& lhs, const Editor::FragmentColorSchemeViewModel& rhs);
Editor::ColorSchemeViewModel ColorSchemeVM() const { return _deducedSchemeVM; };
hstring SourceName() const { return _fragment.Source(); }
hstring Json() const { return _entry.Json(); }
hstring AccessibleName() const noexcept;
private:
Model::FragmentColorSchemeEntry _entry;
Model::FragmentSettings _fragment;
Editor::ColorSchemeViewModel _deducedSchemeVM;
};
struct ExtensionPackageTemplateSelector : public ExtensionPackageTemplateSelectorT<ExtensionPackageTemplateSelector>
{
public:
ExtensionPackageTemplateSelector() = default;
Windows::UI::Xaml::DataTemplate SelectTemplateCore(const Windows::Foundation::IInspectable& item, const Windows::UI::Xaml::DependencyObject& container);
Windows::UI::Xaml::DataTemplate SelectTemplateCore(const Windows::Foundation::IInspectable& item);
WINRT_PROPERTY(Windows::UI::Xaml::DataTemplate, DefaultTemplate, nullptr);
WINRT_PROPERTY(Windows::UI::Xaml::DataTemplate, ComplexTemplate, nullptr);
};
};
namespace winrt::Microsoft::Terminal::Settings::Editor::factory_implementation
{
BASIC_FACTORY(Extensions);
BASIC_FACTORY(ExtensionPackageTemplateSelector);
}

View File

@ -0,0 +1,81 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import "ColorSchemesPageViewModel.idl";
namespace Microsoft.Terminal.Settings.Editor
{
[default_interface] runtimeclass Extensions : Windows.UI.Xaml.Controls.Page
{
Extensions();
ExtensionsViewModel ViewModel { get; };
Windows.UI.Xaml.Thickness CalculateMargin(Boolean hidden);
}
[default_interface] runtimeclass ExtensionsViewModel : Windows.UI.Xaml.Data.INotifyPropertyChanged
{
// Properties
ExtensionPackageViewModel CurrentExtensionPackage;
Windows.UI.Xaml.DataTemplate CurrentExtensionPackageIdentifierTemplate { get; };
Boolean IsExtensionView { get; };
Boolean NoExtensionPackages { get; };
Boolean NoProfilesModified { get; };
Boolean NoProfilesAdded { get; };
Boolean NoSchemesAdded { get; };
Boolean DisplayBadge { get; };
// Views
IVector<ExtensionPackageViewModel> ExtensionPackages { get; };
IObservableVector<FragmentProfileViewModel> ProfilesModified { get; };
IObservableVector<FragmentProfileViewModel> ProfilesAdded { get; };
IObservableVector<FragmentColorSchemeViewModel> ColorSchemesAdded { get; };
// Methods
void UpdateSettings(Microsoft.Terminal.Settings.Model.CascadiaSettings settings, ColorSchemesPageViewModel colorSchemesPageVM);
event Windows.Foundation.TypedEventHandler<Object, Guid> NavigateToProfileRequested;
event Windows.Foundation.TypedEventHandler<Object, ColorSchemeViewModel> NavigateToColorSchemeRequested;
}
[default_interface] runtimeclass ExtensionPackageViewModel : Windows.UI.Xaml.Data.INotifyPropertyChanged
{
Microsoft.Terminal.Settings.Model.ExtensionPackage Package { get; };
Boolean Enabled;
String Scope { get; };
String AccessibleName { get; };
IVector<FragmentExtensionViewModel> FragmentExtensions { get; };
}
[default_interface] runtimeclass FragmentExtensionViewModel : Windows.UI.Xaml.Data.INotifyPropertyChanged
{
Microsoft.Terminal.Settings.Model.FragmentSettings Fragment { get; };
IVectorView<FragmentProfileViewModel> ProfilesModified { get; };
IVectorView<FragmentProfileViewModel> ProfilesAdded { get; };
IVectorView<FragmentColorSchemeViewModel> ColorSchemesAdded { get; };
}
[default_interface] runtimeclass FragmentProfileViewModel : Windows.UI.Xaml.Data.INotifyPropertyChanged
{
Microsoft.Terminal.Settings.Model.Profile Profile { get; };
String SourceName { get; };
String Json { get; };
String AccessibleName { get; };
}
[default_interface] runtimeclass FragmentColorSchemeViewModel : Windows.UI.Xaml.Data.INotifyPropertyChanged
{
ColorSchemeViewModel ColorSchemeVM { get; };
String SourceName { get; };
String Json { get; };
String AccessibleName { get; };
}
[default_interface] runtimeclass ExtensionPackageTemplateSelector : Windows.UI.Xaml.Controls.DataTemplateSelector
{
ExtensionPackageTemplateSelector();
Windows.UI.Xaml.DataTemplate DefaultTemplate;
Windows.UI.Xaml.DataTemplate ComplexTemplate;
}
}

View File

@ -0,0 +1,496 @@
<!--
Copyright (c) Microsoft Corporation. All rights reserved. Licensed under
the MIT License. See LICENSE in the project root for license information.
-->
<Page x:Class="Microsoft.Terminal.Settings.Editor.Extensions"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Microsoft.Terminal.Settings.Editor"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:model="using:Microsoft.Terminal.Settings.Model"
xmlns:mtu="using:Microsoft.Terminal.UI"
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
mc:Ignorable="d">
<Page.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="CommonResources.xaml" />
</ResourceDictionary.MergedDictionaries>
<Style x:Key="ItalicDisclaimerStyle"
BasedOn="{StaticResource DisclaimerStyle}"
TargetType="TextBlock">
<Setter Property="FontStyle" Value="Italic" />
</Style>
<Style x:Key="CodeBlockStyle"
TargetType="TextBlock">
<Setter Property="FontFamily" Value="Cascadia Mono, Consolas" />
<Setter Property="IsTextSelectionEnabled" Value="True" />
</Style>
<Style x:Key="CodeBlockScrollViewerStyle"
TargetType="ScrollViewer">
<Setter Property="AutomationProperties.AccessibilityView" Value="Raw" />
<Setter Property="HorizontalScrollMode" Value="Auto" />
<Setter Property="HorizontalScrollBarVisibility" Value="Auto" />
<Setter Property="VerticalScrollMode" Value="Disabled" />
<Setter Property="VerticalScrollBarVisibility" Value="Disabled" />
</Style>
<local:ExtensionPackageTemplateSelector x:Key="ExtensionPackageIdentifierTemplateSelector"
ComplexTemplate="{StaticResource ComplexExtensionIdentifierTemplate}"
DefaultTemplate="{StaticResource DefaultExtensionIdentifierTemplate}" />
<DataTemplate x:Key="DefaultExtensionIdentifierTemplate"
x:DataType="local:ExtensionPackageViewModel">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<FontIcon Grid.Column="0"
Margin="0,0,8,0"
FontSize="32"
Glyph="&#xE74C;" />
<TextBlock Grid.Column="1"
VerticalAlignment="Center"
Text="{x:Bind Package.Source}" />
</Grid>
</DataTemplate>
<DataTemplate x:Key="ComplexExtensionIdentifierTemplate"
x:DataType="local:ExtensionPackageViewModel">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<IconSourceElement Grid.Row="0"
Grid.RowSpan="2"
Grid.Column="0"
Width="32"
Height="32"
Margin="0,0,8,0"
IconSource="{x:Bind mtu:IconPathConverter.IconSourceWUX(Package.Icon)}" />
<TextBlock Grid.Row="0"
Grid.Column="1"
Text="{x:Bind Package.DisplayName}" />
<TextBlock Grid.Row="1"
Grid.Column="1"
Style="{StaticResource SettingsPageItemDescriptionStyle}"
Text="{x:Bind Package.Source}" />
</Grid>
</DataTemplate>
<local:ExtensionPackageTemplateSelector x:Key="ExtensionPackageNavigatorTemplateSelector"
ComplexTemplate="{StaticResource ComplexExtensionNavigatorTemplate}"
DefaultTemplate="{StaticResource DefaultExtensionNavigatorTemplate}" />
<DataTemplate x:Key="DefaultExtensionNavigatorTemplate"
x:DataType="local:ExtensionPackageViewModel">
<Button AutomationProperties.Name="{x:Bind AccessibleName}"
Click="ExtensionNavigator_Click"
Style="{StaticResource NavigatorButtonStyle}"
Tag="{x:Bind}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ContentPresenter Content="{x:Bind}"
ContentTemplate="{StaticResource DefaultExtensionIdentifierTemplate}" />
<ToggleSwitch Grid.Column="1"
HorizontalAlignment="Right"
VerticalAlignment="Center"
AutomationProperties.Name="{x:Bind AccessibleName}"
IsOn="{x:Bind Enabled, Mode=TwoWay}"
Style="{StaticResource ToggleSwitchInExpanderStyle}" />
</Grid>
</Button>
</DataTemplate>
<DataTemplate x:Key="ComplexExtensionNavigatorTemplate"
x:DataType="local:ExtensionPackageViewModel">
<Button AutomationProperties.Name="{x:Bind AccessibleName}"
Click="ExtensionNavigator_Click"
Style="{StaticResource NavigatorButtonStyle}"
Tag="{x:Bind}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ContentPresenter Content="{x:Bind}"
ContentTemplate="{StaticResource ComplexExtensionIdentifierTemplate}" />
<ToggleSwitch Grid.Column="1"
HorizontalAlignment="Right"
VerticalAlignment="Center"
AutomationProperties.Name="{x:Bind AccessibleName}"
IsOn="{x:Bind Enabled, Mode=TwoWay}"
Style="{StaticResource ToggleSwitchInExpanderStyle}" />
</Grid>
</Button>
</DataTemplate>
<DataTemplate x:Key="FragmentProfileViewModelTemplate"
x:DataType="local:FragmentProfileViewModel">
<muxc:Expander AutomationProperties.Name="{x:Bind AccessibleName}"
Style="{StaticResource ExpanderStyle}">
<muxc:Expander.Header>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0"
Orientation="Horizontal">
<IconSourceElement Width="16"
Height="16"
Margin="0,0,8,0"
IconSource="{x:Bind mtu:IconPathConverter.IconSourceWUX(Profile.EvaluatedIcon), Mode=OneWay}" />
<TextBlock Text="{x:Bind Profile.Name, Mode=OneWay}" />
<Button x:Name="NavigateToProfileButton"
x:Uid="Extensions_NavigateToProfileButton"
Click="NavigateToProfile_Click"
Style="{StaticResource SettingContainerResetButtonStyle}"
Tag="{x:Bind Profile.Guid}">
<FontIcon Glyph="&#xE8A7;"
Style="{StaticResource SettingContainerFontIconStyle}" />
</Button>
</StackPanel>
<TextBlock Grid.Column="1"
Style="{StaticResource SettingContainerCurrentValueTextBlockStyle}"
Text="{x:Bind SourceName}" />
</Grid>
</muxc:Expander.Header>
<muxc:Expander.Content>
<ScrollViewer Style="{StaticResource CodeBlockScrollViewerStyle}">
<TextBlock Style="{StaticResource CodeBlockStyle}"
Text="{x:Bind Json, Mode=OneWay}" />
</ScrollViewer>
</muxc:Expander.Content>
</muxc:Expander>
</DataTemplate>
<!-- This styling matches that of ExpanderSettingContainerStyle for consistency -->
<Style x:Key="ExpanderStyle"
TargetType="muxc:Expander">
<Setter Property="MaxWidth" Value="1000" />
<Setter Property="MinHeight" Value="64" />
<Setter Property="Margin" Value="0,4,0,0" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
</Style>
<DataTemplate x:Key="JsonTemplate"
x:DataType="local:FragmentExtensionViewModel">
<muxc:Expander Header="{x:Bind Fragment.Filename}"
Style="{StaticResource ExpanderStyle}">
<ScrollViewer Style="{StaticResource CodeBlockScrollViewerStyle}">
<TextBlock Style="{StaticResource CodeBlockStyle}"
Text="{x:Bind Fragment.Json}" />
</ScrollViewer>
</muxc:Expander>
</DataTemplate>
<!--
Copied over from Appearances.xaml. We're unable to add the DataTemplate to CommonResources.xaml
because it needs a code-behind class to use {x:Bind}
-->
<DataTemplate x:Key="ColorChipTemplate"
x:DataType="local:ColorTableEntry">
<Border Width="8"
Height="8"
Background="{x:Bind mtu:Converters.ColorToBrush(Color)}"
CornerRadius="1" />
</DataTemplate>
<!--
Copied over from Appearances.xaml. We're unable to add the DataTemplate to CommonResources.xaml
because it needs a code-behind class to use {x:Bind}
-->
<DataTemplate x:Key="ColorSchemeVMTemplate"
x:DataType="local:ColorSchemeViewModel">
<StackPanel Orientation="Horizontal">
<Grid Grid.Column="0"
Padding="8"
VerticalAlignment="Center"
Background="{x:Bind mtu:Converters.ColorToBrush(BackgroundColor.Color), Mode=OneWay}"
ColumnSpacing="1"
CornerRadius="2"
RowSpacing="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ContentControl Grid.Row="0"
Grid.Column="0"
Content="{x:Bind ColorEntryAt(0), Mode=OneWay}"
ContentTemplate="{StaticResource ColorChipTemplate}"
IsTabStop="False" />
<ContentControl Grid.Row="0"
Grid.Column="1"
Content="{x:Bind ColorEntryAt(1), Mode=OneWay}"
ContentTemplate="{StaticResource ColorChipTemplate}"
IsTabStop="False" />
<ContentControl Grid.Row="0"
Grid.Column="2"
Content="{x:Bind ColorEntryAt(2), Mode=OneWay}"
ContentTemplate="{StaticResource ColorChipTemplate}"
IsTabStop="False" />
<ContentControl Grid.Row="0"
Grid.Column="3"
Content="{x:Bind ColorEntryAt(3), Mode=OneWay}"
ContentTemplate="{StaticResource ColorChipTemplate}"
IsTabStop="False" />
<ContentControl Grid.Row="0"
Grid.Column="4"
Content="{x:Bind ColorEntryAt(4), Mode=OneWay}"
ContentTemplate="{StaticResource ColorChipTemplate}"
IsTabStop="False" />
<ContentControl Grid.Row="0"
Grid.Column="5"
Content="{x:Bind ColorEntryAt(5), Mode=OneWay}"
ContentTemplate="{StaticResource ColorChipTemplate}"
IsTabStop="False" />
<ContentControl Grid.Row="0"
Grid.Column="6"
Content="{x:Bind ColorEntryAt(6), Mode=OneWay}"
ContentTemplate="{StaticResource ColorChipTemplate}"
IsTabStop="False" />
<ContentControl Grid.Row="0"
Grid.Column="7"
Content="{x:Bind ColorEntryAt(7), Mode=OneWay}"
ContentTemplate="{StaticResource ColorChipTemplate}"
IsTabStop="False" />
<ContentControl Grid.Row="1"
Grid.Column="0"
Content="{x:Bind ColorEntryAt(8), Mode=OneWay}"
ContentTemplate="{StaticResource ColorChipTemplate}"
IsTabStop="False" />
<ContentControl Grid.Row="1"
Grid.Column="1"
Content="{x:Bind ColorEntryAt(9), Mode=OneWay}"
ContentTemplate="{StaticResource ColorChipTemplate}"
IsTabStop="False" />
<ContentControl Grid.Row="1"
Grid.Column="2"
Content="{x:Bind ColorEntryAt(10), Mode=OneWay}"
ContentTemplate="{StaticResource ColorChipTemplate}"
IsTabStop="False" />
<ContentControl Grid.Row="1"
Grid.Column="3"
Content="{x:Bind ColorEntryAt(11), Mode=OneWay}"
ContentTemplate="{StaticResource ColorChipTemplate}"
IsTabStop="False" />
<ContentControl Grid.Row="1"
Grid.Column="4"
Content="{x:Bind ColorEntryAt(12), Mode=OneWay}"
ContentTemplate="{StaticResource ColorChipTemplate}"
IsTabStop="False" />
<ContentControl Grid.Row="1"
Grid.Column="5"
Content="{x:Bind ColorEntryAt(13), Mode=OneWay}"
ContentTemplate="{StaticResource ColorChipTemplate}"
IsTabStop="False" />
<ContentControl Grid.Row="1"
Grid.Column="6"
Content="{x:Bind ColorEntryAt(14), Mode=OneWay}"
ContentTemplate="{StaticResource ColorChipTemplate}"
IsTabStop="False" />
<ContentControl Grid.Row="1"
Grid.Column="7"
Content="{x:Bind ColorEntryAt(15), Mode=OneWay}"
ContentTemplate="{StaticResource ColorChipTemplate}"
IsTabStop="False" />
<TextBlock Grid.RowSpan="2"
Grid.Column="8"
MaxWidth="192"
Margin="4,0,4,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
AutomationProperties.AccessibilityView="Raw"
FontFamily="Cascadia Code"
Foreground="{x:Bind mtu:Converters.ColorToBrush(ForegroundColor.Color), Mode=OneWay}"
Text="{x:Bind Name, Mode=OneWay}"
TextTrimming="WordEllipsis" />
</Grid>
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="FragmentColorSchemeViewModelTemplate"
x:DataType="local:FragmentColorSchemeViewModel">
<muxc:Expander AutomationProperties.Name="{x:Bind AccessibleName}"
Style="{StaticResource ExpanderStyle}">
<muxc:Expander.Header>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0"
Orientation="Horizontal">
<ContentPresenter Content="{x:Bind ColorSchemeVM, Mode=OneWay}"
ContentTemplate="{StaticResource ColorSchemeVMTemplate}" />
<Button x:Name="NavigateToColorSchemeButton"
x:Uid="Extensions_NavigateToColorSchemeButton"
Click="NavigateToColorScheme_Click"
Style="{StaticResource SettingContainerResetButtonStyle}"
Tag="{x:Bind ColorSchemeVM}">
<FontIcon Glyph="&#xE8A7;"
Style="{StaticResource SettingContainerFontIconStyle}" />
</Button>
</StackPanel>
<TextBlock Grid.Column="1"
Style="{StaticResource SettingContainerCurrentValueTextBlockStyle}"
Text="{x:Bind SourceName}" />
</Grid>
</muxc:Expander.Header>
<muxc:Expander.Content>
<ScrollViewer Style="{StaticResource CodeBlockScrollViewerStyle}">
<TextBlock Style="{StaticResource CodeBlockStyle}"
Text="{x:Bind Json, Mode=OneWay}" />
</ScrollViewer>
</muxc:Expander.Content>
</muxc:Expander>
</DataTemplate>
</ResourceDictionary>
</Page.Resources>
<StackPanel Style="{StaticResource SettingsStackStyle}">
<!-- [Root View Only] -->
<StackPanel MaxWidth="{StaticResource StandardControlMaxWidth}"
Visibility="{x:Bind mtu:Converters.InvertedBooleanToVisibility(ViewModel.IsExtensionView), Mode=OneWay}">
<!-- Learn more about fragment extensions -->
<HyperlinkButton x:Uid="Extensions_DisclaimerHyperlink"
NavigateUri="https://learn.microsoft.com/en-us/windows/terminal/json-fragment-extensions" />
<!-- Grouping: Active Extensions -->
<TextBlock x:Uid="Extensions_ActiveExtensionsHeader"
Style="{StaticResource TextBlockSubHeaderStyle}" />
<ItemsControl x:Name="ActiveExtensionsList"
IsTabStop="False"
ItemTemplateSelector="{StaticResource ExtensionPackageNavigatorTemplateSelector}"
ItemsSource="{x:Bind ViewModel.ExtensionPackages}"
XYFocusKeyboardNavigation="Enabled" />
</StackPanel>
<!-- [Extension View Only] -->
<StackPanel MaxWidth="{StaticResource StandardControlMaxWidth}"
Visibility="{x:Bind ViewModel.IsExtensionView, Mode=OneWay}">
<!-- Extension Status -->
<muxc:Expander AutomationProperties.Name="{x:Bind ViewModel.CurrentExtensionPackage.AccessibleName, Mode=OneWay}"
IsExpanded="True"
Style="{StaticResource ExpanderStyle}">
<muxc:Expander.Header>
<Grid MinHeight="64">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<!--
BODGY
Theoretically, you could use a ContentTemplateSelector directly. However, that doesn't work.
For some reason, we just get the object type's ToString called and the selector gets nullptr as a parameter.
Adding the template as a view model property is a workaround.
-->
<ContentPresenter Grid.Column="0"
VerticalAlignment="Center"
Content="{x:Bind ViewModel.CurrentExtensionPackage, Mode=OneWay}"
ContentTemplate="{x:Bind ViewModel.CurrentExtensionPackageIdentifierTemplate, Mode=OneWay}" />
<ToggleSwitch Grid.Column="1"
Margin="0"
AutomationProperties.Name="{x:Bind ViewModel.CurrentExtensionPackage.AccessibleName, Mode=OneWay}"
IsOn="{x:Bind ViewModel.CurrentExtensionPackage.Enabled, Mode=TwoWay}"
Style="{StaticResource ToggleSwitchInExpanderStyle}"
Tag="{x:Bind ViewModel.CurrentExtensionPackage.Package.Source, Mode=OneWay}" />
</Grid>
</muxc:Expander.Header>
<muxc:Expander.Content>
<StackPanel>
<!-- Scope -->
<local:SettingContainer x:Uid="Extensions_Scope"
Content="{x:Bind ViewModel.CurrentExtensionPackage.Scope, Mode=OneWay}"
IsTabStop="False"
Style="{StaticResource SettingContainerWithTextContent}" />
<!-- JSON -->
<ItemsControl IsTabStop="False"
ItemTemplate="{StaticResource JsonTemplate}"
ItemsSource="{x:Bind ViewModel.CurrentExtensionPackage.FragmentExtensions, Mode=OneWay}" />
</StackPanel>
</muxc:Expander.Content>
</muxc:Expander>
</StackPanel>
<!-- Grouping: Modified Profiles -->
<StackPanel Margin="{x:Bind CalculateMargin(ViewModel.NoProfilesModified), Mode=OneWay}"
Visibility="{x:Bind mtu:Converters.InvertedBooleanToVisibility(ViewModel.NoProfilesModified), Mode=OneWay}">
<TextBlock x:Uid="Extensions_ModifiedProfilesHeader"
Style="{StaticResource TextBlockSubHeaderStyle}" />
<ItemsControl x:Name="ModifiedProfilesList"
IsTabStop="False"
ItemTemplate="{StaticResource FragmentProfileViewModelTemplate}"
ItemsSource="{x:Bind ViewModel.ProfilesModified, Mode=OneWay}"
XYFocusKeyboardNavigation="Enabled" />
</StackPanel>
<!-- Grouping: Added Profiles -->
<StackPanel Margin="{x:Bind CalculateMargin(ViewModel.NoProfilesAdded), Mode=OneWay}"
Visibility="{x:Bind mtu:Converters.InvertedBooleanToVisibility(ViewModel.NoProfilesAdded), Mode=OneWay}">
<TextBlock x:Uid="Extensions_AddedProfilesHeader"
Style="{StaticResource TextBlockSubHeaderStyle}" />
<ItemsControl x:Name="AddedProfilesList"
IsTabStop="False"
ItemTemplate="{StaticResource FragmentProfileViewModelTemplate}"
ItemsSource="{x:Bind ViewModel.ProfilesAdded, Mode=OneWay}"
XYFocusKeyboardNavigation="Enabled" />
</StackPanel>
<!-- Grouping: Added Color Schemes -->
<StackPanel Margin="{x:Bind CalculateMargin(ViewModel.NoProfilesAdded), Mode=OneWay}"
Visibility="{x:Bind mtu:Converters.InvertedBooleanToVisibility(ViewModel.NoSchemesAdded), Mode=OneWay}">
<TextBlock x:Uid="Extensions_AddedColorSchemesHeader"
Style="{StaticResource TextBlockSubHeaderStyle}" />
<ItemsControl x:Name="AddedColorSchemesList"
IsTabStop="False"
ItemTemplate="{StaticResource FragmentColorSchemeViewModelTemplate}"
ItemsSource="{x:Bind ViewModel.ColorSchemesAdded, Mode=OneWay}"
XYFocusKeyboardNavigation="Enabled" />
</StackPanel>
</StackPanel>
</Page>

View File

@ -9,6 +9,7 @@
#include "Compatibility.h"
#include "Rendering.h"
#include "RenderingViewModel.h"
#include "Extensions.h"
#include "Actions.h"
#include "ProfileViewModel.h"
#include "GlobalAppearance.h"
@ -45,6 +46,7 @@ static const std::wstring_view renderingTag{ L"Rendering_Nav" };
static const std::wstring_view compatibilityTag{ L"Compatibility_Nav" };
static const std::wstring_view actionsTag{ L"Actions_Nav" };
static const std::wstring_view newTabMenuTag{ L"NewTabMenu_Nav" };
static const std::wstring_view extensionsTag{ L"Extensions_Nav" };
static const std::wstring_view globalProfileTag{ L"GlobalProfile_Nav" };
static const std::wstring_view addProfileTag{ L"AddProfile" };
static const std::wstring_view colorSchemesTag{ L"ColorSchemes_Nav" };
@ -112,6 +114,33 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
}
});
auto extensionsVMImpl = winrt::make_self<ExtensionsViewModel>(_settingsClone, _colorSchemesPageVM);
extensionsVMImpl->NavigateToProfileRequested({ this, &MainPage::_NavigateToProfileHandler });
extensionsVMImpl->NavigateToColorSchemeRequested({ this, &MainPage::_NavigateToColorSchemeHandler });
_extensionsVM = *extensionsVMImpl;
_extensionsViewModelChangedRevoker = _extensionsVM.PropertyChanged(winrt::auto_revoke, [=](auto&&, const PropertyChangedEventArgs& args) {
const auto settingName{ args.PropertyName() };
if (settingName == L"CurrentExtensionPackage")
{
if (const auto& currentExtensionPackage = _extensionsVM.CurrentExtensionPackage())
{
const auto& pkg = currentExtensionPackage.Package();
const auto label = pkg.DisplayName().empty() ? pkg.Source() : pkg.DisplayName();
const auto crumb = winrt::make<Breadcrumb>(box_value(currentExtensionPackage), label, BreadcrumbSubPage::Extensions_Extension);
_breadcrumbs.Append(crumb);
SettingsMainPage_ScrollViewer().ScrollToVerticalOffset(0);
}
else
{
// If we don't have a current extension package, we're at the root of the Extensions page
_breadcrumbs.Clear();
const auto crumb = winrt::make<Breadcrumb>(box_value(extensionsTag), RS_(L"Nav_Extensions/Content"), BreadcrumbSubPage::None);
_breadcrumbs.Append(crumb);
}
contentFrame().Navigate(xaml_typename<Editor::Extensions>(), _extensionsVM);
}
});
// Make sure to initialize the profiles _after_ we have initialized the color schemes page VM, because we pass
// that VM into the appearance VMs within the profiles
_InitializeProfilesList();
@ -162,6 +191,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
// Update the Nav State with the new version of the settings
_colorSchemesPageVM.UpdateSettings(_settingsClone);
_newTabMenuPageVM.UpdateSettings(_settingsClone);
_extensionsVM.UpdateSettings(_settingsClone, _colorSchemesPageVM);
// We'll update the profile in the _profilesNavState whenever we actually navigate to one
@ -183,7 +213,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
// found the one that was selected before the refresh
SettingsNav().SelectedItem(item);
_Navigate(*stringTag, crumb->SubPage());
_Navigate(*breadcrumbStringTag, crumb->SubPage());
return;
}
}
@ -198,6 +228,17 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
return;
}
}
else if (const auto& breadcrumbExtensionPackage{ crumb->Tag().try_as<Editor::ExtensionPackageViewModel>() })
{
if (stringTag == extensionsTag)
{
// navigate to the Extensions page,
// _Navigate() will handle trying to find the right subpage
SettingsNav().SelectedItem(item);
_Navigate(breadcrumbExtensionPackage, BreadcrumbSubPage::Extensions_Extension);
return;
}
}
}
else if (const auto& profileTag{ tag.try_as<ProfileViewModel>() })
{
@ -457,6 +498,21 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
_breadcrumbs.Append(crumb);
}
}
else if (clickedItemTag == extensionsTag)
{
if (_extensionsVM.CurrentExtensionPackage())
{
// Setting CurrentExtensionPackage triggers the PropertyChanged event,
// which will navigate to the correct page and update the breadcrumbs appropriately
_extensionsVM.CurrentExtensionPackage(nullptr);
}
else
{
contentFrame().Navigate(xaml_typename<Editor::Extensions>(), _extensionsVM);
const auto crumb = winrt::make<Breadcrumb>(box_value(clickedItemTag), RS_(L"Nav_Extensions/Content"), BreadcrumbSubPage::None);
_breadcrumbs.Append(crumb);
}
}
else if (clickedItemTag == globalProfileTag)
{
auto profileVM{ _viewModelForProfile(_settingsClone.ProfileDefaults(), _settingsClone, Dispatcher()) };
@ -587,6 +643,40 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
}
}
void MainPage::_Navigate(const Editor::ExtensionPackageViewModel& extPkgVM, BreadcrumbSubPage subPage)
{
_PreNavigateHelper();
contentFrame().Navigate(xaml_typename<Editor::Extensions>(), _extensionsVM);
const auto crumb = winrt::make<Breadcrumb>(box_value(extensionsTag), RS_(L"Nav_Extensions/Content"), BreadcrumbSubPage::None);
_breadcrumbs.Append(crumb);
if (subPage == BreadcrumbSubPage::None)
{
_extensionsVM.CurrentExtensionPackage(nullptr);
}
else
{
bool found = false;
for (const auto& pkgVM : _extensionsVM.ExtensionPackages())
{
if (pkgVM.Package().Source() == extPkgVM.Package().Source())
{
// Take advantage of the PropertyChanged event to navigate
// to the correct extension package and build the breadcrumbs as we go
_extensionsVM.CurrentExtensionPackage(pkgVM);
found = true;
break;
}
}
if (!found)
{
// If we couldn't find a reasonable match, just go back to the root
_extensionsVM.CurrentExtensionPackage(nullptr);
}
}
}
void MainPage::SaveButton_Click(const IInspectable& /*sender*/, const RoutedEventArgs& /*args*/)
{
_settingsClone.LogSettingChanges(false);
@ -612,6 +702,10 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
_Navigate(*ntmEntryViewModel, subPage);
}
else if (const auto extPkgViewModel = tag.try_as<ExtensionPackageViewModel>())
{
_Navigate(*extPkgViewModel, subPage);
}
else
{
_Navigate(tag.as<hstring>(), subPage);
@ -809,6 +903,35 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
return _breadcrumbs;
}
void MainPage::_NavigateToProfileHandler(const IInspectable& /*sender*/, winrt::guid profileGuid)
{
for (auto&& menuItem : _menuItemSource)
{
if (const auto& navViewItem{ menuItem.try_as<MUX::Controls::NavigationViewItem>() })
{
if (const auto& tag{ navViewItem.Tag() })
{
if (const auto& profileTag{ tag.try_as<ProfileViewModel>() })
{
if (profileTag->OriginalProfileGuid() == profileGuid)
{
SettingsNav().SelectedItem(menuItem);
_Navigate(*profileTag, BreadcrumbSubPage::None);
return;
}
}
}
}
}
// Silently fail if the profile wasn't found
}
void MainPage::_NavigateToColorSchemeHandler(const IInspectable& /*sender*/, const IInspectable& /*args*/)
{
SettingsNav().SelectedItem(ColorSchemesNavItem());
_Navigate(hstring{ colorSchemesTag }, BreadcrumbSubPage::ColorSchemes_Edit);
}
winrt::Windows::UI::Xaml::Media::Brush MainPage::BackgroundBrush()
{
return SettingsNav().Background();

View File

@ -43,6 +43,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
winrt::Windows::UI::Xaml::Media::Brush BackgroundBrush();
Windows::Foundation::Collections::IObservableVector<IInspectable> Breadcrumbs() noexcept;
Editor::ExtensionsViewModel ExtensionsVM() const noexcept { return _extensionsVM; }
til::typed_event<Windows::Foundation::IInspectable, Model::SettingsTarget> OpenJson;
@ -68,16 +69,21 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
void _Navigate(hstring clickedItemTag, BreadcrumbSubPage subPage);
void _Navigate(const Editor::ProfileViewModel& profile, BreadcrumbSubPage subPage);
void _Navigate(const Editor::NewTabMenuEntryViewModel& ntmEntryVM, BreadcrumbSubPage subPage);
void _Navigate(const Editor::ExtensionPackageViewModel& extPkgVM, BreadcrumbSubPage subPage);
void _NavigateToProfileHandler(const IInspectable& sender, winrt::guid profileGuid);
void _NavigateToColorSchemeHandler(const IInspectable& sender, const IInspectable& args);
void _UpdateBackgroundForMica();
void _MoveXamlParsedNavItemsIntoItemSource();
winrt::Microsoft::Terminal::Settings::Editor::ColorSchemesPageViewModel _colorSchemesPageVM{ nullptr };
winrt::Microsoft::Terminal::Settings::Editor::NewTabMenuViewModel _newTabMenuPageVM{ nullptr };
winrt::Microsoft::Terminal::Settings::Editor::ExtensionsViewModel _extensionsVM{ nullptr };
Windows::UI::Xaml::Data::INotifyPropertyChanged::PropertyChanged_revoker _profileViewModelChangedRevoker;
Windows::UI::Xaml::Data::INotifyPropertyChanged::PropertyChanged_revoker _colorSchemesPageViewModelChangedRevoker;
Windows::UI::Xaml::Data::INotifyPropertyChanged::PropertyChanged_revoker _ntmViewModelChangedRevoker;
Windows::UI::Xaml::Data::INotifyPropertyChanged::PropertyChanged_revoker _extensionsViewModelChangedRevoker;
};
}

View File

@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import "Extensions.idl";
namespace Microsoft.Terminal.Settings.Editor
{
// Due to a XAML Compiler bug, it is hard for us to propagate an HWND into a XAML-using runtimeclass.
@ -20,7 +22,8 @@ namespace Microsoft.Terminal.Settings.Editor
Profile_Terminal,
Profile_Advanced,
ColorSchemes_Edit,
NewTabMenu_Folder
NewTabMenu_Folder,
Extensions_Extension
};
runtimeclass Breadcrumb : Windows.Foundation.IStringable
@ -42,6 +45,7 @@ namespace Microsoft.Terminal.Settings.Editor
void SetHostingWindow(UInt64 window);
Windows.Foundation.Collections.IObservableVector<IInspectable> Breadcrumbs { get; };
ExtensionsViewModel ExtensionsVM { get; };
Windows.UI.Xaml.Media.Brush BackgroundBrush { get; };
}

View File

@ -120,7 +120,8 @@
</muxc:NavigationViewItem.Icon>
</muxc:NavigationViewItem>
<muxc:NavigationViewItem x:Uid="Nav_ColorSchemes"
<muxc:NavigationViewItem x:Name="ColorSchemesNavItem"
x:Uid="Nav_ColorSchemes"
Tag="ColorSchemes_Nav">
<muxc:NavigationViewItem.Icon>
<FontIcon Glyph="&#xE790;" />
@ -155,6 +156,17 @@
</muxc:NavigationViewItem.Icon>
</muxc:NavigationViewItem>
<muxc:NavigationViewItem x:Uid="Nav_Extensions"
Tag="Extensions_Nav">
<muxc:NavigationViewItem.Icon>
<FontIcon Glyph="&#xEA86;" />
</muxc:NavigationViewItem.Icon>
<muxc:NavigationViewItem.InfoBadge>
<muxc:InfoBadge Style="{StaticResource NewInfoBadge}"
Visibility="{x:Bind ExtensionsVM.DisplayBadge, Mode=OneWay}" />
</muxc:NavigationViewItem.InfoBadge>
</muxc:NavigationViewItem>
<muxc:NavigationViewItemHeader x:Uid="Nav_Profiles" />
<muxc:NavigationViewItem x:Name="BaseLayerMenuItem"

View File

@ -125,6 +125,10 @@
<DependentUpon>NewTabMenuViewModel.idl</DependentUpon>
<SubType>Code</SubType>
</ClInclude>
<ClInclude Include="Extensions.h">
<DependentUpon>Extensions.xaml</DependentUpon>
<SubType>Code</SubType>
</ClInclude>
<ClInclude Include="Profiles_Base.h">
<DependentUpon>Profiles_Base.xaml</DependentUpon>
<SubType>Code</SubType>
@ -199,6 +203,9 @@
<Page Include="MainPage.xaml">
<SubType>Designer</SubType>
</Page>
<Page Include="Extensions.xaml">
<SubType>Designer</SubType>
</Page>
<Page Include="Profiles_Base.xaml">
<SubType>Designer</SubType>
</Page>
@ -309,6 +316,10 @@
<DependentUpon>NewTabMenuViewModel.idl</DependentUpon>
<SubType>Code</SubType>
</ClCompile>
<ClCompile Include="Extensions.cpp">
<DependentUpon>Extensions.xaml</DependentUpon>
<SubType>Code</SubType>
</ClCompile>
<ClCompile Include="Profiles_Base.cpp">
<DependentUpon>Profiles_Base.xaml</DependentUpon>
<SubType>Code</SubType>
@ -408,6 +419,10 @@
<Midl Include="GlobalAppearanceViewModel.idl" />
<Midl Include="LaunchViewModel.idl" />
<Midl Include="NewTabMenuViewModel.idl" />
<Midl Include="Extensions.idl">
<DependentUpon>Extensions.xaml</DependentUpon>
<SubType>Code</SubType>
</Midl>
<Midl Include="Profiles_Base.idl">
<DependentUpon>Profiles_Base.xaml</DependentUpon>
<SubType>Code</SubType>

View File

@ -22,8 +22,6 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
InitializeComponent();
_entryTemplateSelector = Resources().Lookup(box_value(L"NewTabMenuEntryTemplateSelector")).as<Editor::NewTabMenuEntryTemplateSelector>();
// Ideally, we'd bind IsEnabled to something like mtu:Converters.isEmpty(NewTabMenuListView.SelectedItems.Size) in the XAML,
// but the XAML compiler can't find NewTabMenuListView when we try that. Rather than copying the list of selected items over
// to the view model, we'll just do this instead (much simpler).

View File

@ -42,7 +42,6 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
WINRT_OBSERVABLE_PROPERTY(Editor::NewTabMenuViewModel, ViewModel, _PropertyChangedHandlers, nullptr);
private:
Editor::NewTabMenuEntryTemplateSelector _entryTemplateSelector{ nullptr };
Editor::NewTabMenuEntryViewModel _draggedEntry{ nullptr };
void _ScrollToEntry(const Editor::NewTabMenuEntryViewModel& entry);

View File

@ -321,7 +321,7 @@
<TextBlock x:Uid="NewTabMenu_CurrentFolderTextBlock"
Style="{StaticResource TextBlockSubHeaderStyle}" />
<!-- TODO CARLOS: Icon -->
<!-- TODO GH #18281: Icon -->
<!-- Once PR #17965 merges, we can add that kind of control to set an icon -->
<!-- Name -->

View File

@ -684,6 +684,10 @@
<value>Actions</value>
<comment>Header for the "actions" menu item. This navigates to a page that lets you see and modify commands, key bindings, and actions that can be done in the app.</comment>
</data>
<data name="Nav_Extensions.Content" xml:space="preserve">
<value>Extensions</value>
<comment>Header for the "extensions" menu item. This navigates to a page that lets you see and modify extensions for the app.</comment>
</data>
<data name="Profile_OpacitySlider.[using:Windows.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Background opacity</value>
<comment>Name for a control to determine the level of opacity for the background of the control. The user can choose to make the background of the app more or less opaque.</comment>
@ -2344,6 +2348,49 @@
<value>This option is managed by enterprise policy and cannot be changed here.</value>
<comment>This is displayed in concordance with Globals_StartOnUserLogin if the enterprise administrator has taken control of this setting.</comment>
</data>
<data name="Extensions_ActiveExtensionsHeader.Text" xml:space="preserve">
<value>Active Extensions</value>
</data>
<data name="Extensions_ModifiedProfilesHeader.Text" xml:space="preserve">
<value>Modified Profiles</value>
</data>
<data name="Extensions_AddedProfilesHeader.Text" xml:space="preserve">
<value>Added Profiles</value>
</data>
<data name="Extensions_AddedColorSchemesHeader.Text" xml:space="preserve">
<value>Added Color Schemes</value>
</data>
<data name="Extensions_DisclaimerHyperlink.Content" xml:space="preserve">
<value>Learn more about extensions</value>
</data>
<data name="Extensions_NavigateToProfileButton.[using:Windows.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>Navigate to profile</value>
</data>
<data name="Extensions_NavigateToProfileButton.[using:Windows.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Navigate to profile</value>
</data>
<data name="Extensions_NavigateToColorSchemeButton.[using:Windows.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>Navigate to color scheme</value>
</data>
<data name="Extensions_NavigateToColorSchemeButton.[using:Windows.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Navigate to color scheme</value>
</data>
<data name="Extensions_ScopeUser" xml:space="preserve">
<value>Current User</value>
<comment>Label for the installation scope of an extension.</comment>
</data>
<data name="Extensions_ScopeSystem" xml:space="preserve">
<value>All Users</value>
<comment>Label for the installation scope of an extension</comment>
</data>
<data name="Extensions_Scope.Header" xml:space="preserve">
<value>Scope</value>
<comment>Header for the installation scope of the extension</comment>
</data>
<data name="NewInfoBadgeTextBlock.Text" xml:space="preserve">
<value>NEW</value>
<comment>Text is used on an info badge for new navigation items. Must be all caps.</comment>
</data>
<data name="NewTabMenu_AddMatchProfiles_Help.Content" xml:space="preserve">
<value>Learn more about regular expressions</value>
</data>

View File

@ -134,6 +134,7 @@
</Style>
<Style x:Key="SettingContainerResetButtonStyle"
BasedOn="{StaticResource DefaultButtonStyle}"
TargetType="Button">
<Setter Property="Margin" Value="5,0,0,0" />
<Setter Property="Height" Value="19" />
@ -179,13 +180,18 @@
<Setter Property="FontFamily" Value="Segoe UI, Segoe Fluent Icons, Segoe MDL2 Assets" />
</Style>
<Style x:Key="SettingContainerCurrentValueTextBlockStyle"
BasedOn="{StaticResource SettingsPageItemDescriptionStyle}"
TargetType="TextBlock">
<Setter Property="MaxWidth" Value="250" />
<Setter Property="Margin" Value="0,0,-16,0" />
<Setter Property="HorizontalAlignment" Value="Right" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="FontFamily" Value="Segoe UI, Segoe Fluent Icons, Segoe MDL2 Assets" />
</Style>
<DataTemplate x:Key="ExpanderSettingContainerStringPreviewTemplate">
<TextBlock MaxWidth="250"
Margin="0,0,-16,0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
FontFamily="Segoe UI, Segoe Fluent Icons, Segoe MDL2 Assets"
Style="{StaticResource SettingsPageItemDescriptionStyle}"
<TextBlock Style="{StaticResource SettingContainerCurrentValueTextBlockStyle}"
Text="{Binding}" />
</DataTemplate>
@ -228,6 +234,45 @@
</Setter>
</Style>
<!-- A basic setting container displaying immutable text as content -->
<Style x:Key="SettingContainerWithTextContent"
TargetType="local:SettingContainer">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:SettingContainer">
<Grid AutomationProperties.Name="{TemplateBinding Header}"
Style="{StaticResource NonExpanderGrid}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0"
Style="{StaticResource StackPanelInExpanderStyle}">
<StackPanel Orientation="Horizontal">
<TextBlock Style="{StaticResource SettingsPageItemHeaderStyle}"
Text="{TemplateBinding Header}" />
<Button x:Name="ResetButton"
Style="{StaticResource SettingContainerResetButtonStyle}">
<FontIcon Glyph="&#xE845;"
Style="{StaticResource SettingContainerFontIconStyle}" />
</Button>
</StackPanel>
<TextBlock x:Name="HelpTextBlock"
Style="{StaticResource SettingsPageItemDescriptionStyle}"
Text="{TemplateBinding HelpText}" />
</StackPanel>
<TextBlock Grid.Column="1"
Margin="0,0,8,0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Style="{ThemeResource SecondaryTextBlockStyle}"
Text="{TemplateBinding Content}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!--
A setting container for a setting that has no additional options.
Includes space for an icon on the left side of the header.
@ -302,8 +347,7 @@
<StackPanel Grid.Column="0"
Style="{StaticResource StackPanelInExpanderStyle}">
<StackPanel Orientation="Horizontal">
<TextBlock Style="{StaticResource SettingsPageItemHeaderStyle}"
Text="{TemplateBinding Header}" />
<ContentPresenter Content="{TemplateBinding Header}" />
<Button x:Name="ResetButton"
Style="{StaticResource SettingContainerResetButtonStyle}">
<FontIcon Glyph="&#xE845;"

View File

@ -309,6 +309,31 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
_throttler();
}
bool ApplicationState::DismissBadge(const hstring& badgeId)
{
bool inserted{ false };
{
const auto state = _state.lock();
if (!state->DismissedBadges)
{
state->DismissedBadges = std::unordered_set<hstring>{};
}
inserted = state->DismissedBadges->insert(badgeId).second;
}
_throttler();
return inserted;
}
bool ApplicationState::BadgeDismissed(const hstring& badgeId) const
{
const auto state = _state.lock_shared();
if (state->DismissedBadges)
{
return state->DismissedBadges->contains(badgeId);
}
return false;
}
// Generate all getter/setters
#define MTSM_APPLICATION_STATE_GEN(source, type, name, key, ...) \
type ApplicationState::name() const noexcept \

View File

@ -40,7 +40,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
X(FileSource::Local, Windows::Foundation::Collections::IVector<Model::WindowLayout>, PersistedWindowLayouts, "persistedWindowLayouts") \
X(FileSource::Shared, Windows::Foundation::Collections::IVector<hstring>, RecentCommands, "recentCommands") \
X(FileSource::Shared, Windows::Foundation::Collections::IVector<winrt::Microsoft::Terminal::Settings::Model::InfoBarMessage>, DismissedMessages, "dismissedMessages") \
X(FileSource::Local, Windows::Foundation::Collections::IVector<hstring>, AllowedCommandlines, "allowedCommandlines")
X(FileSource::Local, Windows::Foundation::Collections::IVector<hstring>, AllowedCommandlines, "allowedCommandlines") \
X(FileSource::Local, std::unordered_set<hstring>, DismissedBadges, "dismissedBadges")
struct WindowLayout : WindowLayoutT<WindowLayout>
{
@ -70,6 +71,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
Json::Value ToJson(FileSource parseSource) const noexcept;
void AppendPersistedWindowLayout(Model::WindowLayout layout);
bool DismissBadge(const hstring& badgeId);
bool BadgeDismissed(const hstring& badgeId) const;
// State getters/setters
#define MTSM_APPLICATION_STATE_GEN(source, type, name, key, ...) \

View File

@ -33,6 +33,8 @@ namespace Microsoft.Terminal.Settings.Model
void Reset();
void AppendPersistedWindowLayout(WindowLayout layout);
Boolean DismissBadge(String badgeId);
Boolean BadgeDismissed(String badgeId);
String SettingsHash;
Windows.Foundation.Collections.IVector<WindowLayout> PersistedWindowLayouts;

View File

@ -8,16 +8,29 @@
#include "../../inc/DefaultSettings.h"
#include "DynamicProfileUtils.h"
#include <LibraryResources.h>
using namespace ::Microsoft::Terminal::Settings::Model;
using namespace winrt::Microsoft::Terminal::Settings::Model;
using namespace winrt::Microsoft::Terminal::TerminalConnection;
std::wstring_view GENERATOR_ICON_PATH{ L"ms-appx:///ProfileGeneratorIcons/AzureCloudShell.png" };
std::wstring_view AzureCloudShellGenerator::GetNamespace() const noexcept
{
return AzureGeneratorNamespace;
}
std::wstring_view AzureCloudShellGenerator::GetDisplayName() const noexcept
{
return RS_(L"AzureCloudShellGeneratorDisplayName");
}
std::wstring_view AzureCloudShellGenerator::GetIcon() const noexcept
{
return GENERATOR_ICON_PATH;
}
// Method Description:
// - Checks if the Azure Cloud shell is available on this platform, and if it
// is, creates a profile to be able to launch it.

View File

@ -25,6 +25,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model
{
public:
std::wstring_view GetNamespace() const noexcept override;
std::wstring_view GetDisplayName() const noexcept override;
std::wstring_view GetIcon() const noexcept override;
void GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles) const override;
};
};

View File

@ -114,6 +114,10 @@ Model::CascadiaSettings CascadiaSettings::Copy() const
settings->_globals = _globals->Copy();
settings->_allProfiles = winrt::single_threaded_observable_vector(std::move(allProfiles));
settings->_activeProfiles = winrt::single_threaded_observable_vector(std::move(activeProfiles));
// extension packages don't need a deep clone
// because they're fully immutable. We can just copy the reference over instead.
settings->_extensionPackages = _extensionPackages;
}
// load errors
@ -174,6 +178,16 @@ IObservableVector<Model::Profile> CascadiaSettings::ActiveProfiles() const noexc
return _activeProfiles;
}
IVectorView<Model::ExtensionPackage> CascadiaSettings::Extensions()
{
if (!_extensionPackages)
{
// Lazy load the ExtensionPackage objects
_extensionPackages = winrt::single_threaded_vector<Model::ExtensionPackage>(std::move(SettingsLoader::LoadExtensionPackages()));
}
return _extensionPackages.GetView();
}
// Method Description:
// - Returns the globally configured keybindings
// Arguments:

View File

@ -18,6 +18,10 @@ Author(s):
#pragma once
#include "CascadiaSettings.g.h"
#include "FragmentSettings.g.h"
#include "FragmentProfileEntry.g.h"
#include "FragmentColorSchemeEntry.g.h"
#include "ExtensionPackage.g.h"
#include "GlobalAppSettings.h"
#include "Profile.h"
@ -39,6 +43,28 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
std::runtime_error(message) {}
};
struct ExtensionPackage : ExtensionPackageT<ExtensionPackage>
{
public:
ExtensionPackage(hstring source, FragmentScope scope) :
_source{ source },
_scope{ scope },
_fragments{ winrt::single_threaded_vector<Model::FragmentSettings>() } {}
hstring Source() const noexcept { return _source; }
FragmentScope Scope() const noexcept { return _scope; }
Windows::Foundation::Collections::IVectorView<Model::FragmentSettings> FragmentsView() const noexcept { return _fragments.GetView(); }
Windows::Foundation::Collections::IVector<Model::FragmentSettings> Fragments() const noexcept { return _fragments; }
WINRT_PROPERTY(hstring, Icon);
WINRT_PROPERTY(hstring, DisplayName);
private:
hstring _source;
FragmentScope _scope;
Windows::Foundation::Collections::IVector<Model::FragmentSettings> _fragments;
};
struct ParsedSettings
{
winrt::com_ptr<implementation::GlobalAppSettings> globals;
@ -56,12 +82,14 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
struct SettingsLoader
{
static SettingsLoader Default(const std::string_view& userJSON, const std::string_view& inboxJSON);
static std::vector<Model::ExtensionPackage> LoadExtensionPackages();
SettingsLoader(const std::string_view& userJSON, const std::string_view& inboxJSON);
void GenerateProfiles();
void GenerateExtensionPackagesFromProfileGenerators();
void ApplyRuntimeInitialSettings();
void MergeInboxIntoUserSettings();
void FindFragmentsAndMergeIntoUserSettings();
void FindFragmentsAndMergeIntoUserSettings(bool generateExtensionPackages);
void MergeFragmentIntoUserSettings(const winrt::hstring& source, const std::string_view& content);
void FinalizeLayering();
bool DisableDeletedProfiles();
@ -70,6 +98,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
ParsedSettings inboxSettings;
ParsedSettings userSettings;
std::unordered_map<hstring, winrt::com_ptr<implementation::ExtensionPackage>> extensionPackageMap;
bool duplicateProfile = false;
private:
@ -81,6 +110,12 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
const Json::Value& profilesList;
const Json::Value& themes;
};
struct ParseFragmentMetadata
{
std::wstring_view jsonFilename;
FragmentScope scope;
};
SettingsLoader() = default;
static std::pair<size_t, size_t> _lineAndColumnFromPosition(const std::string_view& string, const size_t position);
static void _rethrowSerializationExceptionWithLocationInfo(const JsonUtils::DeserializationError& e, const std::string_view& settingsString);
@ -88,13 +123,15 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
static const Json::Value& _getJSONValue(const Json::Value& json, const std::string_view& key) noexcept;
std::span<const winrt::com_ptr<implementation::Profile>> _getNonUserOriginProfiles() const;
void _parse(const OriginTag origin, const winrt::hstring& source, const std::string_view& content, ParsedSettings& settings);
void _parseFragment(const winrt::hstring& source, const std::string_view& content, ParsedSettings& settings);
void _parseFragment(const winrt::hstring& source, const std::string_view& content, ParsedSettings& settings, const std::optional<ParseFragmentMetadata>& fragmentMeta);
static JsonSettings _parseJson(const std::string_view& content);
static winrt::com_ptr<implementation::Profile> _parseProfile(const OriginTag origin, const winrt::hstring& source, const Json::Value& profileJson);
void _appendProfile(winrt::com_ptr<Profile>&& profile, const winrt::guid& guid, ParsedSettings& settings);
void _addUserProfileParent(const winrt::com_ptr<implementation::Profile>& profile);
void _addOrMergeUserColorScheme(const winrt::com_ptr<implementation::ColorScheme>& colorScheme);
void _executeGenerator(const IDynamicProfileGenerator& generator);
bool _addOrMergeUserColorScheme(const winrt::com_ptr<implementation::ColorScheme>& colorScheme);
static void _executeGenerator(const IDynamicProfileGenerator& generator, std::vector<winrt::com_ptr<implementation::Profile>>& profilesList);
winrt::com_ptr<implementation::ExtensionPackage> _registerFragment(const winrt::Microsoft::Terminal::Settings::Model::FragmentSettings& fragment, FragmentScope scope);
Json::StreamWriterBuilder _getJsonStyledWriter();
std::unordered_set<winrt::hstring, til::transparent_hstring_hash, til::transparent_hstring_equal_to> _ignoredNamespaces;
std::set<std::string> themesChangeLog;
@ -127,6 +164,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
winrt::Windows::Foundation::Collections::IObservableVector<Model::Profile> AllProfiles() const noexcept;
winrt::Windows::Foundation::Collections::IObservableVector<Model::Profile> ActiveProfiles() const noexcept;
Model::ActionMap ActionMap() const noexcept;
winrt::Windows::Foundation::Collections::IVectorView<Model::ExtensionPackage> Extensions();
void ResetApplicationState() const;
void ResetToDefaultSettings();
void WriteSettingsToDisk();
@ -188,6 +226,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
winrt::com_ptr<implementation::Profile> _baseLayerProfile = winrt::make_self<implementation::Profile>();
winrt::Windows::Foundation::Collections::IObservableVector<Model::Profile> _allProfiles = winrt::single_threaded_observable_vector<Model::Profile>();
winrt::Windows::Foundation::Collections::IObservableVector<Model::Profile> _activeProfiles = winrt::single_threaded_observable_vector<Model::Profile>();
winrt::Windows::Foundation::Collections::IVector<Model::ExtensionPackage> _extensionPackages = nullptr;
std::set<std::string> _themesChangeLog{};
// load errors
@ -203,6 +242,68 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
mutable std::once_flag _commandLinesCacheOnce;
mutable std::vector<std::pair<std::wstring, Model::Profile>> _commandLinesCache;
};
struct FragmentProfileEntry : FragmentProfileEntryT<FragmentProfileEntry>
{
public:
FragmentProfileEntry(winrt::guid profileGuid, hstring json) :
_profileGuid{ profileGuid },
_json{ json } {}
winrt::guid ProfileGuid() const noexcept { return _profileGuid; }
hstring Json() const noexcept { return _json; }
private:
winrt::guid _profileGuid;
hstring _json;
};
struct FragmentColorSchemeEntry : FragmentColorSchemeEntryT<FragmentColorSchemeEntry>
{
public:
FragmentColorSchemeEntry(hstring schemeName, hstring json) :
_schemeName{ schemeName },
_json{ json } {}
hstring ColorSchemeName() const noexcept { return _schemeName; }
hstring Json() const noexcept { return _json; }
private:
hstring _schemeName;
hstring _json;
};
struct FragmentSettings : FragmentSettingsT<FragmentSettings>
{
public:
FragmentSettings(hstring source, hstring json, hstring filename) :
_source{ source },
_json{ json },
_filename{ filename } {}
hstring Source() const noexcept { return _source; }
hstring Json() const noexcept { return _json; }
hstring Filename() const noexcept { return _filename; }
Windows::Foundation::Collections::IVector<Model::FragmentProfileEntry> ModifiedProfiles() const noexcept { return _modifiedProfiles; }
void ModifiedProfiles(const Windows::Foundation::Collections::IVector<Model::FragmentProfileEntry>& modifiedProfiles) noexcept { _modifiedProfiles = modifiedProfiles; }
Windows::Foundation::Collections::IVector<Model::FragmentProfileEntry> NewProfiles() const noexcept { return _newProfiles; }
void NewProfiles(const Windows::Foundation::Collections::IVector<Model::FragmentProfileEntry>& newProfiles) noexcept { _newProfiles = newProfiles; }
Windows::Foundation::Collections::IVector<Model::FragmentColorSchemeEntry> ColorSchemes() const noexcept { return _colorSchemes; }
void ColorSchemes(const Windows::Foundation::Collections::IVector<Model::FragmentColorSchemeEntry>& colorSchemes) noexcept { _colorSchemes = colorSchemes; }
// views
Windows::Foundation::Collections::IVectorView<Model::FragmentProfileEntry> ModifiedProfilesView() const noexcept { return _modifiedProfiles ? _modifiedProfiles.GetView() : nullptr; }
Windows::Foundation::Collections::IVectorView<Model::FragmentProfileEntry> NewProfilesView() const noexcept { return _newProfiles ? _newProfiles.GetView() : nullptr; }
Windows::Foundation::Collections::IVectorView<Model::FragmentColorSchemeEntry> ColorSchemesView() const noexcept { return _colorSchemes ? _colorSchemes.GetView() : nullptr; }
private:
hstring _source;
hstring _json;
hstring _filename;
Windows::Foundation::Collections::IVector<Model::FragmentProfileEntry> _modifiedProfiles;
Windows::Foundation::Collections::IVector<Model::FragmentProfileEntry> _newProfiles;
Windows::Foundation::Collections::IVector<Model::FragmentColorSchemeEntry> _colorSchemes;
};
}
namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation

View File

@ -8,6 +8,12 @@ import "DefaultTerminal.idl";
namespace Microsoft.Terminal.Settings.Model
{
enum FragmentScope
{
User,
Machine
};
[default_interface] runtimeclass CascadiaSettings {
static CascadiaSettings LoadDefaults();
static CascadiaSettings LoadAll();
@ -40,6 +46,7 @@ namespace Microsoft.Terminal.Settings.Model
Profile DuplicateProfile(Profile sourceProfile);
ActionMap ActionMap { get; };
Windows.Foundation.Collections.IVectorView<ExtensionPackage> Extensions { get; };
IVectorView<SettingsLoadWarnings> Warnings { get; };
Windows.Foundation.IReference<SettingsLoadErrors> GetLoadingError { get; };
@ -58,4 +65,35 @@ namespace Microsoft.Terminal.Settings.Model
void ExpandCommands();
}
runtimeclass FragmentProfileEntry
{
Guid ProfileGuid { get; };
String Json { get; };
}
runtimeclass FragmentColorSchemeEntry
{
String ColorSchemeName { get; };
String Json { get; };
}
runtimeclass FragmentSettings
{
String Source { get; };
String Json { get; };
String Filename { get; };
IVectorView<FragmentProfileEntry> ModifiedProfilesView { get; };
IVectorView<FragmentProfileEntry> NewProfilesView { get; };
IVectorView<FragmentColorSchemeEntry> ColorSchemesView { get; };
}
runtimeclass ExtensionPackage
{
String Source { get; };
String DisplayName { get; };
String Icon { get; };
FragmentScope Scope { get; };
IVectorView<FragmentSettings> FragmentsView { get; };
}
}

View File

@ -125,6 +125,20 @@ SettingsLoader SettingsLoader::Default(const std::string_view& userJSON, const s
return loader;
}
std::vector<Model::ExtensionPackage> SettingsLoader::LoadExtensionPackages()
{
SettingsLoader loader{};
loader.GenerateExtensionPackagesFromProfileGenerators();
loader.FindFragmentsAndMergeIntoUserSettings(true /*generateExtensionPackages*/);
std::vector<Model::ExtensionPackage> extensionPackages;
for (auto [_, extPkg] : loader.extensionPackageMap)
{
extensionPackages.emplace_back(std::move(*extPkg));
}
return extensionPackages;
}
// The SettingsLoader class is an internal implementation detail of CascadiaSettings.
// Member methods aren't safe against misuse and you need to ensure to call them in a specific order.
// See CascadiaSettings::LoadAll() for a specific usage example.
@ -175,16 +189,83 @@ SettingsLoader::SettingsLoader(const std::string_view& userJSON, const std::stri
_userProfileCount = userSettings.profiles.size();
}
// This method is used to generate the JSON writer used for writing json in a styled format.
// We use it a few times throughout the loader, so we lazy load it and cache it here.
Json::StreamWriterBuilder SettingsLoader::_getJsonStyledWriter()
{
static bool jsonWriterInitialized = false;
static Json::StreamWriterBuilder styledWriter;
if (!jsonWriterInitialized)
{
styledWriter["indentation"] = " ";
styledWriter["commentStyle"] = "All";
styledWriter.settings_["enableYAMLCompatibility"] = true; // suppress spaces around colons
styledWriter.settings_["precision"] = 6; // prevent values like 1.1000000000000001
jsonWriterInitialized = true;
}
return styledWriter;
}
// Generate dynamic profiles and add them to the list of "inbox" profiles
// (meaning profiles specified by the application rather by the user).
void SettingsLoader::GenerateProfiles()
{
_executeGenerator(PowershellCoreProfileGenerator{});
_executeGenerator(WslDistroGenerator{});
_executeGenerator(AzureCloudShellGenerator{});
_executeGenerator(VisualStudioGenerator{});
auto generateProfiles = [&](const IDynamicProfileGenerator& generator) {
if (!_ignoredNamespaces.contains(generator.GetNamespace()))
{
_executeGenerator(generator, inboxSettings.profiles);
}
};
// Generate profiles for each generator and add them to the inbox settings.
// Be sure to update the same list below.
generateProfiles(PowershellCoreProfileGenerator{});
generateProfiles(WslDistroGenerator{});
generateProfiles(AzureCloudShellGenerator{});
generateProfiles(VisualStudioGenerator{});
#if TIL_FEATURE_DYNAMICSSHPROFILES_ENABLED
_executeGenerator(SshHostGenerator{});
generateProfiles(SshHostGenerator{});
#endif
}
// Generate ExtensionPackage objects from the profile generators.
void SettingsLoader::GenerateExtensionPackagesFromProfileGenerators()
{
auto generateExtensionPackages = [&](const IDynamicProfileGenerator& generator) {
std::vector<winrt::com_ptr<implementation::Profile>> profilesList;
_executeGenerator(generator, profilesList);
// These are needed for the FragmentSettings object
std::vector<Model::FragmentProfileEntry> profileEntries;
Json::Value profilesListJson{ Json::ValueType::arrayValue };
for (const auto& profile : profilesList)
{
const auto profileJson = profile->ToJson();
profilesListJson.append(profileJson);
profileEntries.push_back(winrt::make<FragmentProfileEntry>(profile->Guid(), hstring{ til::u8u16(Json::writeString(_getJsonStyledWriter(), profileJson)) }));
}
// Manually construct the JSON for the FragmentSettings object
Json::Value json{ Json::ValueType::objectValue };
json[JsonKey(ProfilesKey)] = profilesListJson;
auto generatorExtension = winrt::make_self<FragmentSettings>(hstring{ generator.GetNamespace() }, hstring{ til::u8u16(Json::writeString(_getJsonStyledWriter(), json)) }, hstring{ L"settings.json" });
generatorExtension->NewProfiles(winrt::single_threaded_vector<Model::FragmentProfileEntry>(std::move(profileEntries)));
auto extPkg = _registerFragment(std::move(*generatorExtension), FragmentScope::Machine);
extPkg->DisplayName(hstring{ generator.GetDisplayName() });
extPkg->Icon(hstring{ generator.GetIcon() });
};
// Generate extension package objects for each generator.
// Be sure to update the same list above.
generateExtensionPackages(PowershellCoreProfileGenerator{});
generateExtensionPackages(WslDistroGenerator{});
generateExtensionPackages(AzureCloudShellGenerator{});
generateExtensionPackages(VisualStudioGenerator{});
#if TIL_FEATURE_DYNAMICSSHPROFILES_ENABLED
generateExtensionPackages(SshHostGenerator{});
#endif
}
@ -243,21 +324,27 @@ void SettingsLoader::MergeInboxIntoUserSettings()
// merge them. Unfortunately however the "updates" key in fragment profiles make this impossible:
// The targeted profile might be one that got created as part of SettingsLoader::MergeInboxIntoUserSettings.
// Additionally the GUID in "updates" will conflict with existing GUIDs in .inboxSettings.
void SettingsLoader::FindFragmentsAndMergeIntoUserSettings()
void SettingsLoader::FindFragmentsAndMergeIntoUserSettings(bool generateExtensionPackages)
{
ParsedSettings fragmentSettings;
const auto parseAndLayerFragmentFiles = [&](const std::filesystem::path& path, const winrt::hstring& source) {
const auto parseAndLayerFragmentFiles = [&](const std::filesystem::path& path, const winrt::hstring& source, FragmentScope scope) {
for (const auto& fragmentExt : std::filesystem::directory_iterator{ path })
{
if (fragmentExt.path().extension() == jsonExtension)
const auto fragExtPath = fragmentExt.path();
if (fragExtPath.extension() == jsonExtension)
{
try
{
const auto content = til::io::read_file_as_utf8_string_if_exists(fragmentExt.path());
const auto content = til::io::read_file_as_utf8_string_if_exists(fragExtPath);
if (!content.empty())
{
_parseFragment(source, content, fragmentSettings);
_parseFragment(source,
content,
fragmentSettings,
generateExtensionPackages ?
static_cast<std::optional<ParseFragmentMetadata>>(ParseFragmentMetadata{ fragExtPath.filename().wstring(), scope }) :
std::nullopt);
}
}
CATCH_LOG();
@ -279,9 +366,11 @@ void SettingsLoader::FindFragmentsAndMergeIntoUserSettings()
const auto filename = fragmentExtFolder.path().filename();
const auto& source = filename.native();
if (!_ignoredNamespaces.contains(std::wstring_view{ source }) && fragmentExtFolder.is_directory())
if (fragmentExtFolder.is_directory())
{
parseAndLayerFragmentFiles(fragmentExtFolder.path(), winrt::hstring{ source });
parseAndLayerFragmentFiles(fragmentExtFolder.path(),
winrt::hstring{ source },
rfid == FOLDERID_LocalAppData ? FragmentScope::User : FragmentScope::Machine); // scope
}
}
}
@ -313,8 +402,13 @@ void SettingsLoader::FindFragmentsAndMergeIntoUserSettings()
for (const auto& ext : extensions)
{
const auto packageName = ext.Package().Id().FamilyName();
if (_ignoredNamespaces.contains(std::wstring_view{ packageName }))
const auto& package = ext.Package();
const auto packageName = package.Id().FamilyName();
// If the extension was explicitly disabled, skip over it early to avoid the async API!
// NOTE: only do this if we're NOT generating extension packages. If we are, we need to get all the
// package metadata anyway to display in the settings UI later.
if (!generateExtensionPackages && _ignoredNamespaces.contains(std::wstring_view{ packageName }))
{
continue;
}
@ -335,7 +429,18 @@ void SettingsLoader::FindFragmentsAndMergeIntoUserSettings()
if (std::filesystem::is_directory(path))
{
parseAndLayerFragmentFiles(path, packageName);
// MSIX does not support machine-wide scope
// See https://github.com/microsoft/winget-cli/discussions/1983
parseAndLayerFragmentFiles(path,
packageName,
FragmentScope::User);
if (generateExtensionPackages)
{
auto extPkg = extensionPackageMap[packageName];
extPkg->Icon(package.Logo().AbsoluteUri());
extPkg->DisplayName(package.DisplayName());
}
}
}
}
@ -346,7 +451,7 @@ void SettingsLoader::FindFragmentsAndMergeIntoUserSettings()
void SettingsLoader::MergeFragmentIntoUserSettings(const winrt::hstring& source, const std::string_view& content)
{
ParsedSettings fragmentSettings;
_parseFragment(source, content, fragmentSettings);
_parseFragment(source, content, fragmentSettings, std::nullopt);
}
// Call this method before passing SettingsLoader to the CascadiaSettings constructor.
@ -725,15 +830,23 @@ void SettingsLoader::_parse(const OriginTag origin, const winrt::hstring& source
// Just like _parse, but is to be used for fragment files, which don't support anything but color
// schemes and profiles. Additionally this function supports profiles which specify an "updates" key.
void SettingsLoader::_parseFragment(const winrt::hstring& source, const std::string_view& content, ParsedSettings& settings)
// - fragmentMeta: If set, construct and register FragmentSettings objects. Provides metadata necessary for doing so.
// Otherwise, completely skip over that extra work and apply parsed settings to the user settings, if allowed by disabledProfileSources ("_ignoredNamespaces").
void SettingsLoader::_parseFragment(const winrt::hstring& source, const std::string_view& content, ParsedSettings& settings, const std::optional<ParseFragmentMetadata>& fragmentMeta)
{
auto json = _parseJson(content);
const bool buildFragmentSettings = fragmentMeta.has_value();
const bool applyToUserSettings = !buildFragmentSettings && !_ignoredNamespaces.contains(std::wstring_view{ source });
winrt::com_ptr<implementation::FragmentSettings> fragmentSettings = buildFragmentSettings ?
winrt::make_self<FragmentSettings>(source, hstring{ til::u8u16(Json::writeString(_getJsonStyledWriter(), json.root)) }, hstring{ fragmentMeta->jsonFilename }) :
nullptr;
settings.clear();
// Load GlobalAppSettings and ColorSchemes
{
settings.globals = winrt::make_self<GlobalAppSettings>();
std::vector<Model::FragmentColorSchemeEntry> fragmentColorSchemes;
for (const auto& schemeJson : json.colorSchemes)
{
try
@ -741,72 +854,111 @@ void SettingsLoader::_parseFragment(const winrt::hstring& source, const std::str
if (const auto scheme = ColorScheme::FromJson(schemeJson))
{
scheme->Origin(OriginTag::Fragment);
// Don't add the color scheme to the Fragment's GlobalSettings; that will
// cause layering issues later. Add them to a staging area for later processing.
// (search for STAGED COLORS to find the next step)
settings.colorSchemes.emplace(scheme->Name(), std::move(scheme));
if (buildFragmentSettings)
{
fragmentColorSchemes.emplace_back(winrt::make<FragmentColorSchemeEntry>(scheme->Name(), hstring{ til::u8u16(Json::writeString(_getJsonStyledWriter(), schemeJson)) }));
}
if (applyToUserSettings)
{
// Don't add the color scheme to the Fragment's GlobalSettings; that will
// cause layering issues later. Add them to a staging area for later processing.
// (search for STAGED COLORS to find the next step)
settings.colorSchemes.emplace(scheme->Name(), std::move(scheme));
}
}
}
CATCH_LOG()
}
// Parse out actions from the fragment. Manually opt-out of keybinding
// parsing - fragments shouldn't be allowed to bind actions to keys
// directly. We may want to revisit circa GH#2205
settings.globals->LayerActionsFrom(json.root, OriginTag::Fragment, false);
if (buildFragmentSettings)
{
fragmentSettings->ColorSchemes(fragmentColorSchemes.empty() ? nullptr : single_threaded_vector<Model::FragmentColorSchemeEntry>(std::move(fragmentColorSchemes)));
}
if (applyToUserSettings)
{
// Parse out actions from the fragment. Manually opt-out of keybinding
// parsing - fragments shouldn't be allowed to bind actions to keys
// directly. We may want to revisit circa GH#2205
settings.globals = winrt::make_self<GlobalAppSettings>();
settings.globals->LayerActionsFrom(json.root, OriginTag::Fragment, false);
}
}
// Load new and modified profiles
{
const auto size = json.profilesList.size();
settings.profiles.reserve(size);
settings.profilesByGuid.reserve(size);
if (applyToUserSettings)
{
const auto size = json.profilesList.size();
settings.profiles.reserve(size);
settings.profilesByGuid.reserve(size);
}
std::vector<Model::FragmentProfileEntry> newProfiles;
std::vector<Model::FragmentProfileEntry> modifiedProfiles;
for (const auto& profileJson : json.profilesList)
{
try
{
auto profile = _parseProfile(OriginTag::Fragment, source, profileJson);
// GH#9962: Discard Guid-less, Name-less profiles, but...
// allow ones with an Updates field, as those are special for fragments.
// We need to make sure to only call Guid() if HasGuid() is true,
// as Guid() will dynamically generate a return value otherwise.
auto profile = _parseProfile(OriginTag::Fragment, source, profileJson);
const auto guid = profile->HasGuid() ? profile->Guid() : profile->Updates();
auto destinationSet = profile->HasGuid() ? &newProfiles : &modifiedProfiles;
if (guid != winrt::guid{})
{
_appendProfile(std::move(profile), guid, settings);
if (buildFragmentSettings)
{
destinationSet->emplace_back(winrt::make<FragmentProfileEntry>(guid, hstring{ til::u8u16(Json::writeString(_getJsonStyledWriter(), profileJson)) }));
}
if (applyToUserSettings)
{
_appendProfile(std::move(profile), guid, settings);
}
}
}
CATCH_LOG()
}
if (buildFragmentSettings)
{
fragmentSettings->NewProfiles(newProfiles.empty() ? nullptr : single_threaded_vector<Model::FragmentProfileEntry>(std::move(newProfiles)));
fragmentSettings->ModifiedProfiles(modifiedProfiles.empty() ? nullptr : single_threaded_vector<Model::FragmentProfileEntry>(std::move(modifiedProfiles)));
_registerFragment(std::move(*fragmentSettings), fragmentMeta->scope);
}
}
for (const auto& fragmentProfile : settings.profiles)
// Merge profiles, color schemes, and globals into the user settings (aka inheritance)
if (applyToUserSettings)
{
if (const auto updates = fragmentProfile->Updates(); updates != winrt::guid{})
for (const auto& fragmentProfile : settings.profiles)
{
if (const auto it = userSettings.profilesByGuid.find(updates); it != userSettings.profilesByGuid.end())
if (const auto updates = fragmentProfile->Updates(); updates != winrt::guid{})
{
it->second->AddMostImportantParent(fragmentProfile);
if (const auto it = userSettings.profilesByGuid.find(updates); it != userSettings.profilesByGuid.end())
{
it->second->AddMostImportantParent(fragmentProfile);
}
}
else
{
_addUserProfileParent(fragmentProfile);
}
}
else
// STAGED COLORS are processed here: we merge them into the partially-loaded
// settings directly so that we can resolve conflicts between user-generated
// color schemes and fragment-originated ones.
for (const auto& [_, fragmentColorScheme] : settings.colorSchemes)
{
_addUserProfileParent(fragmentProfile);
_addOrMergeUserColorScheme(fragmentColorScheme);
}
}
// STAGED COLORS are processed here: we merge them into the partially-loaded
// settings directly so that we can resolve conflicts between user-generated
// color schemes and fragment-originated ones.
for (const auto& fragmentColorScheme : settings.colorSchemes)
{
_addOrMergeUserColorScheme(fragmentColorScheme.second);
// Add the parsed fragment globals as a parent of the user's settings.
// Later, in FinalizeInheritance, this will result in the action map from
// the fragments being applied before the user's own settings.
userSettings.globals->AddLeastImportantParent(settings.globals);
}
// Add the parsed fragment globals as a parent of the user's settings.
// Later, in FinalizeInheritance, this will result in the action map from
// the fragments being applied before the user's own settings.
userSettings.globals->AddLeastImportantParent(settings.globals);
}
SettingsLoader::JsonSettings SettingsLoader::_parseJson(const std::string_view& content)
@ -906,7 +1058,8 @@ void SettingsLoader::_addUserProfileParent(const winrt::com_ptr<implementation::
}
}
void SettingsLoader::_addOrMergeUserColorScheme(const winrt::com_ptr<implementation::ColorScheme>& newScheme)
// returns whether the scheme was successfully added
bool SettingsLoader::_addOrMergeUserColorScheme(const winrt::com_ptr<implementation::ColorScheme>& newScheme)
{
// On entry, all the user color schemes have been loaded. Therefore, any insertions of inbox or fragment schemes
// will fail; we can leverage this to detect when they are equivalent and delete the user's duplicate copies.
@ -932,36 +1085,33 @@ void SettingsLoader::_addOrMergeUserColorScheme(const winrt::com_ptr<implementat
userSettings.colorSchemeRemappings.emplace(newScheme->Name(), newName);
// And re-add it to the end.
userSettings.colorSchemes.emplace(newName, std::move(existingScheme));
return true;
}
}
return false;
}
return true;
}
// As the name implies it executes a generator.
// Generated profiles are added to .inboxSettings. Used by GenerateProfiles().
void SettingsLoader::_executeGenerator(const IDynamicProfileGenerator& generator)
void SettingsLoader::_executeGenerator(const IDynamicProfileGenerator& generator, std::vector<winrt::com_ptr<implementation::Profile>>& profilesList)
{
const auto generatorNamespace = generator.GetNamespace();
if (_ignoredNamespaces.contains(generatorNamespace))
{
return;
}
const auto previousSize = inboxSettings.profiles.size();
const auto previousSize = profilesList.size();
try
{
generator.GenerateProfiles(inboxSettings.profiles);
generator.GenerateProfiles(profilesList);
}
CATCH_LOG_MSG("Dynamic Profile Namespace: \"%.*s\"", gsl::narrow<int>(generatorNamespace.size()), generatorNamespace.data())
// If the generator produced some profiles we're going to give them default attributes.
// By setting the Origin/Source/etc. here, we deduplicate some code and ensure they aren't missing accidentally.
if (inboxSettings.profiles.size() > previousSize)
if (profilesList.size() > previousSize)
{
const winrt::hstring source{ generatorNamespace };
for (const auto& profile : std::span(inboxSettings.profiles).subspan(previousSize))
for (const auto& profile : std::span(profilesList).subspan(previousSize))
{
profile->Origin(OriginTag::Generated);
profile->Source(source);
@ -969,6 +1119,26 @@ void SettingsLoader::_executeGenerator(const IDynamicProfileGenerator& generator
}
}
winrt::com_ptr<ExtensionPackage> SettingsLoader::_registerFragment(const winrt::Microsoft::Terminal::Settings::Model::FragmentSettings& fragment, FragmentScope scope)
{
winrt::com_ptr<ExtensionPackage> extPkg{ nullptr };
const auto src = fragment.Source();
const auto found = extensionPackageMap.find(src);
if (found != extensionPackageMap.end())
{
// retrieve from extensionPackageMap
extPkg = found->second;
}
else
{
// create a new entry in extensionPackageMap
const auto em = extensionPackageMap.emplace(src, winrt::make_self<ExtensionPackage>(src, scope));
extPkg = em.first->second;
}
extPkg->Fragments().Append(fragment);
return extPkg;
}
// Method Description:
// - Creates a CascadiaSettings from whatever's saved on disk, or instantiates
// a new one with the default values. If we're running as a packaged app,
@ -1041,7 +1211,7 @@ try
loader.MergeInboxIntoUserSettings();
// Fragments might reference user profiles created by a generator.
// --> FindFragmentsAndMergeIntoUserSettings must be called after MergeInboxIntoUserSettings.
loader.FindFragmentsAndMergeIntoUserSettings();
loader.FindFragmentsAndMergeIntoUserSettings(false /*generateExtensionPackages*/);
loader.FinalizeLayering();
// DisableDeletedProfiles returns true whenever we encountered any new generated/dynamic profiles.

View File

@ -92,6 +92,14 @@ winrt::com_ptr<GlobalAppSettings> GlobalAppSettings::Copy() const
globals->_NewTabMenu->Append(get_self<NewTabMenuEntry>(entry)->Copy());
}
}
if (_DisabledProfileSources)
{
globals->_DisabledProfileSources = winrt::single_threaded_vector<hstring>();
for (const auto& src : *_DisabledProfileSources)
{
globals->_DisabledProfileSources->Append(src);
}
}
for (const auto& parent : _parents)
{

View File

@ -30,6 +30,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model
public:
virtual ~IDynamicProfileGenerator() = default;
virtual std::wstring_view GetNamespace() const noexcept = 0;
virtual std::wstring_view GetDisplayName() const noexcept = 0;
virtual std::wstring_view GetIcon() const noexcept = 0;
virtual void GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles) const = 0;
};
};

View File

@ -15,12 +15,14 @@
#include <winrt/Windows.Management.Deployment.h>
#include <appmodel.h>
#include <shlobj.h>
#include <LibraryResources.h>
static constexpr std::wstring_view POWERSHELL_PFN{ L"Microsoft.PowerShell_8wekyb3d8bbwe" };
static constexpr std::wstring_view POWERSHELL_PREVIEW_PFN{ L"Microsoft.PowerShellPreview_8wekyb3d8bbwe" };
static constexpr std::wstring_view PWSH_EXE{ L"pwsh.exe" };
static constexpr std::wstring_view POWERSHELL_ICON{ L"ms-appx:///ProfileIcons/pwsh.png" };
static constexpr std::wstring_view POWERSHELL_PREVIEW_ICON{ L"ms-appx:///ProfileIcons/pwsh-preview.png" };
static constexpr std::wstring_view GENERATOR_POWERSHELL_ICON{ L"ms-appx:///ProfileGeneratorIcons/PowerShell.png" };
static constexpr std::wstring_view POWERSHELL_PREFERRED_PROFILE_NAME{ L"PowerShell" };
namespace
@ -294,6 +296,16 @@ std::wstring_view PowershellCoreProfileGenerator::GetNamespace() const noexcept
return PowershellCoreGeneratorNamespace;
}
std::wstring_view PowershellCoreProfileGenerator::GetDisplayName() const noexcept
{
return RS_(L"PowershellCoreProfileGeneratorDisplayName");
}
std::wstring_view PowershellCoreProfileGenerator::GetIcon() const noexcept
{
return GENERATOR_POWERSHELL_ICON;
}
// Method Description:
// - Checks if pwsh is installed, and if it is, creates a profile to launch it.
// Arguments:

View File

@ -26,6 +26,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model
static const std::wstring_view GetPreferredPowershellProfileName();
std::wstring_view GetNamespace() const noexcept override;
std::wstring_view GetDisplayName() const noexcept override;
std::wstring_view GetIcon() const noexcept override;
void GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles) const override;
};
};

View File

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
@ -740,4 +740,24 @@
<data name="OpenCWDCommandKey" xml:space="preserve">
<value>Open current working directory</value>
</data>
</root>
<data name="WslDistroGeneratorDisplayName" xml:space="preserve">
<value>WSL Distribution Profile Generator</value>
<comment>The display name of a dynamic profile generator for WSL distros</comment>
</data>
<data name="PowershellCoreProfileGeneratorDisplayName" xml:space="preserve">
<value>PowerShell Profile Generator</value>
<comment>The display name of a dynamic profile generator for PowerShell</comment>
</data>
<data name="AzureCloudShellGeneratorDisplayName" xml:space="preserve">
<value>Azure Cloud Shell Profile Generator</value>
<comment>The display name of a dynamic profile generator for Azure Cloud Shell</comment>
</data>
<data name="VisualStudioGeneratorDisplayName" xml:space="preserve">
<value>Visual Studio Profile Generator</value>
<comment>The display name of a dynamic profile generator for Visual Studio</comment>
</data>
<data name="SshHostGeneratorDisplayName" xml:space="preserve">
<value>SSH Host Profile Generator</value>
<comment>The display name of a dynamic profile generator for SSH hosts</comment>
</data>
</root>

View File

@ -7,11 +7,13 @@
#include "../../inc/DefaultSettings.h"
#include "DynamicProfileUtils.h"
#include <LibraryResources.h>
static constexpr std::wstring_view SshHostGeneratorNamespace{ L"Windows.Terminal.SSH" };
static constexpr std::wstring_view PROFILE_TITLE_PREFIX = L"SSH - ";
static constexpr std::wstring_view PROFILE_ICON_PATH = L"ms-appx:///ProfileIcons/{550ce7b8-d500-50ad-8a1a-c400c3262db3}.png";
static constexpr std::wstring_view GENERATOR_ICON_PATH = L"ms-appx:///ProfileGeneratorIcons/SSH.png";
// OpenSSH is installed under System32 when installed via Optional Features
static constexpr std::wstring_view SSH_EXE_PATH1 = L"%SystemRoot%\\System32\\OpenSSH\\ssh.exe";
@ -132,6 +134,16 @@ std::wstring_view SshHostGenerator::GetNamespace() const noexcept
return SshHostGeneratorNamespace;
}
std::wstring_view SshHostGenerator::GetDisplayName() const noexcept
{
return RS_(L"SshHostGeneratorDisplayName");
}
std::wstring_view SshHostGenerator::GetIcon() const noexcept
{
return GENERATOR_ICON_PATH;
}
// Method Description:
// - Generate a list of profiles for each detected OpenSSH host.
// Arguments:

View File

@ -24,6 +24,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model
{
public:
std::wstring_view GetNamespace() const noexcept override;
std::wstring_view GetDisplayName() const noexcept override;
std::wstring_view GetIcon() const noexcept override;
void GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles) const override;
private:

View File

@ -6,16 +6,28 @@
#include "VisualStudioGenerator.h"
#include "VsDevCmdGenerator.h"
#include "VsDevShellGenerator.h"
#include <LibraryResources.h>
using namespace winrt::Microsoft::Terminal::Settings::Model;
std::wstring_view VisualStudioGenerator::Namespace{ L"Windows.Terminal.VisualStudio" };
static constexpr std::wstring_view IconPath{ L"ms-appx:///ProfileGeneratorIcons/VisualStudio.png" };
std::wstring_view VisualStudioGenerator::GetNamespace() const noexcept
{
return Namespace;
}
std::wstring_view VisualStudioGenerator::GetDisplayName() const noexcept
{
return RS_(L"VisualStudioGeneratorDisplayName");
}
std::wstring_view VisualStudioGenerator::GetIcon() const noexcept
{
return IconPath;
}
void VisualStudioGenerator::GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles) const
{
const auto instances = VsSetupConfiguration::QueryInstances();

View File

@ -28,6 +28,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model
public:
static std::wstring_view Namespace;
std::wstring_view GetNamespace() const noexcept override;
std::wstring_view GetDisplayName() const noexcept override;
std::wstring_view GetIcon() const noexcept override;
void GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles) const override;
class IVisualStudioProfileGenerator

View File

@ -8,10 +8,13 @@
#include "../../inc/DefaultSettings.h"
#include "DynamicProfileUtils.h"
#include <LibraryResources.h>
static constexpr std::wstring_view WslHomeDirectory{ L"~" };
static constexpr std::wstring_view DockerDistributionPrefix{ L"docker-desktop" };
static constexpr std::wstring_view RancherDistributionPrefix{ L"rancher-desktop" };
static constexpr std::wstring_view IconPath{ L"ms-appx:///ProfileIcons/{9acb9455-ca41-5af7-950f-6bca1bc9722f}.png" };
static constexpr std::wstring_view GeneratorIconPath{ L"ms-appx:///ProfileGeneratorIcons/WSL.png" };
// The WSL entries are structured as such:
// HKCU\Software\Microsoft\Windows\CurrentVersion\Lxss
@ -47,6 +50,16 @@ std::wstring_view WslDistroGenerator::GetNamespace() const noexcept
return WslGeneratorNamespace;
}
std::wstring_view WslDistroGenerator::GetDisplayName() const noexcept
{
return RS_(L"WslDistroGeneratorDisplayName");
}
std::wstring_view WslDistroGenerator::GetIcon() const noexcept
{
return GeneratorIconPath;
}
static winrt::com_ptr<implementation::Profile> makeProfile(const std::wstring& distName)
{
const auto WSLDistro{ CreateDynamicProfile(distName) };
@ -65,7 +78,7 @@ static winrt::com_ptr<implementation::Profile> makeProfile(const std::wstring& d
{
WSLDistro->StartingDirectory(winrt::hstring{ DEFAULT_STARTING_DIRECTORY });
}
WSLDistro->Icon(L"ms-appx:///ProfileIcons/{9acb9455-ca41-5af7-950f-6bca1bc9722f}.png");
WSLDistro->Icon(winrt::hstring{ IconPath });
WSLDistro->PathTranslationStyle(winrt::Microsoft::Terminal::Control::PathTranslationStyle::WSL);
return WSLDistro;
}

View File

@ -24,6 +24,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model
{
public:
std::wstring_view GetNamespace() const noexcept override;
std::wstring_view GetDisplayName() const noexcept override;
std::wstring_view GetIcon() const noexcept override;
void GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles) const override;
};
};