Compare commits

...

19 Commits

Author SHA1 Message Date
Carlos Zamora
87946e9f01 simplify AppState code; display 'NEW' 2025-04-22 16:30:45 -07:00
Carlos Zamora
6959e7ea28 Add Badge to highlight new Extensions Page 2025-04-22 15:19:38 -07:00
Carlos Zamora
f25e6fe2d1 [fix rebase] remove deep clone of ext pkgs 2025-04-22 15:12:33 -07:00
Carlos Zamora
e9f83fc4eb Explicitly introduce and use profile generator icons; add VS logo 2025-04-22 14:58:40 -07:00
Carlos Zamora
c371c484a3 use high resolution assets 2025-04-22 14:58:40 -07:00
Carlos Zamora
1072d69fb7 a11y check: screen reader, keyboard navigation, FastPass 2025-04-22 14:58:40 -07:00
Carlos Zamora
b472d9fed9 add DisplayName and Icon to profile generators; improve identifier system 2025-04-22 14:58:37 -07:00
Carlos Zamora
7fcdeaac40 Add display name and icon to SUI extensions page 2025-04-22 14:57:41 -07:00
Carlos Zamora
2766f21d31 JsonSource->Filename; remove deep copy of fragments & generators 2025-04-22 14:51:12 -07:00
Carlos Zamora
0af4eb0e21 Fix XAML crash for release builds (applied incompatible styling) 2025-04-21 14:32:08 -07:00
Carlos Zamora
b0a7ef1d39 apply feedback from Dustin 2025-04-21 14:22:19 -07:00
Carlos Zamora
7cdbb7c795 address feedback 2025-02-27 10:17:11 -08:00
Carlos Zamora
5afc9bc86a add dynamic profile generators 2025-02-27 10:06:09 -08:00
Carlos Zamora
6b83fa705a wil::to_vector 2025-02-27 10:06:09 -08:00
Carlos Zamora
e6c43c0d4c copy over DisabledProfileSources 2025-02-27 10:06:09 -08:00
Carlos Zamora
75d02c29bd add nested page (part 2) + support for multiple json files 2025-02-27 10:06:09 -08:00
Carlos Zamora
f11515e692 add nested page 2025-02-27 10:06:09 -08:00
Carlos Zamora
7f0c9e5374 rename some resources; remove some TODOs 2025-02-27 10:06:07 -08:00
Carlos Zamora
019bb766db Add Extensions page to Settings UI 2025-02-27 10:04:50 -08:00
42 changed files with 1846 additions and 79 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 943 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 787 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

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

View File

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

View File

@@ -1199,7 +1199,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>
@@ -1222,7 +1222,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"
@@ -1266,4 +1267,24 @@
</Setter.Value>
</Setter>
</Style>
<Style x:Key="NewInfoBadge"
TargetType="muxc:InfoBadge">
<Setter Property="Padding" Value="5,1,5,2" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="muxc:InfoBadge">
<Border x:Name="RootGrid"
Padding="{TemplateBinding Padding}"
Background="{TemplateBinding Background}"
CornerRadius="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.InfoBadgeCornerRadius}">
<TextBlock x:Uid="NewInfoBadgeTextBlock"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="10" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,351 @@
// 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->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 }
{
UpdateSettings(settings, colorSchemesPageVM);
PropertyChanged([this](auto&&, const PropertyChangedEventArgs& args) {
const auto viewModelProperty{ args.PropertyName() };
if (viewModelProperty == L"CurrentExtensionPackage")
{
// Update the views to reflect the current extension package, if one is selected.
// Otherwise, show components from all extensions
_profilesModifiedView.Clear();
_profilesAddedView.Clear();
_colorSchemesAddedView.Clear();
// 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())
{
for (const auto& profile : ext.ProfilesModified())
{
_profilesModifiedView.Append(profile);
}
for (const auto& profile : ext.ProfilesAdded())
{
_profilesAddedView.Append(profile);
}
for (const auto& scheme : ext.ColorSchemesAdded())
{
_colorSchemesAddedView.Append(scheme);
}
}
};
if (const auto currentExtensionPackage = CurrentExtensionPackage())
{
addPackageContentsToView(currentExtensionPackage);
}
else
{
for (const auto& extPkg : _extensionPackages)
{
addPackageContentsToView(extPkg);
}
}
_NotifyChanges(L"IsExtensionView", L"CurrentExtensionPackageIdentifierTemplate");
}
});
}
void ExtensionsViewModel::UpdateSettings(const Model::CascadiaSettings& settings, const Editor::ColorSchemesPageViewModel& colorSchemesPageVM)
{
_settings = settings;
_colorSchemesPageVM = colorSchemesPageVM;
_CurrentExtensionPackage = nullptr;
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);
extensionPackages.push_back(*extPkgVM);
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);
}
}
}
}
}
extPkgVM->FragmentExtensions().Append(winrt::make<FragmentExtensionViewModel>(fragExt, currentProfilesModified, currentProfilesAdded, currentColorSchemesAdded));
}
}
_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));
}
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);
}
}
}
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");
}
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;
}
// Returns the accessible name for the extension package with the disabled state (if disabled) in the following format:
// "<DisplayName?>, <Source>: <Disabled?>"
hstring ExtensionPackageViewModel::AccessibleNameWithStatus() const noexcept
{
if (Enabled())
{
return AccessibleName();
}
return hstring{ fmt::format(FMT_COMPILE(L"{}: {}"), AccessibleName(), RS_(L"Extension_StateDisabled/Text")) };
}
hstring FragmentProfileViewModel::AccessibleName() const noexcept
{
return hstring{ fmt::format(FMT_COMPILE(L"{}, {}"), Profile().Name(), SourceName()) };
}
hstring FragmentColorSchemeViewModel::AccessibleName() const noexcept
{
return hstring{ fmt::format(FMT_COMPILE(L"{}, {}"), ColorSchemeVM().Name(), SourceName()) };
}
DataTemplate ExtensionPackageTemplateSelector::SelectTemplateCore(const IInspectable& item, const DependencyObject& /*container*/)
{
return SelectTemplateCore(item);
}
DataTemplate ExtensionPackageTemplateSelector::SelectTemplateCore(const IInspectable& item)
{
if (const auto extPkgVM = item.try_as<Editor::ExtensionPackageViewModel>())
{
if (!extPkgVM.Package().DisplayName().empty())
{
return ComplexTemplate();
}
return DefaultTemplate();
}
return nullptr;
}
}

View File

@@ -0,0 +1,180 @@
// 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:
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 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;
};
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>() } {}
Model::ExtensionPackage Package() const noexcept { return _package; }
hstring Scope() const noexcept;
bool Enabled() const;
void Enabled(bool val);
hstring AccessibleName() const noexcept;
hstring AccessibleNameWithStatus() 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 } {}
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 } {}
Editor::ColorSchemeViewModel ColorSchemeVM() const { return _deducedSchemeVM; };
hstring SourceName() const { return _fragment.Source(); }
hstring Json() const { return _entry.Json(); }
hstring AccessibleName() const noexcept;
private:
Model::FragmentColorSchemeEntry _entry;
Model::FragmentSettings _fragment;
Editor::ColorSchemeViewModel _deducedSchemeVM;
};
struct ExtensionPackageTemplateSelector : public ExtensionPackageTemplateSelectorT<ExtensionPackageTemplateSelector>
{
public:
ExtensionPackageTemplateSelector() = default;
Windows::UI::Xaml::DataTemplate SelectTemplateCore(const Windows::Foundation::IInspectable& item, const Windows::UI::Xaml::DependencyObject& container);
Windows::UI::Xaml::DataTemplate SelectTemplateCore(const Windows::Foundation::IInspectable& item);
WINRT_PROPERTY(Windows::UI::Xaml::DataTemplate, DefaultTemplate, nullptr);
WINRT_PROPERTY(Windows::UI::Xaml::DataTemplate, ComplexTemplate, nullptr);
};
};
namespace winrt::Microsoft::Terminal::Settings::Editor::factory_implementation
{
BASIC_FACTORY(Extensions);
BASIC_FACTORY(ExtensionPackageTemplateSelector);
}

View File

@@ -0,0 +1,80 @@
// 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; };
}
[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; };
String AccessibleNameWithStatus { get; };
IVector<FragmentExtensionViewModel> FragmentExtensions { get; };
}
[default_interface] runtimeclass FragmentExtensionViewModel : Windows.UI.Xaml.Data.INotifyPropertyChanged
{
Microsoft.Terminal.Settings.Model.FragmentSettings Fragment { get; };
IVectorView<FragmentProfileViewModel> ProfilesModified { get; };
IVectorView<FragmentProfileViewModel> ProfilesAdded { get; };
IVectorView<FragmentColorSchemeViewModel> ColorSchemesAdded { get; };
}
[default_interface] runtimeclass FragmentProfileViewModel : Windows.UI.Xaml.Data.INotifyPropertyChanged
{
Microsoft.Terminal.Settings.Model.Profile Profile { get; };
String SourceName { get; };
String Json { get; };
String AccessibleName { get; };
}
[default_interface] runtimeclass FragmentColorSchemeViewModel : Windows.UI.Xaml.Data.INotifyPropertyChanged
{
ColorSchemeViewModel ColorSchemeVM { get; };
String SourceName { get; };
String Json { get; };
String AccessibleName { get; };
}
[default_interface] runtimeclass ExtensionPackageTemplateSelector : Windows.UI.Xaml.Controls.DataTemplateSelector
{
ExtensionPackageTemplateSelector();
Windows.UI.Xaml.DataTemplate DefaultTemplate;
Windows.UI.Xaml.DataTemplate ComplexTemplate;
}
}

View File

@@ -0,0 +1,493 @@
<!--
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">
<TextBlock Text="{x:Bind Package.Source}" />
</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}" />
<TextBlock x:Uid="Extension_StateDisabled"
Grid.Column="1"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Style="{StaticResource SecondaryTextBlockStyle}"
Visibility="{x:Bind mtu:Converters.InvertedBooleanToVisibility(Enabled)}" />
</Grid>
</Button>
</DataTemplate>
<DataTemplate x:Key="ComplexExtensionNavigatorTemplate"
x:DataType="local:ExtensionPackageViewModel">
<Button AutomationProperties.Name="{x:Bind AccessibleNameWithStatus}"
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}" />
<TextBlock x:Uid="Extension_StateDisabled"
Grid.Column="1"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Style="{StaticResource SecondaryTextBlockStyle}"
Visibility="{x:Bind mtu:Converters.InvertedBooleanToVisibility(Enabled)}" />
</Grid>
</Button>
</DataTemplate>
<DataTemplate x:Key="FragmentProfileViewModelTemplate"
x:DataType="local:FragmentProfileViewModel">
<muxc:Expander AutomationProperties.Name="{x:Bind AccessibleName}"
Style="{StaticResource ExpanderStyle}">
<muxc:Expander.Header>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0"
Orientation="Horizontal">
<IconSourceElement Width="16"
Height="16"
Margin="0,0,8,0"
IconSource="{x:Bind mtu:IconPathConverter.IconSourceWUX(Profile.EvaluatedIcon), Mode=OneWay}" />
<TextBlock Text="{x:Bind Profile.Name, Mode=OneWay}" />
<Button x:Name="NavigateToProfileButton"
x:Uid="Extensions_NavigateToProfileButton"
Click="NavigateToProfile_Click"
Style="{StaticResource SettingContainerResetButtonStyle}"
Tag="{x:Bind Profile.Guid}">
<FontIcon Glyph="&#xE8A7;"
Style="{StaticResource SettingContainerFontIconStyle}" />
</Button>
</StackPanel>
<TextBlock Grid.Column="1"
Style="{StaticResource SettingContainerCurrentValueTextBlockStyle}"
Text="{x:Bind SourceName}" />
</Grid>
</muxc:Expander.Header>
<muxc:Expander.Content>
<ScrollViewer Style="{StaticResource CodeBlockScrollViewerStyle}">
<TextBlock Style="{StaticResource CodeBlockStyle}"
Text="{x:Bind Json, Mode=OneWay}" />
</ScrollViewer>
</muxc:Expander.Content>
</muxc:Expander>
</DataTemplate>
<!-- This styling matches that of ExpanderSettingContainerStyle for consistency -->
<Style x:Key="ExpanderStyle"
TargetType="muxc:Expander">
<Setter Property="MaxWidth" Value="1000" />
<Setter Property="MinHeight" Value="64" />
<Setter Property="Margin" Value="0,4,0,0" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
</Style>
<DataTemplate x:Key="JsonTemplate"
x:DataType="local:FragmentExtensionViewModel">
<muxc:Expander Header="{x:Bind Fragment.Filename}"
Style="{StaticResource ExpanderStyle}">
<ScrollViewer Style="{StaticResource CodeBlockScrollViewerStyle}">
<TextBlock Style="{StaticResource CodeBlockStyle}"
Text="{x:Bind Fragment.Json}" />
</ScrollViewer>
</muxc:Expander>
</DataTemplate>
<!--
Copied over from Appearances.xaml. We're unable to add the DataTemplate to CommonResources.xaml
because it needs a code-behind class to use {x:Bind}
-->
<DataTemplate x:Key="ColorChipTemplate"
x:DataType="local:ColorTableEntry">
<Border Width="8"
Height="8"
Background="{x:Bind mtu:Converters.ColorToBrush(Color)}"
CornerRadius="1" />
</DataTemplate>
<!--
Copied over from Appearances.xaml. We're unable to add the DataTemplate to CommonResources.xaml
because it needs a code-behind class to use {x:Bind}
-->
<DataTemplate x:Key="ColorSchemeVMTemplate"
x:DataType="local:ColorSchemeViewModel">
<StackPanel Orientation="Horizontal">
<Grid Grid.Column="0"
Padding="8"
VerticalAlignment="Center"
Background="{x:Bind mtu:Converters.ColorToBrush(BackgroundColor.Color), Mode=OneWay}"
ColumnSpacing="1"
CornerRadius="2"
RowSpacing="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ContentControl Grid.Row="0"
Grid.Column="0"
Content="{x:Bind ColorEntryAt(0), Mode=OneWay}"
ContentTemplate="{StaticResource ColorChipTemplate}"
IsTabStop="False" />
<ContentControl Grid.Row="0"
Grid.Column="1"
Content="{x:Bind ColorEntryAt(1), Mode=OneWay}"
ContentTemplate="{StaticResource ColorChipTemplate}"
IsTabStop="False" />
<ContentControl Grid.Row="0"
Grid.Column="2"
Content="{x:Bind ColorEntryAt(2), Mode=OneWay}"
ContentTemplate="{StaticResource ColorChipTemplate}"
IsTabStop="False" />
<ContentControl Grid.Row="0"
Grid.Column="3"
Content="{x:Bind ColorEntryAt(3), Mode=OneWay}"
ContentTemplate="{StaticResource ColorChipTemplate}"
IsTabStop="False" />
<ContentControl Grid.Row="0"
Grid.Column="4"
Content="{x:Bind ColorEntryAt(4), Mode=OneWay}"
ContentTemplate="{StaticResource ColorChipTemplate}"
IsTabStop="False" />
<ContentControl Grid.Row="0"
Grid.Column="5"
Content="{x:Bind ColorEntryAt(5), Mode=OneWay}"
ContentTemplate="{StaticResource ColorChipTemplate}"
IsTabStop="False" />
<ContentControl Grid.Row="0"
Grid.Column="6"
Content="{x:Bind ColorEntryAt(6), Mode=OneWay}"
ContentTemplate="{StaticResource ColorChipTemplate}"
IsTabStop="False" />
<ContentControl Grid.Row="0"
Grid.Column="7"
Content="{x:Bind ColorEntryAt(7), Mode=OneWay}"
ContentTemplate="{StaticResource ColorChipTemplate}"
IsTabStop="False" />
<ContentControl Grid.Row="1"
Grid.Column="0"
Content="{x:Bind ColorEntryAt(8), Mode=OneWay}"
ContentTemplate="{StaticResource ColorChipTemplate}"
IsTabStop="False" />
<ContentControl Grid.Row="1"
Grid.Column="1"
Content="{x:Bind ColorEntryAt(9), Mode=OneWay}"
ContentTemplate="{StaticResource ColorChipTemplate}"
IsTabStop="False" />
<ContentControl Grid.Row="1"
Grid.Column="2"
Content="{x:Bind ColorEntryAt(10), Mode=OneWay}"
ContentTemplate="{StaticResource ColorChipTemplate}"
IsTabStop="False" />
<ContentControl Grid.Row="1"
Grid.Column="3"
Content="{x:Bind ColorEntryAt(11), Mode=OneWay}"
ContentTemplate="{StaticResource ColorChipTemplate}"
IsTabStop="False" />
<ContentControl Grid.Row="1"
Grid.Column="4"
Content="{x:Bind ColorEntryAt(12), Mode=OneWay}"
ContentTemplate="{StaticResource ColorChipTemplate}"
IsTabStop="False" />
<ContentControl Grid.Row="1"
Grid.Column="5"
Content="{x:Bind ColorEntryAt(13), Mode=OneWay}"
ContentTemplate="{StaticResource ColorChipTemplate}"
IsTabStop="False" />
<ContentControl Grid.Row="1"
Grid.Column="6"
Content="{x:Bind ColorEntryAt(14), Mode=OneWay}"
ContentTemplate="{StaticResource ColorChipTemplate}"
IsTabStop="False" />
<ContentControl Grid.Row="1"
Grid.Column="7"
Content="{x:Bind ColorEntryAt(15), Mode=OneWay}"
ContentTemplate="{StaticResource ColorChipTemplate}"
IsTabStop="False" />
<TextBlock Grid.RowSpan="2"
Grid.Column="8"
MaxWidth="192"
Margin="4,0,4,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
AutomationProperties.AccessibilityView="Raw"
FontFamily="Cascadia Code"
Foreground="{x:Bind mtu:Converters.ColorToBrush(ForegroundColor.Color), Mode=OneWay}"
Text="{x:Bind Name, Mode=OneWay}"
TextTrimming="WordEllipsis" />
</Grid>
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="FragmentColorSchemeViewModelTemplate"
x:DataType="local:FragmentColorSchemeViewModel">
<muxc:Expander AutomationProperties.Name="{x:Bind AccessibleName}"
Style="{StaticResource ExpanderStyle}">
<muxc:Expander.Header>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0"
Orientation="Horizontal">
<ContentPresenter Content="{x:Bind ColorSchemeVM, Mode=OneWay}"
ContentTemplate="{StaticResource ColorSchemeVMTemplate}" />
<Button x:Name="NavigateToColorSchemeButton"
x:Uid="Extensions_NavigateToColorSchemeButton"
Click="NavigateToColorScheme_Click"
Style="{StaticResource SettingContainerResetButtonStyle}"
Tag="{x:Bind ColorSchemeVM}">
<FontIcon Glyph="&#xE8A7;"
Style="{StaticResource SettingContainerFontIconStyle}" />
</Button>
</StackPanel>
<TextBlock Grid.Column="1"
Style="{StaticResource SettingContainerCurrentValueTextBlockStyle}"
Text="{x:Bind SourceName}" />
</Grid>
</muxc:Expander.Header>
<muxc:Expander.Content>
<ScrollViewer Style="{StaticResource CodeBlockScrollViewerStyle}">
<TextBlock Style="{StaticResource CodeBlockStyle}"
Text="{x:Bind Json, Mode=OneWay}" />
</ScrollViewer>
</muxc:Expander.Content>
</muxc:Expander>
</DataTemplate>
</ResourceDictionary>
</Page.Resources>
<StackPanel Spacing="20"
Style="{StaticResource SettingsStackStyle}">
<!-- [Root View Only] -->
<!-- Set margin.bottom to -20 because the next stack panel gets the 20 from the root's spacing property -->
<StackPanel MaxWidth="{StaticResource StandardControlMaxWidth}"
Margin="0,0,0,-20"
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}" />
<TextBlock x:Uid="Extensions_NoActiveExtensionsDisclaimer"
Style="{StaticResource ItalicDisclaimerStyle}"
Visibility="{x:Bind ViewModel.NoExtensionPackages, Mode=OneWay}" />
<ItemsControl x:Name="ActiveExtensionsList"
IsTabStop="False"
ItemTemplateSelector="{StaticResource ExtensionPackageNavigatorTemplateSelector}"
ItemsSource="{x:Bind ViewModel.ExtensionPackages}"
XYFocusKeyboardNavigation="Enabled" />
</StackPanel>
<!-- [Extension View Only] -->
<!-- Set margin.top to -20 because the previous stack panel gets the 20 from the root's spacing property -->
<StackPanel MaxWidth="{StaticResource StandardControlMaxWidth}"
Margin="0,-20,0,0"
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>
<TextBlock x:Uid="Extensions_ModifiedProfilesHeader"
Style="{StaticResource TextBlockSubHeaderStyle}" />
<TextBlock x:Uid="Extensions_NoModifiedProfilesDisclaimer"
Style="{StaticResource ItalicDisclaimerStyle}"
Visibility="{x:Bind ViewModel.NoProfilesModified, Mode=OneWay}" />
<ItemsControl x:Name="ModifiedProfilesList"
IsTabStop="False"
ItemTemplate="{StaticResource FragmentProfileViewModelTemplate}"
ItemsSource="{x:Bind ViewModel.ProfilesModified, Mode=OneWay}" />
</StackPanel>
<!-- Grouping: Added Profiles -->
<StackPanel>
<TextBlock x:Uid="Extensions_AddedProfilesHeader"
Style="{StaticResource TextBlockSubHeaderStyle}" />
<TextBlock x:Uid="Extensions_NoAddedProfilesDisclaimer"
Style="{StaticResource ItalicDisclaimerStyle}"
Visibility="{x:Bind ViewModel.NoProfilesAdded, Mode=OneWay}" />
<ItemsControl x:Name="AddedProfilesList"
IsTabStop="False"
ItemTemplate="{StaticResource FragmentProfileViewModelTemplate}"
ItemsSource="{x:Bind ViewModel.ProfilesAdded, Mode=OneWay}" />
</StackPanel>
<!-- Grouping: Added Color Schemes -->
<StackPanel>
<TextBlock x:Uid="Extensions_AddedColorSchemesHeader"
Style="{StaticResource TextBlockSubHeaderStyle}" />
<TextBlock x:Uid="Extensions_NoAddedColorSchemesDisclaimer"
Style="{StaticResource ItalicDisclaimerStyle}"
Visibility="{x:Bind ViewModel.NoSchemesAdded, Mode=OneWay}" />
<ItemsControl x:Name="AddedColorSchemesList"
IsTabStop="False"
ItemTemplate="{StaticResource FragmentColorSchemeViewModelTemplate}"
ItemsSource="{x:Bind ViewModel.ColorSchemesAdded, Mode=OneWay}" />
</StackPanel>
</StackPanel>
</Page>

View File

@@ -9,6 +9,7 @@
#include "Compatibility.h"
#include "Rendering.h"
#include "RenderingViewModel.h"
#include "Extensions.h"
#include "Actions.h"
#include "ProfileViewModel.h"
#include "GlobalAppearance.h"
@@ -44,6 +45,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" };
@@ -111,6 +113,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();
@@ -161,6 +190,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
@@ -182,7 +212,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;
}
}
@@ -197,6 +227,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 NewTabMenu page,
// _Navigate() will handle trying to find the right subpage
SettingsNav().SelectedItem(item);
_Navigate(breadcrumbExtensionPackage, BreadcrumbSubPage::NewTabMenu_Folder);
return;
}
}
}
else if (const auto& profileTag{ tag.try_as<ProfileViewModel>() })
{
@@ -445,6 +486,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()) };
@@ -575,6 +631,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::OpenJsonTapped(const IInspectable& /*sender*/, const Windows::UI::Xaml::Input::TappedRoutedEventArgs& /*args*/)
{
const auto window = CoreWindow::GetForCurrentThread();
@@ -621,6 +711,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);
@@ -818,6 +912,35 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
return _breadcrumbs;
}
void MainPage::_NavigateToProfileHandler(const IInspectable& /*sender*/, winrt::guid profileGuid)
{
for (auto&& menuItem : _menuItemSource)
{
if (const auto& navViewItem{ menuItem.try_as<MUX::Controls::NavigationViewItem>() })
{
if (const auto& tag{ navViewItem.Tag() })
{
if (const auto& profileTag{ tag.try_as<ProfileViewModel>() })
{
if (profileTag->OriginalProfileGuid() == profileGuid)
{
SettingsNav().SelectedItem(menuItem);
_Navigate(*profileTag, BreadcrumbSubPage::None);
return;
}
}
}
}
}
// Silently fail if the profile wasn't found
}
void MainPage::_NavigateToColorSchemeHandler(const IInspectable& /*sender*/, const IInspectable& /*args*/)
{
SettingsNav().SelectedItem(ColorSchemesNavItem());
_Navigate(hstring{ colorSchemesTag }, BreadcrumbSubPage::ColorSchemes_Edit);
}
winrt::Windows::UI::Xaml::Media::Brush MainPage::BackgroundBrush()
{
return SettingsNav().Background();

View File

@@ -45,6 +45,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;
@@ -70,16 +71,21 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
void _Navigate(hstring clickedItemTag, BreadcrumbSubPage subPage);
void _Navigate(const Editor::ProfileViewModel& profile, BreadcrumbSubPage subPage);
void _Navigate(const Editor::NewTabMenuEntryViewModel& ntmEntryVM, BreadcrumbSubPage subPage);
void _Navigate(const Editor::ExtensionPackageViewModel& extPkgVM, BreadcrumbSubPage subPage);
void _NavigateToProfileHandler(const IInspectable& sender, winrt::guid profileGuid);
void _NavigateToColorSchemeHandler(const IInspectable& sender, const IInspectable& args);
void _UpdateBackgroundForMica();
void _MoveXamlParsedNavItemsIntoItemSource();
winrt::Microsoft::Terminal::Settings::Editor::ColorSchemesPageViewModel _colorSchemesPageVM{ nullptr };
winrt::Microsoft::Terminal::Settings::Editor::NewTabMenuViewModel _newTabMenuPageVM{ nullptr };
winrt::Microsoft::Terminal::Settings::Editor::ExtensionsViewModel _extensionsVM{ nullptr };
Windows::UI::Xaml::Data::INotifyPropertyChanged::PropertyChanged_revoker _profileViewModelChangedRevoker;
Windows::UI::Xaml::Data::INotifyPropertyChanged::PropertyChanged_revoker _colorSchemesPageViewModelChangedRevoker;
Windows::UI::Xaml::Data::INotifyPropertyChanged::PropertyChanged_revoker _ntmViewModelChangedRevoker;
Windows::UI::Xaml::Data::INotifyPropertyChanged::PropertyChanged_revoker _extensionsViewModelChangedRevoker;
};
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -684,6 +684,10 @@
<value>Actions</value>
<comment>Header for the "actions" menu item. This navigates to a page that lets you see and modify commands, key bindings, and actions that can be done in the app.</comment>
</data>
<data name="Nav_Extensions.Content" xml:space="preserve">
<value>Extensions</value>
<comment>Header for the "extensions" menu item. This navigates to a page that lets you see and modify extensions for the app.</comment>
</data>
<data name="Profile_OpacitySlider.[using:Windows.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Background opacity</value>
<comment>Name for a control to determine the level of opacity for the background of the control. The user can choose to make the background of the app more or less opaque.</comment>
@@ -2340,4 +2344,67 @@
<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_NoActiveExtensionsDisclaimer.Text" xml:space="preserve">
<value>None</value>
<comment>Text displayed when no extensions are available. Shown in place of a list of entries.</comment>
</data>
<data name="Extensions_NoModifiedProfilesDisclaimer.Text" xml:space="preserve">
<value>None</value>
<comment>Text displayed when no profiles were modified. Shown in place of a list of entries.</comment>
</data>
<data name="Extensions_NoAddedColorSchemesDisclaimer.Text" xml:space="preserve">
<value>None</value>
<comment>Text displayed when no color schemes were added. Shown in place of a list of entries.</comment>
</data>
<data name="Extensions_NoAddedProfilesDisclaimer.Text" xml:space="preserve">
<value>None</value>
<comment>Text displayed when no profiles were added. Shown in place of a list of entries.</comment>
</data>
<data name="Extensions_DisclaimerHyperlink.Content" xml:space="preserve">
<value>Learn more about fragment 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="Extension_StateDisabled.Text" xml:space="preserve">
<value>Disabled</value>
<comment>Text displayed when an extension is disabled</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>
</root>

View File

@@ -179,13 +179,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 +233,45 @@
</Setter>
</Style>
<!-- A basic setting container displaying immutable text as content -->
<Style x:Key="SettingContainerWithTextContent"
TargetType="local:SettingContainer">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:SettingContainer">
<Grid AutomationProperties.Name="{TemplateBinding Header}"
Style="{StaticResource NonExpanderGrid}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0"
Style="{StaticResource StackPanelInExpanderStyle}">
<StackPanel Orientation="Horizontal">
<TextBlock Style="{StaticResource SettingsPageItemHeaderStyle}"
Text="{TemplateBinding Header}" />
<Button x:Name="ResetButton"
Style="{StaticResource SettingContainerResetButtonStyle}">
<FontIcon Glyph="&#xE845;"
Style="{StaticResource SettingContainerFontIconStyle}" />
</Button>
</StackPanel>
<TextBlock x:Name="HelpTextBlock"
Style="{StaticResource SettingsPageItemDescriptionStyle}"
Text="{TemplateBinding HelpText}" />
</StackPanel>
<TextBlock Grid.Column="1"
Margin="0,0,8,0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Style="{ThemeResource SecondaryTextBlockStyle}"
Text="{TemplateBinding Content}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!--
A setting container for a setting that has no additional options.
Includes space for an icon on the left side of the header.
@@ -302,8 +346,7 @@
<StackPanel Grid.Column="0"
Style="{StaticResource StackPanelInExpanderStyle}">
<StackPanel Orientation="Horizontal">
<TextBlock Style="{StaticResource SettingsPageItemHeaderStyle}"
Text="{TemplateBinding Header}" />
<ContentPresenter Content="{TemplateBinding Header}" />
<Button x:Name="ResetButton"
Style="{StaticResource SettingContainerResetButtonStyle}">
<FontIcon Glyph="&#xE845;"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -113,6 +113,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
@@ -173,6 +177,11 @@ IObservableVector<Model::Profile> CascadiaSettings::ActiveProfiles() const noexc
return _activeProfiles;
}
IVectorView<Model::ExtensionPackage> CascadiaSettings::Extensions() const noexcept
{
return _extensionPackages.GetView();
}
// Method Description:
// - Returns the globally configured keybindings
// Arguments:

View File

@@ -18,6 +18,10 @@ Author(s):
#pragma once
#include "CascadiaSettings.g.h"
#include "FragmentSettings.g.h"
#include "FragmentProfileEntry.g.h"
#include "FragmentColorSchemeEntry.g.h"
#include "ExtensionPackage.g.h"
#include "GlobalAppSettings.h"
#include "Profile.h"
@@ -39,6 +43,28 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
std::runtime_error(message) {}
};
struct ExtensionPackage : ExtensionPackageT<ExtensionPackage>
{
public:
ExtensionPackage(hstring source, FragmentScope scope) :
_source{ source },
_scope{ scope },
_fragments{ winrt::single_threaded_vector<Model::FragmentSettings>() } {}
hstring Source() const noexcept { return _source; }
FragmentScope Scope() const noexcept { return _scope; }
Windows::Foundation::Collections::IVectorView<Model::FragmentSettings> FragmentsView() const noexcept { return _fragments.GetView(); }
Windows::Foundation::Collections::IVector<Model::FragmentSettings> Fragments() const noexcept { return _fragments; }
WINRT_PROPERTY(hstring, Icon);
WINRT_PROPERTY(hstring, DisplayName);
private:
hstring _source;
FragmentScope _scope;
Windows::Foundation::Collections::IVector<Model::FragmentSettings> _fragments;
};
struct ParsedSettings
{
winrt::com_ptr<implementation::GlobalAppSettings> globals;
@@ -70,6 +96,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:
@@ -88,13 +115,14 @@ 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, FragmentScope scope, std::wstring_view jsonFilename, bool applyToSettings);
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);
bool _addOrMergeUserColorScheme(const winrt::com_ptr<implementation::ColorScheme>& colorScheme);
void _executeGenerator(const IDynamicProfileGenerator& generator);
winrt::com_ptr<implementation::ExtensionPackage> _registerFragment(const winrt::Microsoft::Terminal::Settings::Model::FragmentSettings& fragment, FragmentScope scope);
std::unordered_set<winrt::hstring, til::transparent_hstring_hash, til::transparent_hstring_equal_to> _ignoredNamespaces;
std::set<std::string> themesChangeLog;
@@ -127,6 +155,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() const noexcept;
void WriteSettingsToDisk();
Json::Value ToJson() const;
Model::Profile ProfileDefaults() const;
@@ -184,6 +213,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 = winrt::single_threaded_vector<Model::ExtensionPackage>();
std::set<std::string> _themesChangeLog{};
// load errors
@@ -199,6 +229,69 @@ 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; }
WINRT_PROPERTY(bool, Conflict, false);
private:
hstring _schemeName;
hstring _json;
};
struct FragmentSettings : FragmentSettingsT<FragmentSettings>
{
public:
FragmentSettings(hstring source, hstring json, hstring filename) :
_source{ source },
_json{ json },
_filename{ filename } {}
hstring Source() const noexcept { return _source; }
hstring Json() const noexcept { return _json; }
hstring Filename() const noexcept { return _filename; }
Windows::Foundation::Collections::IVector<Model::FragmentProfileEntry> ModifiedProfiles() const noexcept { return _modifiedProfiles; }
void ModifiedProfiles(const Windows::Foundation::Collections::IVector<Model::FragmentProfileEntry>& modifiedProfiles) noexcept { _modifiedProfiles = modifiedProfiles; }
Windows::Foundation::Collections::IVector<Model::FragmentProfileEntry> NewProfiles() const noexcept { return _newProfiles; }
void NewProfiles(const Windows::Foundation::Collections::IVector<Model::FragmentProfileEntry>& newProfiles) noexcept { _newProfiles = newProfiles; }
Windows::Foundation::Collections::IVector<Model::FragmentColorSchemeEntry> ColorSchemes() const noexcept { return _colorSchemes; }
void ColorSchemes(const Windows::Foundation::Collections::IVector<Model::FragmentColorSchemeEntry>& colorSchemes) noexcept { _colorSchemes = colorSchemes; }
// views
Windows::Foundation::Collections::IVectorView<Model::FragmentProfileEntry> ModifiedProfilesView() const noexcept { return _modifiedProfiles ? _modifiedProfiles.GetView() : nullptr; }
Windows::Foundation::Collections::IVectorView<Model::FragmentProfileEntry> NewProfilesView() const noexcept { return _newProfiles ? _newProfiles.GetView() : nullptr; }
Windows::Foundation::Collections::IVectorView<Model::FragmentColorSchemeEntry> ColorSchemesView() const noexcept { return _colorSchemes ? _colorSchemes.GetView() : nullptr; }
private:
hstring _source;
hstring _json;
hstring _filename;
Windows::Foundation::Collections::IVector<Model::FragmentProfileEntry> _modifiedProfiles;
Windows::Foundation::Collections::IVector<Model::FragmentProfileEntry> _newProfiles;
Windows::Foundation::Collections::IVector<Model::FragmentColorSchemeEntry> _colorSchemes;
};
}
namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation

View File

@@ -8,6 +8,12 @@ import "DefaultTerminal.idl";
namespace Microsoft.Terminal.Settings.Model
{
enum FragmentScope
{
User,
Machine
};
[default_interface] runtimeclass CascadiaSettings {
static CascadiaSettings LoadDefaults();
static CascadiaSettings LoadAll();
@@ -38,6 +44,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; };
@@ -56,4 +63,36 @@ namespace Microsoft.Terminal.Settings.Model
void ExpandCommands();
}
[default_interface] runtimeclass FragmentProfileEntry
{
Guid ProfileGuid { get; };
String Json { get; };
}
[default_interface] runtimeclass FragmentColorSchemeEntry
{
String ColorSchemeName { get; };
String Json { get; };
Boolean Conflict { get; };
}
[default_interface] runtimeclass FragmentSettings
{
String Source { get; };
String Json { get; };
String Filename { get; };
IVectorView<FragmentProfileEntry> ModifiedProfilesView { get; };
IVectorView<FragmentProfileEntry> NewProfilesView { get; };
IVectorView<FragmentColorSchemeEntry> ColorSchemesView { get; };
}
[default_interface] runtimeclass ExtensionPackage
{
String Source { get; };
String DisplayName { get; };
String Icon { get; };
FragmentScope Scope { get; };
IVectorView<FragmentSettings> FragmentsView { get; };
}
}

View File

@@ -246,17 +246,18 @@ void SettingsLoader::FindFragmentsAndMergeIntoUserSettings()
{
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, bool applyToSettings) {
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, scope, fragExtPath.filename().wstring(), applyToSettings);
}
}
CATCH_LOG();
@@ -278,9 +279,12 @@ 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
!_ignoredNamespaces.contains(std::wstring_view{ source })); // applyToSettings
}
}
}
@@ -312,11 +316,8 @@ void SettingsLoader::FindFragmentsAndMergeIntoUserSettings()
for (const auto& ext : extensions)
{
const auto packageName = ext.Package().Id().FamilyName();
if (_ignoredNamespaces.contains(std::wstring_view{ packageName }))
{
continue;
}
const auto& package = ext.Package();
const auto packageName = package.Id().FamilyName();
// Likewise, getting the public folder from an extension is an async operation.
auto foundFolder = extractValueFromTaskWithoutMainThreadAwait(ext.GetPublicFolderAsync());
@@ -334,7 +335,16 @@ 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,
!_ignoredNamespaces.contains(std::wstring_view{ packageName })); // applyToSettings
auto extPkg = extensionPackageMap[packageName];
extPkg->Icon(package.Logo().AbsoluteUri());
extPkg->DisplayName(package.DisplayName());
}
}
}
@@ -345,7 +355,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, FragmentScope::User, L"filename.json", true);
}
// Call this method before passing SettingsLoader to the CascadiaSettings constructor.
@@ -724,15 +734,24 @@ 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)
// - scope: The scope of the fragment file (user or machine).
// - jsonFilename: The filename of the JSON file being parsed.
// - applyToSettings: If true, the parsed settings will be applied to the user settings. Otherwise, load the fragment for the settings UI, but don't apply it.
void SettingsLoader::_parseFragment(const winrt::hstring& source, const std::string_view& content, ParsedSettings& settings, FragmentScope scope, std::wstring_view jsonFilename, bool applyToSettings)
{
auto json = _parseJson(content);
Json::StreamWriterBuilder styledWriter;
styledWriter["indentation"] = " ";
styledWriter["commentStyle"] = "All";
auto fragmentSettings = winrt::make_self<FragmentSettings>(source, hstring{ til::u8u16(Json::writeString(styledWriter, json.root)) }, hstring{ jsonFilename });
settings.clear();
{
settings.globals = winrt::make_self<GlobalAppSettings>();
std::vector<Model::FragmentColorSchemeEntry> fragmentColorSchemes;
for (const auto& schemeJson : json.colorSchemes)
{
try
@@ -740,19 +759,27 @@ 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 (applyToSettings)
{
// 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));
}
fragmentColorSchemes.emplace_back(winrt::make<FragmentColorSchemeEntry>(scheme->Name(), hstring{ til::u8u16(Json::writeString(styledWriter, schemeJson)) }));
}
}
CATCH_LOG()
}
fragmentSettings->ColorSchemes(fragmentColorSchemes.empty() ? nullptr : single_threaded_vector<Model::FragmentColorSchemeEntry>(std::move(fragmentColorSchemes)));
// 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 (applyToSettings)
{
// 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);
}
}
{
@@ -760,52 +787,77 @@ void SettingsLoader::_parseFragment(const winrt::hstring& source, const std::str
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 (applyToSettings)
{
_appendProfile(std::move(profile), guid, settings);
}
destinationSet.emplace_back(winrt::make<FragmentProfileEntry>(guid, hstring{ til::u8u16(Json::writeString(styledWriter, profileJson)) }));
}
}
CATCH_LOG()
}
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)));
}
for (const auto& fragmentProfile : settings.profiles)
if (applyToSettings)
{
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);
if (!_addOrMergeUserColorScheme(fragmentColorScheme))
{
// Color scheme wasn't added because it conflicted with a non-user created scheme.
// Mark the fragment's color scheme as conflicting.
for (auto schemeEntry : fragmentSettings->ColorSchemes())
{
if (schemeEntry.ColorSchemeName() == fragmentColorScheme->Name())
{
get_self<FragmentColorSchemeEntry>(schemeEntry)->Conflict(true);
break;
}
}
}
}
}
// 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);
_registerFragment(std::move(*fragmentSettings), scope);
}
SettingsLoader::JsonSettings SettingsLoader::_parseJson(const std::string_view& content)
@@ -905,7 +957,8 @@ void SettingsLoader::_addUserProfileParent(const winrt::com_ptr<implementation::
}
}
void SettingsLoader::_addOrMergeUserColorScheme(const winrt::com_ptr<implementation::ColorScheme>& newScheme)
// returns true if the scheme was successfully added, otherwise false
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.
@@ -931,9 +984,12 @@ 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.
@@ -941,31 +997,67 @@ void SettingsLoader::_addOrMergeUserColorScheme(const winrt::com_ptr<implementat
void SettingsLoader::_executeGenerator(const IDynamicProfileGenerator& generator)
{
const auto generatorNamespace = generator.GetNamespace();
if (_ignoredNamespaces.contains(generatorNamespace))
{
return;
}
const auto previousSize = inboxSettings.profiles.size();
std::vector<winrt::com_ptr<implementation::Profile>> generatedProfiles;
try
{
generator.GenerateProfiles(inboxSettings.profiles);
generator.GenerateProfiles(generatedProfiles);
}
CATCH_LOG_MSG("Dynamic Profile Namespace: \"%.*s\"", gsl::narrow<int>(generatorNamespace.size()), generatorNamespace.data())
// These are needed for the FragmentSettings object
std::vector<Model::FragmentProfileEntry> profileEntries;
Json::Value profilesListJson{ Json::ValueType::arrayValue };
Json::StreamWriterBuilder styledWriter;
styledWriter["indentation"] = " ";
// 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)
const winrt::hstring source{ generatorNamespace };
for (const auto& profile : generatedProfiles)
{
const winrt::hstring source{ generatorNamespace };
profile->Origin(OriginTag::Generated);
profile->Source(source);
for (const auto& profile : std::span(inboxSettings.profiles).subspan(previousSize))
const auto profileJson = profile->ToJson();
profilesListJson.append(profileJson);
profileEntries.push_back(winrt::make<FragmentProfileEntry>(profile->Guid(), hstring{ til::u8u16(Json::writeString(styledWriter, profileJson)) }));
}
if (!_ignoredNamespaces.contains(generatorNamespace))
{
// Add generated profiles to the user settings
for (auto& profile : generatedProfiles)
{
profile->Origin(OriginTag::Generated);
profile->Source(source);
inboxSettings.profiles.push_back(profile);
}
}
// 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{ generatorNamespace }, hstring{ til::u8u16(Json::writeString(styledWriter, 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() });
}
winrt::com_ptr<ExtensionPackage> SettingsLoader::_registerFragment(const winrt::Microsoft::Terminal::Settings::Model::FragmentSettings& fragment, FragmentScope scope)
{
const auto src = fragment.Source();
if (auto extPkg = extensionPackageMap[src])
{
extPkg->Fragments().Append(fragment);
return extPkg;
}
else
{
auto newExtPkg = winrt::make_self<ExtensionPackage>(src, scope);
newExtPkg->Fragments().Append(fragment);
extensionPackageMap[src] = newExtPkg;
return newExtPkg;
}
}
// Method Description:
@@ -1258,6 +1350,11 @@ CascadiaSettings::CascadiaSettings(SettingsLoader&& loader) :
_warnings = winrt::single_threaded_vector(std::move(warnings));
_themesChangeLog = std::move(loader.userSettings.themesChangeLog);
for (auto [_, extPkg] : loader.extensionPackageMap)
{
_extensionPackages.Append(*extPkg);
}
_resolveDefaultProfile();
_resolveNewTabMenuProfiles();
_validateSettings();

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 Distro Profile Generator</value>
<comment>The display name of a dynamic profile generator for WSL distros</comment>
</data>
<data name="PowershellCoreProfileGeneratorDisplayName" xml:space="preserve">
<value>PowerShell Profile Generator</value>
<comment>The display name of a dynamic profile generator for PowerShell</comment>
</data>
<data name="AzureCloudShellGeneratorDisplayName" xml:space="preserve">
<value>Azure Cloud Shell Profile Generator</value>
<comment>The display name of a dynamic profile generator for Azure Cloud Shell</comment>
</data>
<data name="VisualStudioGeneratorDisplayName" xml:space="preserve">
<value>Visual Studio Profile Generator</value>
<comment>The display name of a dynamic profile generator for Visual Studio</comment>
</data>
<data name="SshHostGeneratorDisplayName" xml:space="preserve">
<value>SSH Host Profile Generator</value>
<comment>The display name of a dynamic profile generator for SSH hosts</comment>
</data>
</root>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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