mirror of
https://github.com/microsoft/terminal.git
synced 2025-12-10 00:48:23 -06:00
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:
parent
3acb3d510b
commit
e332c67f51
Binary file not shown.
|
After Width: | Height: | Size: 943 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src/cascadia/CascadiaPackage/ProfileGeneratorIcons/SSH.png
Normal file
BIN
src/cascadia/CascadiaPackage/ProfileGeneratorIcons/SSH.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 787 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
BIN
src/cascadia/CascadiaPackage/ProfileGeneratorIcons/WSL.png
Normal file
BIN
src/cascadia/CascadiaPackage/ProfileGeneratorIcons/WSL.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
@ -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>
|
||||
|
||||
@ -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}" />
|
||||
|
||||
@ -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>
|
||||
|
||||
509
src/cascadia/TerminalSettingsEditor/Extensions.cpp
Normal file
509
src/cascadia/TerminalSettingsEditor/Extensions.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
193
src/cascadia/TerminalSettingsEditor/Extensions.h
Normal file
193
src/cascadia/TerminalSettingsEditor/Extensions.h
Normal 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);
|
||||
}
|
||||
81
src/cascadia/TerminalSettingsEditor/Extensions.idl
Normal file
81
src/cascadia/TerminalSettingsEditor/Extensions.idl
Normal 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;
|
||||
}
|
||||
}
|
||||
496
src/cascadia/TerminalSettingsEditor/Extensions.xaml
Normal file
496
src/cascadia/TerminalSettingsEditor/Extensions.xaml
Normal 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="" />
|
||||
<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=""
|
||||
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=""
|
||||
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>
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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; };
|
||||
}
|
||||
|
||||
@ -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="" />
|
||||
@ -155,6 +156,17 @@
|
||||
</muxc:NavigationViewItem.Icon>
|
||||
</muxc:NavigationViewItem>
|
||||
|
||||
<muxc:NavigationViewItem x:Uid="Nav_Extensions"
|
||||
Tag="Extensions_Nav">
|
||||
<muxc:NavigationViewItem.Icon>
|
||||
<FontIcon Glyph="" />
|
||||
</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"
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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).
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 -->
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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=""
|
||||
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=""
|
||||
|
||||
@ -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 \
|
||||
|
||||
@ -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, ...) \
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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;
|
||||
};
|
||||
};
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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; };
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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)
|
||||
{
|
||||
|
||||
@ -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;
|
||||
};
|
||||
};
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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;
|
||||
};
|
||||
};
|
||||
|
||||
@ -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>
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
};
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user