From 27008bf4bd89e609d9d0061aa45bb4a99f5d2456 Mon Sep 17 00:00:00 2001 From: Carlos Zamora Date: Wed, 26 Feb 2025 10:41:41 -0800 Subject: [PATCH 1/3] Add display name and icon to SUI extensions page --- .../TerminalSettingsEditor/Extensions.cpp | 237 +++++++++--------- .../TerminalSettingsEditor/Extensions.h | 59 +++-- .../TerminalSettingsEditor/Extensions.idl | 24 +- .../TerminalSettingsEditor/Extensions.xaml | 74 +++++- .../TerminalSettingsEditor/MainPage.cpp | 94 ++++--- .../TerminalSettingsEditor/MainPage.h | 1 + .../CascadiaSettings.cpp | 48 ++-- .../TerminalSettingsModel/CascadiaSettings.h | 40 ++- .../CascadiaSettings.idl | 19 +- .../CascadiaSettingsSerialization.cpp | 37 ++- 10 files changed, 382 insertions(+), 251 deletions(-) diff --git a/src/cascadia/TerminalSettingsEditor/Extensions.cpp b/src/cascadia/TerminalSettingsEditor/Extensions.cpp index 1792785f93a..7a10e470915 100644 --- a/src/cascadia/TerminalSettingsEditor/Extensions.cpp +++ b/src/cascadia/TerminalSettingsEditor/Extensions.cpp @@ -7,6 +7,7 @@ #include "ExtensionPackageViewModel.g.cpp" #include "ExtensionsViewModel.g.cpp" #include "FragmentProfileViewModel.g.cpp" +#include "ExtensionPackageTemplateSelector.g.cpp" #include #include "..\WinRTUtils\inc\Utils.h" @@ -22,6 +23,8 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation Extensions::Extensions() { InitializeComponent(); + + _extensionPackageTemplateSelector = Resources().Lookup(box_value(L"ExtensionPackageTemplateSelector")).as(); } void Extensions::OnNavigatedTo(const NavigationEventArgs& e) @@ -29,24 +32,10 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation _ViewModel = e.Parameter().as(); } - void Extensions::ExtensionLoaded(const IInspectable& sender, const RoutedEventArgs& /*args*/) - { - const auto& toggleSwitch = sender.as(); - const auto& extensionSource = toggleSwitch.Tag().as(); - toggleSwitch.IsOn(_ViewModel.GetExtensionState(extensionSource)); - } - - void Extensions::ExtensionToggled(const IInspectable& sender, const RoutedEventArgs& /*args*/) - { - const auto& toggleSwitch = sender.as(); - const auto& extensionSource = toggleSwitch.Tag().as(); - _ViewModel.SetExtensionState(extensionSource, toggleSwitch.IsOn()); - } - void Extensions::ExtensionNavigator_Click(const IInspectable& sender, const RoutedEventArgs& /*args*/) { - const auto source = sender.as().Tag().as(); - _ViewModel.CurrentExtensionSource(source); + const auto extPkgVM = sender.as().Tag().as(); + _ViewModel.CurrentExtensionPackage(extPkgVM); } void Extensions::NavigateToProfile_Click(const IInspectable& sender, const RoutedEventArgs& /*args*/) @@ -69,20 +58,18 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation PropertyChanged([this](auto&&, const PropertyChangedEventArgs& args) { const auto viewModelProperty{ args.PropertyName() }; - if (viewModelProperty == L"CurrentExtensionSource") + if (viewModelProperty == L"CurrentExtensionPackage") { - // Update the views to reflect the current extension source, if one is selected. + // 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(); - const auto currentExtensionSource = CurrentExtensionSource(); - for (const auto& ext : _fragmentExtensions) - { - // No extension selected --> show all enabled extension components - // Otherwise, only show the ones for the selected extension - if (const auto extSrc = ext.Fragment().Source(); (currentExtensionSource.empty() && GetExtensionState(extSrc)) || extSrc == currentExtensionSource) + // Helper lambda to add the contents of an extension package to the current view + auto addPackageContentsToView = [&](const Editor::ExtensionPackageViewModel& extPkg) { + auto extPkgVM = get_self(extPkg); + for (const auto& ext : extPkgVM->FragmentExtensions()) { for (const auto& profile : ext.ProfilesModified()) { @@ -97,9 +84,21 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation _colorSchemesAddedView.Append(scheme); } } + }; + + if (const auto currentExtensionPackage = CurrentExtensionPackage()) + { + addPackageContentsToView(currentExtensionPackage); + } + else + { + for (const auto& extPkg : _extensionPackages) + { + addPackageContentsToView(extPkg); + } } - _NotifyChanges(L"IsExtensionView", L"CurrentExtensionFragments"); + _NotifyChanges(L"IsExtensionView"); } }); } @@ -108,134 +107,86 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { _settings = settings; _colorSchemesPageVM = colorSchemesPageVM; - _extensionSources.clear(); - _CurrentExtensionSource.clear(); - - std::vector extensions; - extensions.reserve(settings.FragmentExtensions().Size() + settings.DynamicProfileGenerators().Size()); - for (auto ext : settings.FragmentExtensions()) - { - extensions.push_back(ext); - } - for (auto ext : settings.DynamicProfileGenerators()) - { - extensions.push_back(ext); - } + _CurrentExtensionPackage = nullptr; - std::vector extensionVMs; - extensionVMs.reserve(extensions.size()); + std::vector extensions = wil::to_vector(settings.Extensions()); // these vectors track components all extensions successfully added + std::vector extensionPackages; std::vector profilesModifiedTotal; std::vector profilesAddedTotal; std::vector colorSchemesAddedTotal; - for (const auto& fragExt : extensions) + for (const auto& extPkg : extensions) { - const auto extensionEnabled = GetExtensionState(fragExt.Source()); + auto extPkgVM = winrt::make_self(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 currentProfilesModified; - std::vector currentProfilesAdded; - std::vector currentColorSchemesAdded; + // these vectors track everything the current extension attempted to bring in + std::vector currentProfilesModified; + std::vector currentProfilesAdded; + std::vector currentColorSchemesAdded; - 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())) + for (const auto&& entry : fragExt.ModifiedProfilesView()) { - auto vm = winrt::make(entry, fragExt, deducedProfile); - currentProfilesModified.push_back(vm); - if (extensionEnabled) + // Ensure entry successfully modifies a profile before creating and registering the object + if (const auto& deducedProfile = _settings.FindProfile(entry.ProfileGuid())) { - profilesModifiedTotal.push_back(vm); + auto vm = winrt::make(entry, fragExt, deducedProfile); + currentProfilesModified.push_back(vm); + if (extensionEnabled) + { + profilesModifiedTotal.push_back(vm); + } } } - } - 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())) + for (const auto&& entry : fragExt.NewProfilesView()) { - auto vm = winrt::make(entry, fragExt, deducedProfile); - currentProfilesAdded.push_back(vm); - if (extensionEnabled) + // 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())) { - profilesAddedTotal.push_back(vm); + auto vm = winrt::make(entry, fragExt, deducedProfile); + currentProfilesAdded.push_back(vm); + if (extensionEnabled) + { + profilesAddedTotal.push_back(vm); + } } } - } - for (const auto&& entry : fragExt.ColorSchemesView()) - { - for (const auto& schemeVM : _colorSchemesPageVM.AllColorSchemes()) + for (const auto&& entry : fragExt.ColorSchemesView()) { - if (schemeVM.Name() == entry.ColorSchemeName()) + for (const auto& schemeVM : _colorSchemesPageVM.AllColorSchemes()) { - auto vm = winrt::make(entry, fragExt, schemeVM); - currentColorSchemesAdded.push_back(vm); - if (extensionEnabled) + if (schemeVM.Name() == entry.ColorSchemeName()) { - colorSchemesAddedTotal.push_back(vm); + auto vm = winrt::make(entry, fragExt, schemeVM); + currentColorSchemesAdded.push_back(vm); + if (extensionEnabled) + { + colorSchemesAddedTotal.push_back(vm); + } } } } + extPkgVM->FragmentExtensions().Append(winrt::make(fragExt, currentProfilesModified, currentProfilesAdded, currentColorSchemesAdded)); } - - _extensionSources.insert(fragExt.Source()); - extensionVMs.push_back(winrt::make(fragExt, currentProfilesModified, currentProfilesAdded, currentColorSchemesAdded)); } - _fragmentExtensions = single_threaded_observable_vector(std::move(extensionVMs)); + _extensionPackages = single_threaded_observable_vector(std::move(extensionPackages)); _profilesModifiedView = single_threaded_observable_vector(std::move(profilesModifiedTotal)); _profilesAddedView = single_threaded_observable_vector(std::move(profilesAddedTotal)); _colorSchemesAddedView = single_threaded_observable_vector(std::move(colorSchemesAddedTotal)); } - IVector ExtensionsViewModel::CurrentExtensionFragments() const noexcept - { - std::vector fragmentExtensionVMs; - for (auto&& extVM : _fragmentExtensions) - { - if (_CurrentExtensionSource.empty() || extVM.Fragment().Source() == _CurrentExtensionSource) - { - fragmentExtensionVMs.push_back(extVM); - } - } - return winrt::single_threaded_vector(std::move(fragmentExtensionVMs)); - } - - hstring ExtensionsViewModel::CurrentExtensionScope() const noexcept - { - if (!_CurrentExtensionSource.empty()) - { - for (auto&& extVM : _fragmentExtensions) - { - const auto& fragExt = extVM.Fragment(); - if (fragExt.Source() == _CurrentExtensionSource) - { - return fragExt.Scope() == Model::FragmentScope::User ? RS_(L"Extensions_ScopeUser") : RS_(L"Extensions_ScopeSystem"); - } - } - } - return hstring{}; - } - - IObservableVector ExtensionsViewModel::ExtensionPackages() const noexcept - { - std::vector extensionPackages; - for (auto&& extSrc : _extensionSources) - { - extensionPackages.push_back(winrt::make(extSrc, GetExtensionState(extSrc))); - } - return winrt::single_threaded_observable_vector(std::move(extensionPackages)); - } - // Returns true if the extension is enabled, false otherwise - bool ExtensionsViewModel::GetExtensionState(hstring extensionSource) const + bool ExtensionsViewModel::GetExtensionState(hstring extensionSource, const Model::CascadiaSettings& settings) { - if (const auto& disabledExtensions = _DisabledProfileSources()) + if (const auto& disabledExtensions = settings.GlobalSettings().DisabledProfileSources()) { uint32_t ignored; return !disabledExtensions.IndexOf(extensionSource, ignored); @@ -245,12 +196,12 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation } // Enable/Disable an extension - void ExtensionsViewModel::SetExtensionState(hstring extensionSource, bool enableExt) + 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 = _DisabledProfileSources(); + const auto& disabledExtensions = settings.GlobalSettings().DisabledProfileSources(); if (disabledExtensions) { currentlyEnabled = !disabledExtensions.IndexOf(extensionSource, idx); @@ -265,7 +216,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation if (!disabledExtensions && !enableExt) { std::vector disabledProfileSources{ extensionSource }; - _settings.GlobalSettings().DisabledProfileSources(single_threaded_vector(std::move(disabledProfileSources))); + settings.GlobalSettings().DisabledProfileSources(single_threaded_vector(std::move(disabledProfileSources))); return; } @@ -292,12 +243,50 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation NavigateToColorSchemeRequested.raise(*this, nullptr); } + 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"); + } + } + hstring ExtensionPackageViewModel::AccessibleName() const noexcept { - if (_enabled) + if (Enabled()) { - return _source; + return _package.Source(); + } + return hstring{ fmt::format(L"{}: {}", _package.Source(), RS_(L"Extension_StateDisabled/Text")) }; + } + + 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()) + { + if (!extPkgVM.Package().DisplayName().empty()) + { + return ComplexTemplate(); + } + return DefaultTemplate(); } - return hstring{ fmt::format(L"{}: {}", _source, RS_(L"Extension_StateDisabled/Text")) }; + assert(false); + return nullptr; } } diff --git a/src/cascadia/TerminalSettingsEditor/Extensions.h b/src/cascadia/TerminalSettingsEditor/Extensions.h index 8bd9dfaf911..7941677fcce 100644 --- a/src/cascadia/TerminalSettingsEditor/Extensions.h +++ b/src/cascadia/TerminalSettingsEditor/Extensions.h @@ -9,6 +9,7 @@ #include "FragmentExtensionViewModel.g.h" #include "FragmentProfileViewModel.g.h" #include "FragmentColorSchemeViewModel.g.h" +#include "ExtensionPackageTemplateSelector.g.h" #include "ViewModelHelpers.h" #include "Utils.h" @@ -20,14 +21,15 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation Extensions(); void OnNavigatedTo(const Windows::UI::Xaml::Navigation::NavigationEventArgs& e); - void ExtensionLoaded(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& args); - void ExtensionToggled(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& args); 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 _extensionPackageTemplateSelector; }; struct ExtensionsViewModel : ExtensionsViewModelT, ViewModelHelper @@ -36,57 +38,59 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation ExtensionsViewModel(const Model::CascadiaSettings& settings, const Editor::ColorSchemesPageViewModel& colorSchemesPageVM); // Properties - bool IsExtensionView() const noexcept { return _CurrentExtensionSource != hstring{}; } - Windows::Foundation::Collections::IVector CurrentExtensionFragments() const noexcept; - hstring CurrentExtensionScope() const noexcept; - bool NoActiveExtensions() const noexcept { return _fragmentExtensions.Size() == 0; } + 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; } // Views - Windows::Foundation::Collections::IObservableVector ExtensionPackages() const noexcept; + Windows::Foundation::Collections::IObservableVector ExtensionPackages() const noexcept { return _extensionPackages; } Windows::Foundation::Collections::IObservableVector ProfilesModified() const noexcept { return _profilesModifiedView; } Windows::Foundation::Collections::IObservableVector ProfilesAdded() const noexcept { return _profilesAddedView; } Windows::Foundation::Collections::IObservableVector ColorSchemesAdded() const noexcept { return _colorSchemesAddedView; } // Methods void UpdateSettings(const Model::CascadiaSettings& settings, const Editor::ColorSchemesPageViewModel& colorSchemesPageVM); - bool GetExtensionState(hstring extensionSource) const; - void SetExtensionState(hstring extensionSource, bool enableExt); void NavigateToProfile(const guid profileGuid); void NavigateToColorScheme(const Editor::ColorSchemeViewModel& schemeVM); + static bool GetExtensionState(hstring extensionSource, const Model::CascadiaSettings& settings); + static void SetExtensionState(hstring extensionSource, const Model::CascadiaSettings& settings, bool enableExt); + til::typed_event NavigateToProfileRequested; til::typed_event NavigateToColorSchemeRequested; - VIEW_MODEL_OBSERVABLE_PROPERTY(hstring, CurrentExtensionSource); + VIEW_MODEL_OBSERVABLE_PROPERTY(Editor::ExtensionPackageViewModel, CurrentExtensionPackage, nullptr); private: Model::CascadiaSettings _settings; Editor::ColorSchemesPageViewModel _colorSchemesPageVM; - std::unordered_set _extensionSources; - Windows::Foundation::Collections::IVector _fragmentExtensions; + Windows::Foundation::Collections::IObservableVector _extensionPackages; Windows::Foundation::Collections::IObservableVector _profilesModifiedView; Windows::Foundation::Collections::IObservableVector _profilesAddedView; Windows::Foundation::Collections::IObservableVector _colorSchemesAddedView; - - Windows::Foundation::Collections::IVector _DisabledProfileSources() const noexcept { return _settings.GlobalSettings().DisabledProfileSources(); } }; struct ExtensionPackageViewModel : ExtensionPackageViewModelT, ViewModelHelper { public: - ExtensionPackageViewModel(hstring source, bool enabled) : - _source{ source }, - _enabled{ enabled } {} - hstring Source() const noexcept { return _source; } - bool Enabled() const noexcept { return _enabled; } + ExtensionPackageViewModel(const Model::ExtensionPackage& pkg, const Model::CascadiaSettings& settings) : + _package{ pkg }, + _settings{ settings }, + _fragmentExtensions{ single_threaded_observable_vector() } {} + + Model::ExtensionPackage Package() const noexcept { return _package; } + hstring Scope() const noexcept; + bool Enabled() const; + void Enabled(bool val); hstring AccessibleName() const noexcept; + Windows::Foundation::Collections::IObservableVector FragmentExtensions() { return _fragmentExtensions; } private: - hstring _source; - bool _enabled; + Model::ExtensionPackage _package; + Model::CascadiaSettings _settings; + Windows::Foundation::Collections::IObservableVector _fragmentExtensions; }; struct FragmentExtensionViewModel : FragmentExtensionViewModelT, ViewModelHelper @@ -148,9 +152,22 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation Model::FragmentSettings _fragment; Editor::ColorSchemeViewModel _deducedSchemeVM; }; + + struct ExtensionPackageTemplateSelector : public ExtensionPackageTemplateSelectorT + { + 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); } diff --git a/src/cascadia/TerminalSettingsEditor/Extensions.idl b/src/cascadia/TerminalSettingsEditor/Extensions.idl index ba5cac9885f..a806e5eed2e 100644 --- a/src/cascadia/TerminalSettingsEditor/Extensions.idl +++ b/src/cascadia/TerminalSettingsEditor/Extensions.idl @@ -14,25 +14,21 @@ namespace Microsoft.Terminal.Settings.Editor [default_interface] runtimeclass ExtensionsViewModel : Windows.UI.Xaml.Data.INotifyPropertyChanged { // Properties - String CurrentExtensionSource; - IVector CurrentExtensionFragments { get; }; - String CurrentExtensionScope { get; }; + ExtensionPackageViewModel CurrentExtensionPackage; Boolean IsExtensionView { get; }; - Boolean NoActiveExtensions { get; }; + Boolean NoExtensionPackages { get; }; Boolean NoProfilesModified { get; }; Boolean NoProfilesAdded { get; }; Boolean NoSchemesAdded { get; }; // Views - IObservableVector ExtensionPackages { get; }; + IVector ExtensionPackages { get; }; IObservableVector ProfilesModified { get; }; IObservableVector ProfilesAdded { get; }; IObservableVector ColorSchemesAdded { get; }; // Methods void UpdateSettings(Microsoft.Terminal.Settings.Model.CascadiaSettings settings, ColorSchemesPageViewModel colorSchemesPageVM); - Boolean GetExtensionState(String extensionSource); - void SetExtensionState(String extensionSource, Boolean enableExt); event Windows.Foundation.TypedEventHandler NavigateToProfileRequested; event Windows.Foundation.TypedEventHandler NavigateToColorSchemeRequested; @@ -40,9 +36,11 @@ namespace Microsoft.Terminal.Settings.Editor [default_interface] runtimeclass ExtensionPackageViewModel : Windows.UI.Xaml.Data.INotifyPropertyChanged { - String Source { get; }; - Boolean Enabled { get; }; + Microsoft.Terminal.Settings.Model.ExtensionPackage Package { get; }; + Boolean Enabled; + String Scope { get; }; String AccessibleName { get; }; + IVector FragmentExtensions { get; }; } [default_interface] runtimeclass FragmentExtensionViewModel : Windows.UI.Xaml.Data.INotifyPropertyChanged @@ -66,4 +64,12 @@ namespace Microsoft.Terminal.Settings.Editor String SourceName { get; }; String Json { get; }; } + + [default_interface] runtimeclass ExtensionPackageTemplateSelector : Windows.UI.Xaml.Controls.DataTemplateSelector + { + ExtensionPackageTemplateSelector(); + + Windows.UI.Xaml.DataTemplate DefaultTemplate; + Windows.UI.Xaml.DataTemplate ComplexTemplate; + } } diff --git a/src/cascadia/TerminalSettingsEditor/Extensions.xaml b/src/cascadia/TerminalSettingsEditor/Extensions.xaml index 84c191bde3b..3f92d8acd04 100644 --- a/src/cascadia/TerminalSettingsEditor/Extensions.xaml +++ b/src/cascadia/TerminalSettingsEditor/Extensions.xaml @@ -39,22 +39,71 @@ - + + + + + +