diff --git a/src/cascadia/CascadiaPackage/ProfileGeneratorIcons/AzureCloudShell.png b/src/cascadia/CascadiaPackage/ProfileGeneratorIcons/AzureCloudShell.png new file mode 100644 index 0000000000..c7ee454ce9 Binary files /dev/null and b/src/cascadia/CascadiaPackage/ProfileGeneratorIcons/AzureCloudShell.png differ diff --git a/src/cascadia/CascadiaPackage/ProfileGeneratorIcons/PowerShell.png b/src/cascadia/CascadiaPackage/ProfileGeneratorIcons/PowerShell.png new file mode 100644 index 0000000000..72f5e54e31 Binary files /dev/null and b/src/cascadia/CascadiaPackage/ProfileGeneratorIcons/PowerShell.png differ diff --git a/src/cascadia/CascadiaPackage/ProfileGeneratorIcons/SSH.png b/src/cascadia/CascadiaPackage/ProfileGeneratorIcons/SSH.png new file mode 100644 index 0000000000..ebba22951f Binary files /dev/null and b/src/cascadia/CascadiaPackage/ProfileGeneratorIcons/SSH.png differ diff --git a/src/cascadia/CascadiaPackage/ProfileGeneratorIcons/VisualStudio.png b/src/cascadia/CascadiaPackage/ProfileGeneratorIcons/VisualStudio.png new file mode 100644 index 0000000000..c41b6cb581 Binary files /dev/null and b/src/cascadia/CascadiaPackage/ProfileGeneratorIcons/VisualStudio.png differ diff --git a/src/cascadia/CascadiaPackage/ProfileGeneratorIcons/WSL.png b/src/cascadia/CascadiaPackage/ProfileGeneratorIcons/WSL.png new file mode 100644 index 0000000000..b572346ff5 Binary files /dev/null and b/src/cascadia/CascadiaPackage/ProfileGeneratorIcons/WSL.png differ diff --git a/src/cascadia/CascadiaResources.build.items b/src/cascadia/CascadiaResources.build.items index cfd11c79f4..336fad5048 100644 --- a/src/cascadia/CascadiaResources.build.items +++ b/src/cascadia/CascadiaResources.build.items @@ -21,6 +21,11 @@ true ProfileIcons\%(RecursiveDir)%(FileName)%(Extension) + + + true + ProfileGeneratorIcons\%(RecursiveDir)%(FileName)%(Extension) + true diff --git a/src/cascadia/TerminalSettingsEditor/AddProfile.xaml b/src/cascadia/TerminalSettingsEditor/AddProfile.xaml index b278f71ae6..3ea92ad0f6 100644 --- a/src/cascadia/TerminalSettingsEditor/AddProfile.xaml +++ b/src/cascadia/TerminalSettingsEditor/AddProfile.xaml @@ -59,7 +59,7 @@ + IconSource="{x:Bind mtu:IconPathConverter.IconSourceWUX(EvaluatedIcon), Mode=OneTime}" /> diff --git a/src/cascadia/TerminalSettingsEditor/CommonResources.xaml b/src/cascadia/TerminalSettingsEditor/CommonResources.xaml index 7655deb559..8f26f152ba 100644 --- a/src/cascadia/TerminalSettingsEditor/CommonResources.xaml +++ b/src/cascadia/TerminalSettingsEditor/CommonResources.xaml @@ -1204,7 +1204,7 @@ - + @@ -1227,7 +1227,8 @@ Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" ContentTransitions="{TemplateBinding ContentTransitions}" /> - + + diff --git a/src/cascadia/TerminalSettingsEditor/Extensions.cpp b/src/cascadia/TerminalSettingsEditor/Extensions.cpp new file mode 100644 index 0000000000..ffaff9a33d --- /dev/null +++ b/src/cascadia/TerminalSettingsEditor/Extensions.cpp @@ -0,0 +1,509 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "Extensions.h" +#include "Extensions.g.cpp" +#include "ExtensionPackageViewModel.g.cpp" +#include "ExtensionsViewModel.g.cpp" +#include "FragmentProfileViewModel.g.cpp" +#include "ExtensionPackageTemplateSelector.g.cpp" + +#include +#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(); + + 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(); + auto vmImpl = get_self(_ViewModel); + vmImpl->ExtensionPackageIdentifierTemplateSelector(_extensionPackageIdentifierTemplateSelector); + vmImpl->LazyLoadExtensions(); + vmImpl->MarkAsVisited(); + } + + void Extensions::ExtensionNavigator_Click(const IInspectable& sender, const RoutedEventArgs& /*args*/) + { + const auto extPkgVM = sender.as().Tag().as(); + _ViewModel.CurrentExtensionPackage(extPkgVM); + } + + void Extensions::NavigateToProfile_Click(const IInspectable& sender, const RoutedEventArgs& /*args*/) + { + const auto& profileGuid = sender.as().Tag().as(); + get_self(_ViewModel)->NavigateToProfile(profileGuid); + } + + void Extensions::NavigateToColorScheme_Click(const IInspectable& sender, const RoutedEventArgs& /*args*/) + { + const auto& schemeVM = sender.as().Tag().as(); + get_self(_ViewModel)->NavigateToColorScheme(schemeVM); + } + + ExtensionsViewModel::ExtensionsViewModel(const Model::CascadiaSettings& settings, const Editor::ColorSchemesPageViewModel& colorSchemesPageVM) : + _settings{ settings }, + _colorSchemesPageVM{ colorSchemesPageVM }, + _extensionsLoaded{ false } + { + UpdateSettings(settings, colorSchemesPageVM); + + PropertyChanged([this](auto&&, const PropertyChangedEventArgs& args) { + const auto viewModelProperty{ args.PropertyName() }; + + const bool extensionPackageChanged = viewModelProperty == L"CurrentExtensionPackage"; + const bool profilesModifiedChanged = viewModelProperty == L"ProfilesModified"; + const bool profilesAddedChanged = viewModelProperty == L"ProfilesAdded"; + const bool colorSchemesAddedChanged = viewModelProperty == L"ColorSchemesAdded"; + if (extensionPackageChanged || (!IsExtensionView() && (profilesModifiedChanged || profilesAddedChanged || colorSchemesAddedChanged))) + { + // Use these booleans to track which of our observable vectors need to be refreshed. + // This prevents a full refresh of the UI when enabling/disabling extensions. + // If the CurrentExtensionPackage changed, we want to update all components. + // Otherwise, just update the ones that we were notified about. + const bool updateProfilesModified = extensionPackageChanged || profilesModifiedChanged; + const bool updateProfilesAdded = extensionPackageChanged || profilesAddedChanged; + const bool updateColorSchemesAdded = extensionPackageChanged || colorSchemesAddedChanged; + _UpdateListViews(updateProfilesModified, updateProfilesAdded, updateColorSchemesAdded); + + if (extensionPackageChanged) + { + _NotifyChanges(L"IsExtensionView", L"CurrentExtensionPackageIdentifierTemplate"); + } + else if (profilesModifiedChanged) + { + _NotifyChanges(L"NoProfilesModified"); + } + else if (profilesAddedChanged) + { + _NotifyChanges(L"NoProfilesAdded"); + } + else if (colorSchemesAddedChanged) + { + _NotifyChanges(L"NoSchemesAdded"); + } + } + }); + } + + void ExtensionsViewModel::_UpdateListViews(bool updateProfilesModified, bool updateProfilesAdded, bool updateColorSchemesAdded) + { + // STL vectors to track relevant components for extensions to display in UI + std::vector profilesModifiedTotal; + std::vector profilesAddedTotal; + std::vector colorSchemesAddedTotal; + + // Helper lambda to add the contents of an extension package to the current view. + auto addPackageContentsToView = [&](const Editor::ExtensionPackageViewModel& extPkg) { + auto extPkgVM = get_self(extPkg); + for (const auto& ext : extPkgVM->FragmentExtensions()) + { + if (updateProfilesModified) + { + for (const auto& profile : ext.ProfilesModified()) + { + profilesModifiedTotal.push_back(profile); + } + } + if (updateProfilesAdded) + { + for (const auto& profile : ext.ProfilesAdded()) + { + profilesAddedTotal.push_back(profile); + } + } + if (updateColorSchemesAdded) + { + for (const auto& scheme : ext.ColorSchemesAdded()) + { + colorSchemesAddedTotal.push_back(scheme); + } + } + } + }; + + // Populate the STL vectors that we want to update + if (const auto currentExtensionPackage = CurrentExtensionPackage()) + { + // Update all of the views to reflect the current extension package, if one is selected. + addPackageContentsToView(currentExtensionPackage); + } + else + { + // Only populate the views with components from enabled extensions + for (const auto& extPkg : _extensionPackages) + { + if (extPkg.Enabled()) + { + addPackageContentsToView(extPkg); + } + } + } + + // Sort the lists linguistically for nicer presentation. + // Update the WinRT lists bound to UI. + if (updateProfilesModified) + { + std::sort(profilesModifiedTotal.begin(), profilesModifiedTotal.end(), FragmentProfileViewModel::SortAscending); + _profilesModifiedView = winrt::single_threaded_observable_vector(std::move(profilesModifiedTotal)); + } + if (updateProfilesAdded) + { + std::sort(profilesAddedTotal.begin(), profilesAddedTotal.end(), FragmentProfileViewModel::SortAscending); + _profilesAddedView = winrt::single_threaded_observable_vector(std::move(profilesAddedTotal)); + } + if (updateColorSchemesAdded) + { + std::sort(colorSchemesAddedTotal.begin(), colorSchemesAddedTotal.end(), FragmentColorSchemeViewModel::SortAscending); + _colorSchemesAddedView = winrt::single_threaded_observable_vector(std::move(colorSchemesAddedTotal)); + } + } + + void ExtensionsViewModel::UpdateSettings(const Model::CascadiaSettings& settings, const Editor::ColorSchemesPageViewModel& colorSchemesPageVM) + { + _settings = settings; + _colorSchemesPageVM = colorSchemesPageVM; + _CurrentExtensionPackage = nullptr; + + // The extension packages may not be loaded yet because we want to wait until we actually navigate to the page to do so. + // In that case, omit "updating" them. They'll get the proper references when we lazy load them. + if (_extensionPackages) + { + for (const auto& extPkg : _extensionPackages) + { + get_self(extPkg)->UpdateSettings(_settings); + } + } + } + + void ExtensionsViewModel::LazyLoadExtensions() + { + if (_extensionsLoaded) + { + return; + } + 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& extPkg : extensions) + { + auto extPkgVM = winrt::make_self(extPkg, _settings); + for (const auto& fragExt : extPkg.FragmentsView()) + { + const auto extensionEnabled = GetExtensionState(fragExt.Source(), _settings); + + // these vectors track everything the current extension attempted to bring in + std::vector currentProfilesModified; + std::vector currentProfilesAdded; + std::vector 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(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(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(entry, fragExt, schemeVM); + currentColorSchemesAdded.push_back(vm); + if (extensionEnabled) + { + colorSchemesAddedTotal.push_back(vm); + } + } + } + } + } + + // sort the lists linguistically for nicer presentation + std::sort(currentProfilesModified.begin(), currentProfilesModified.end(), FragmentProfileViewModel::SortAscending); + std::sort(currentProfilesAdded.begin(), currentProfilesAdded.end(), FragmentProfileViewModel::SortAscending); + std::sort(currentColorSchemesAdded.begin(), currentColorSchemesAdded.end(), FragmentColorSchemeViewModel::SortAscending); + + extPkgVM->FragmentExtensions().Append(winrt::make(fragExt, currentProfilesModified, currentProfilesAdded, currentColorSchemesAdded)); + extPkgVM->PropertyChanged([&](const IInspectable& sender, const PropertyChangedEventArgs& args) { + const auto viewModelProperty{ args.PropertyName() }; + if (viewModelProperty == L"Enabled") + { + // If the extension was enabled/disabled, + // check if any of its fragments modified profiles, added profiles, or added color schemes. + // Only notify what was affected! + bool hasModifiedProfiles = false; + bool hasAddedProfiles = false; + bool hasAddedColorSchemes = false; + for (const auto& fragExtVM : sender.as()->FragmentExtensions()) + { + const auto profilesModified = fragExtVM.ProfilesModified(); + const auto profilesAdded = fragExtVM.ProfilesAdded(); + const auto colorSchemesAdded = fragExtVM.ColorSchemesAdded(); + hasModifiedProfiles |= profilesModified && profilesModified.Size() > 0; + hasAddedProfiles |= profilesAdded && profilesAdded.Size() > 0; + hasAddedColorSchemes |= colorSchemesAdded && colorSchemesAdded.Size() > 0; + } + if (hasModifiedProfiles) + { + _NotifyChanges(L"ProfilesModified"); + } + if (hasAddedProfiles) + { + _NotifyChanges(L"ProfilesAdded"); + } + if (hasAddedColorSchemes) + { + _NotifyChanges(L"ColorSchemesAdded"); + } + } + }); + } + extensionPackages.push_back(*extPkgVM); + } + + // sort the lists linguistically for nicer presentation + std::sort(extensionPackages.begin(), extensionPackages.end(), ExtensionPackageViewModel::SortAscending); + std::sort(profilesModifiedTotal.begin(), profilesModifiedTotal.end(), FragmentProfileViewModel::SortAscending); + std::sort(profilesAddedTotal.begin(), profilesAddedTotal.end(), FragmentProfileViewModel::SortAscending); + std::sort(colorSchemesAddedTotal.begin(), colorSchemesAddedTotal.end(), FragmentColorSchemeViewModel::SortAscending); + + _extensionPackages = single_threaded_observable_vector(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)); + _extensionsLoaded = true; + } + + Windows::UI::Xaml::DataTemplate ExtensionsViewModel::CurrentExtensionPackageIdentifierTemplate() const + { + return _ExtensionPackageIdentifierTemplateSelector.SelectTemplate(CurrentExtensionPackage()); + } + + bool ExtensionsViewModel::DisplayBadge() const noexcept + { + return !Model::ApplicationState::SharedInstance().BadgeDismissed(ExtensionPageId); + } + + // Returns true if the extension is enabled, false otherwise + bool ExtensionsViewModel::GetExtensionState(hstring extensionSource, const Model::CascadiaSettings& settings) + { + if (const auto& disabledExtensions = settings.GlobalSettings().DisabledProfileSources()) + { + uint32_t ignored; + return !disabledExtensions.IndexOf(extensionSource, ignored); + } + // "disabledProfileSources" not defined --> all extensions are enabled + return true; + } + + // Enable/Disable an extension + void ExtensionsViewModel::SetExtensionState(hstring extensionSource, const Model::CascadiaSettings& settings, bool enableExt) + { + // get the current status of the extension + uint32_t idx; + bool currentlyEnabled = true; + const auto& disabledExtensions = settings.GlobalSettings().DisabledProfileSources(); + if (disabledExtensions) + { + currentlyEnabled = !disabledExtensions.IndexOf(extensionSource, idx); + } + + // current status mismatches the desired status, + // update the list of disabled extensions + if (currentlyEnabled != enableExt) + { + // If we're disabling an extension and we don't have "disabledProfileSources" defined, + // create it in the model directly + if (!disabledExtensions && !enableExt) + { + std::vector disabledProfileSources{ extensionSource }; + settings.GlobalSettings().DisabledProfileSources(single_threaded_vector(std::move(disabledProfileSources))); + return; + } + + // Update the list of disabled extensions + if (enableExt) + { + disabledExtensions.RemoveAt(idx); + } + else + { + disabledExtensions.Append(extensionSource); + } + } + } + + Thickness Extensions::CalculateMargin(bool hidden) + { + return ThicknessHelper::FromLengths(/*left*/ 0, + /*top*/ hidden ? 0 : 20, + /*right*/ 0, + /*bottom*/ 0); + } + + void ExtensionsViewModel::NavigateToProfile(const guid profileGuid) + { + NavigateToProfileRequested.raise(*this, profileGuid); + } + + void ExtensionsViewModel::NavigateToColorScheme(const Editor::ColorSchemeViewModel& schemeVM) + { + _colorSchemesPageVM.CurrentScheme(schemeVM); + NavigateToColorSchemeRequested.raise(*this, nullptr); + } + + void ExtensionsViewModel::MarkAsVisited() + { + Model::ApplicationState::SharedInstance().DismissBadge(ExtensionPageId); + _NotifyChanges(L"DisplayBadge"); + } + + bool ExtensionPackageViewModel::SortAscending(const Editor::ExtensionPackageViewModel& lhs, const Editor::ExtensionPackageViewModel& rhs) + { + auto getKey = [&](const Editor::ExtensionPackageViewModel& pkgVM) { + const auto pkg = pkgVM.Package(); + const auto displayName = pkg.DisplayName(); + return displayName.empty() ? pkg.Source() : displayName; + }; + + return til::compare_linguistic_insensitive(getKey(lhs), getKey(rhs)) < 0; + } + + void ExtensionPackageViewModel::UpdateSettings(const Model::CascadiaSettings& settings) + { + const auto oldEnabled = Enabled(); + _settings = settings; + if (oldEnabled != Enabled()) + { + // The enabled state of the extension has changed, notify the UI + _NotifyChanges(L"Enabled"); + } + } + + hstring ExtensionPackageViewModel::Scope() const noexcept + { + return _package.Scope() == Model::FragmentScope::User ? RS_(L"Extensions_ScopeUser") : RS_(L"Extensions_ScopeSystem"); + } + + bool ExtensionPackageViewModel::Enabled() const + { + return ExtensionsViewModel::GetExtensionState(_package.Source(), _settings); + } + + void ExtensionPackageViewModel::Enabled(bool val) + { + if (Enabled() != val) + { + ExtensionsViewModel::SetExtensionState(_package.Source(), _settings, val); + _NotifyChanges(L"Enabled"); + } + } + + // Returns the accessible name for the extension package in the following format: + // ", " + hstring ExtensionPackageViewModel::AccessibleName() const noexcept + { + hstring name; + const auto source = _package.Source(); + if (const auto displayName = _package.DisplayName(); !displayName.empty()) + { + return hstring{ fmt::format(FMT_COMPILE(L"{}, {}"), displayName, source) }; + } + return source; + } + + bool FragmentProfileViewModel::SortAscending(const Editor::FragmentProfileViewModel& lhs, const Editor::FragmentProfileViewModel& rhs) + { + return til::compare_linguistic_insensitive(lhs.Profile().Name(), rhs.Profile().Name()) < 0; + } + + hstring FragmentProfileViewModel::AccessibleName() const noexcept + { + return hstring{ fmt::format(FMT_COMPILE(L"{}, {}"), Profile().Name(), SourceName()) }; + } + + bool FragmentColorSchemeViewModel::SortAscending(const Editor::FragmentColorSchemeViewModel& lhs, const Editor::FragmentColorSchemeViewModel& rhs) + { + return til::compare_linguistic_insensitive(lhs.ColorSchemeVM().Name(), rhs.ColorSchemeVM().Name()) < 0; + } + + hstring FragmentColorSchemeViewModel::AccessibleName() const noexcept + { + return hstring{ fmt::format(FMT_COMPILE(L"{}, {}"), ColorSchemeVM().Name(), SourceName()) }; + } + + DataTemplate ExtensionPackageTemplateSelector::SelectTemplateCore(const IInspectable& item, const DependencyObject& /*container*/) + { + return SelectTemplateCore(item); + } + + DataTemplate ExtensionPackageTemplateSelector::SelectTemplateCore(const IInspectable& item) + { + if (const auto extPkgVM = item.try_as()) + { + if (!extPkgVM.Package().DisplayName().empty()) + { + return ComplexTemplate(); + } + return DefaultTemplate(); + } + return nullptr; + } +} diff --git a/src/cascadia/TerminalSettingsEditor/Extensions.h b/src/cascadia/TerminalSettingsEditor/Extensions.h new file mode 100644 index 0000000000..6b7eafad57 --- /dev/null +++ b/src/cascadia/TerminalSettingsEditor/Extensions.h @@ -0,0 +1,193 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include "Extensions.g.h" +#include "ExtensionsViewModel.g.h" +#include "ExtensionPackageViewModel.g.h" +#include "FragmentExtensionViewModel.g.h" +#include "FragmentProfileViewModel.g.h" +#include "FragmentColorSchemeViewModel.g.h" +#include "ExtensionPackageTemplateSelector.g.h" +#include "ViewModelHelpers.h" +#include "Utils.h" + +namespace winrt::Microsoft::Terminal::Settings::Editor::implementation +{ + struct Extensions : public HasScrollViewer, ExtensionsT + { + public: + Windows::UI::Xaml::Thickness CalculateMargin(bool hidden); + + Extensions(); + + void OnNavigatedTo(const Windows::UI::Xaml::Navigation::NavigationEventArgs& e); + + void ExtensionNavigator_Click(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& args); + void NavigateToProfile_Click(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& args); + void NavigateToColorScheme_Click(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& args); + + WINRT_PROPERTY(Editor::ExtensionsViewModel, ViewModel, nullptr); + + private: + Editor::ExtensionPackageTemplateSelector _extensionPackageIdentifierTemplateSelector; + }; + + struct ExtensionsViewModel : ExtensionsViewModelT, ViewModelHelper + { + 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 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 LazyLoadExtensions(); + void UpdateSettings(const Model::CascadiaSettings& settings, const Editor::ColorSchemesPageViewModel& colorSchemesPageVM); + void NavigateToProfile(const guid profileGuid); + void NavigateToColorScheme(const Editor::ColorSchemeViewModel& schemeVM); + void MarkAsVisited(); + + static bool GetExtensionState(hstring extensionSource, const Model::CascadiaSettings& settings); + static void SetExtensionState(hstring extensionSource, const Model::CascadiaSettings& settings, bool enableExt); + + til::typed_event NavigateToProfileRequested; + til::typed_event 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 _extensionPackages; + Windows::Foundation::Collections::IObservableVector _profilesModifiedView; + Windows::Foundation::Collections::IObservableVector _profilesAddedView; + Windows::Foundation::Collections::IObservableVector _colorSchemesAddedView; + bool _extensionsLoaded; + + void _UpdateListViews(bool updateProfilesModified, bool updateProfilesAdded, bool updateColorSchemesAdded); + }; + + struct ExtensionPackageViewModel : ExtensionPackageViewModelT, ViewModelHelper + { + public: + ExtensionPackageViewModel(const Model::ExtensionPackage& pkg, const Model::CascadiaSettings& settings) : + _package{ pkg }, + _settings{ settings }, + _fragmentExtensions{ single_threaded_observable_vector() } {} + + static bool SortAscending(const Editor::ExtensionPackageViewModel& lhs, const Editor::ExtensionPackageViewModel& rhs); + + void UpdateSettings(const Model::CascadiaSettings& settings); + + Model::ExtensionPackage Package() const noexcept { return _package; } + hstring Scope() const noexcept; + bool Enabled() const; + void Enabled(bool val); + hstring AccessibleName() const noexcept; + Windows::Foundation::Collections::IObservableVector FragmentExtensions() { return _fragmentExtensions; } + + private: + Model::ExtensionPackage _package; + Model::CascadiaSettings _settings; + Windows::Foundation::Collections::IObservableVector _fragmentExtensions; + }; + + struct FragmentExtensionViewModel : FragmentExtensionViewModelT, ViewModelHelper + { + public: + FragmentExtensionViewModel(const Model::FragmentSettings& fragment, + std::vector& profilesModified, + std::vector& profilesAdded, + std::vector& 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 ProfilesModified() const noexcept { return _profilesModified.GetView(); } + Windows::Foundation::Collections::IVectorView ProfilesAdded() const noexcept { return _profilesAdded.GetView(); } + Windows::Foundation::Collections::IVectorView ColorSchemesAdded() const noexcept { return _colorSchemesAdded.GetView(); } + + private: + Model::FragmentSettings _fragment; + Windows::Foundation::Collections::IVector _profilesModified; + Windows::Foundation::Collections::IVector _profilesAdded; + Windows::Foundation::Collections::IVector _colorSchemesAdded; + }; + + struct FragmentProfileViewModel : FragmentProfileViewModelT, ViewModelHelper + { + public: + FragmentProfileViewModel(const Model::FragmentProfileEntry& entry, const Model::FragmentSettings& fragment, const Model::Profile& deducedProfile) : + _entry{ entry }, + _fragment{ fragment }, + _deducedProfile{ deducedProfile } {} + + static bool SortAscending(const Editor::FragmentProfileViewModel& lhs, const Editor::FragmentProfileViewModel& rhs); + + Model::Profile Profile() const { return _deducedProfile; }; + hstring SourceName() const { return _fragment.Source(); } + hstring Json() const { return _entry.Json(); } + hstring AccessibleName() const noexcept; + + private: + Model::FragmentProfileEntry _entry; + Model::FragmentSettings _fragment; + Model::Profile _deducedProfile; + }; + + struct FragmentColorSchemeViewModel : FragmentColorSchemeViewModelT, ViewModelHelper + { + public: + FragmentColorSchemeViewModel(const Model::FragmentColorSchemeEntry& entry, const Model::FragmentSettings& fragment, const Editor::ColorSchemeViewModel& deducedSchemeVM) : + _entry{ entry }, + _fragment{ fragment }, + _deducedSchemeVM{ deducedSchemeVM } {} + + static bool SortAscending(const Editor::FragmentColorSchemeViewModel& lhs, const Editor::FragmentColorSchemeViewModel& rhs); + + Editor::ColorSchemeViewModel ColorSchemeVM() const { return _deducedSchemeVM; }; + hstring SourceName() const { return _fragment.Source(); } + hstring Json() const { return _entry.Json(); } + hstring AccessibleName() const noexcept; + + private: + Model::FragmentColorSchemeEntry _entry; + Model::FragmentSettings _fragment; + Editor::ColorSchemeViewModel _deducedSchemeVM; + }; + + struct ExtensionPackageTemplateSelector : public ExtensionPackageTemplateSelectorT + { + 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 new file mode 100644 index 0000000000..4dc3018c36 --- /dev/null +++ b/src/cascadia/TerminalSettingsEditor/Extensions.idl @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import "ColorSchemesPageViewModel.idl"; + +namespace Microsoft.Terminal.Settings.Editor +{ + [default_interface] runtimeclass Extensions : Windows.UI.Xaml.Controls.Page + { + Extensions(); + ExtensionsViewModel ViewModel { get; }; + + Windows.UI.Xaml.Thickness CalculateMargin(Boolean hidden); + } + + [default_interface] runtimeclass ExtensionsViewModel : Windows.UI.Xaml.Data.INotifyPropertyChanged + { + // Properties + ExtensionPackageViewModel CurrentExtensionPackage; + Windows.UI.Xaml.DataTemplate CurrentExtensionPackageIdentifierTemplate { get; }; + Boolean IsExtensionView { get; }; + Boolean NoExtensionPackages { get; }; + Boolean NoProfilesModified { get; }; + Boolean NoProfilesAdded { get; }; + Boolean NoSchemesAdded { get; }; + Boolean DisplayBadge { get; }; + + // Views + IVector ExtensionPackages { get; }; + IObservableVector ProfilesModified { get; }; + IObservableVector ProfilesAdded { get; }; + IObservableVector ColorSchemesAdded { get; }; + + // Methods + void UpdateSettings(Microsoft.Terminal.Settings.Model.CascadiaSettings settings, ColorSchemesPageViewModel colorSchemesPageVM); + + event Windows.Foundation.TypedEventHandler NavigateToProfileRequested; + event Windows.Foundation.TypedEventHandler NavigateToColorSchemeRequested; + } + + [default_interface] runtimeclass ExtensionPackageViewModel : Windows.UI.Xaml.Data.INotifyPropertyChanged + { + Microsoft.Terminal.Settings.Model.ExtensionPackage Package { get; }; + Boolean Enabled; + String Scope { get; }; + String AccessibleName { get; }; + IVector FragmentExtensions { get; }; + } + + [default_interface] runtimeclass FragmentExtensionViewModel : Windows.UI.Xaml.Data.INotifyPropertyChanged + { + Microsoft.Terminal.Settings.Model.FragmentSettings Fragment { get; }; + IVectorView ProfilesModified { get; }; + IVectorView ProfilesAdded { get; }; + IVectorView 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; + } +} diff --git a/src/cascadia/TerminalSettingsEditor/Extensions.xaml b/src/cascadia/TerminalSettingsEditor/Extensions.xaml new file mode 100644 index 0000000000..eec3e69290 --- /dev/null +++ b/src/cascadia/TerminalSettingsEditor/Extensions.xaml @@ -0,0 +1,496 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/cascadia/TerminalSettingsEditor/MainPage.cpp b/src/cascadia/TerminalSettingsEditor/MainPage.cpp index 6e66fac02a..098827d9a9 100644 --- a/src/cascadia/TerminalSettingsEditor/MainPage.cpp +++ b/src/cascadia/TerminalSettingsEditor/MainPage.cpp @@ -9,6 +9,7 @@ #include "Compatibility.h" #include "Rendering.h" #include "RenderingViewModel.h" +#include "Extensions.h" #include "Actions.h" #include "ProfileViewModel.h" #include "GlobalAppearance.h" @@ -45,6 +46,7 @@ static const std::wstring_view renderingTag{ L"Rendering_Nav" }; static const std::wstring_view compatibilityTag{ L"Compatibility_Nav" }; static const std::wstring_view actionsTag{ L"Actions_Nav" }; static const std::wstring_view newTabMenuTag{ L"NewTabMenu_Nav" }; +static const std::wstring_view extensionsTag{ L"Extensions_Nav" }; static const std::wstring_view globalProfileTag{ L"GlobalProfile_Nav" }; static const std::wstring_view addProfileTag{ L"AddProfile" }; static const std::wstring_view colorSchemesTag{ L"ColorSchemes_Nav" }; @@ -112,6 +114,33 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation } }); + auto extensionsVMImpl = winrt::make_self(_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(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(box_value(extensionsTag), RS_(L"Nav_Extensions/Content"), BreadcrumbSubPage::None); + _breadcrumbs.Append(crumb); + } + contentFrame().Navigate(xaml_typename(), _extensionsVM); + } + }); + // Make sure to initialize the profiles _after_ we have initialized the color schemes page VM, because we pass // that VM into the appearance VMs within the profiles _InitializeProfilesList(); @@ -162,6 +191,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation // Update the Nav State with the new version of the settings _colorSchemesPageVM.UpdateSettings(_settingsClone); _newTabMenuPageVM.UpdateSettings(_settingsClone); + _extensionsVM.UpdateSettings(_settingsClone, _colorSchemesPageVM); // We'll update the profile in the _profilesNavState whenever we actually navigate to one @@ -183,7 +213,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { // found the one that was selected before the refresh SettingsNav().SelectedItem(item); - _Navigate(*stringTag, crumb->SubPage()); + _Navigate(*breadcrumbStringTag, crumb->SubPage()); return; } } @@ -198,6 +228,17 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation return; } } + else if (const auto& breadcrumbExtensionPackage{ crumb->Tag().try_as() }) + { + if (stringTag == extensionsTag) + { + // navigate to the Extensions page, + // _Navigate() will handle trying to find the right subpage + SettingsNav().SelectedItem(item); + _Navigate(breadcrumbExtensionPackage, BreadcrumbSubPage::Extensions_Extension); + return; + } + } } else if (const auto& profileTag{ tag.try_as() }) { @@ -457,6 +498,21 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation _breadcrumbs.Append(crumb); } } + else if (clickedItemTag == extensionsTag) + { + if (_extensionsVM.CurrentExtensionPackage()) + { + // Setting CurrentExtensionPackage triggers the PropertyChanged event, + // which will navigate to the correct page and update the breadcrumbs appropriately + _extensionsVM.CurrentExtensionPackage(nullptr); + } + else + { + contentFrame().Navigate(xaml_typename(), _extensionsVM); + const auto crumb = winrt::make(box_value(clickedItemTag), RS_(L"Nav_Extensions/Content"), BreadcrumbSubPage::None); + _breadcrumbs.Append(crumb); + } + } else if (clickedItemTag == globalProfileTag) { auto profileVM{ _viewModelForProfile(_settingsClone.ProfileDefaults(), _settingsClone, Dispatcher()) }; @@ -587,6 +643,40 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation } } + void MainPage::_Navigate(const Editor::ExtensionPackageViewModel& extPkgVM, BreadcrumbSubPage subPage) + { + _PreNavigateHelper(); + + contentFrame().Navigate(xaml_typename(), _extensionsVM); + const auto crumb = winrt::make(box_value(extensionsTag), RS_(L"Nav_Extensions/Content"), BreadcrumbSubPage::None); + _breadcrumbs.Append(crumb); + + if (subPage == BreadcrumbSubPage::None) + { + _extensionsVM.CurrentExtensionPackage(nullptr); + } + else + { + bool found = false; + for (const auto& pkgVM : _extensionsVM.ExtensionPackages()) + { + if (pkgVM.Package().Source() == extPkgVM.Package().Source()) + { + // Take advantage of the PropertyChanged event to navigate + // to the correct extension package and build the breadcrumbs as we go + _extensionsVM.CurrentExtensionPackage(pkgVM); + found = true; + break; + } + } + if (!found) + { + // If we couldn't find a reasonable match, just go back to the root + _extensionsVM.CurrentExtensionPackage(nullptr); + } + } + } + void MainPage::SaveButton_Click(const IInspectable& /*sender*/, const RoutedEventArgs& /*args*/) { _settingsClone.LogSettingChanges(false); @@ -612,6 +702,10 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { _Navigate(*ntmEntryViewModel, subPage); } + else if (const auto extPkgViewModel = tag.try_as()) + { + _Navigate(*extPkgViewModel, subPage); + } else { _Navigate(tag.as(), subPage); @@ -809,6 +903,35 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation return _breadcrumbs; } + void MainPage::_NavigateToProfileHandler(const IInspectable& /*sender*/, winrt::guid profileGuid) + { + for (auto&& menuItem : _menuItemSource) + { + if (const auto& navViewItem{ menuItem.try_as() }) + { + if (const auto& tag{ navViewItem.Tag() }) + { + if (const auto& profileTag{ tag.try_as() }) + { + 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(); diff --git a/src/cascadia/TerminalSettingsEditor/MainPage.h b/src/cascadia/TerminalSettingsEditor/MainPage.h index 582f8813b1..aebeb516e3 100644 --- a/src/cascadia/TerminalSettingsEditor/MainPage.h +++ b/src/cascadia/TerminalSettingsEditor/MainPage.h @@ -43,6 +43,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation winrt::Windows::UI::Xaml::Media::Brush BackgroundBrush(); Windows::Foundation::Collections::IObservableVector Breadcrumbs() noexcept; + Editor::ExtensionsViewModel ExtensionsVM() const noexcept { return _extensionsVM; } til::typed_event OpenJson; @@ -68,16 +69,21 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation void _Navigate(hstring clickedItemTag, BreadcrumbSubPage subPage); void _Navigate(const Editor::ProfileViewModel& profile, BreadcrumbSubPage subPage); void _Navigate(const Editor::NewTabMenuEntryViewModel& ntmEntryVM, BreadcrumbSubPage subPage); + void _Navigate(const Editor::ExtensionPackageViewModel& extPkgVM, BreadcrumbSubPage subPage); + void _NavigateToProfileHandler(const IInspectable& sender, winrt::guid profileGuid); + void _NavigateToColorSchemeHandler(const IInspectable& sender, const IInspectable& args); void _UpdateBackgroundForMica(); void _MoveXamlParsedNavItemsIntoItemSource(); winrt::Microsoft::Terminal::Settings::Editor::ColorSchemesPageViewModel _colorSchemesPageVM{ nullptr }; winrt::Microsoft::Terminal::Settings::Editor::NewTabMenuViewModel _newTabMenuPageVM{ nullptr }; + winrt::Microsoft::Terminal::Settings::Editor::ExtensionsViewModel _extensionsVM{ nullptr }; Windows::UI::Xaml::Data::INotifyPropertyChanged::PropertyChanged_revoker _profileViewModelChangedRevoker; Windows::UI::Xaml::Data::INotifyPropertyChanged::PropertyChanged_revoker _colorSchemesPageViewModelChangedRevoker; Windows::UI::Xaml::Data::INotifyPropertyChanged::PropertyChanged_revoker _ntmViewModelChangedRevoker; + Windows::UI::Xaml::Data::INotifyPropertyChanged::PropertyChanged_revoker _extensionsViewModelChangedRevoker; }; } diff --git a/src/cascadia/TerminalSettingsEditor/MainPage.idl b/src/cascadia/TerminalSettingsEditor/MainPage.idl index 0f5a6e49b8..390ae5cb80 100644 --- a/src/cascadia/TerminalSettingsEditor/MainPage.idl +++ b/src/cascadia/TerminalSettingsEditor/MainPage.idl @@ -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 Breadcrumbs { get; }; + ExtensionsViewModel ExtensionsVM { get; }; Windows.UI.Xaml.Media.Brush BackgroundBrush { get; }; } diff --git a/src/cascadia/TerminalSettingsEditor/MainPage.xaml b/src/cascadia/TerminalSettingsEditor/MainPage.xaml index c6d36e8f20..9e23093f73 100644 --- a/src/cascadia/TerminalSettingsEditor/MainPage.xaml +++ b/src/cascadia/TerminalSettingsEditor/MainPage.xaml @@ -120,7 +120,8 @@ - @@ -155,6 +156,17 @@ + + + + + + + + + NewTabMenuViewModel.idl Code + + Extensions.xaml + Code + Profiles_Base.xaml Code @@ -199,6 +203,9 @@ Designer + + Designer + Designer @@ -309,6 +316,10 @@ NewTabMenuViewModel.idl Code + + Extensions.xaml + Code + Profiles_Base.xaml Code @@ -408,6 +419,10 @@ + + Extensions.xaml + Code + Profiles_Base.xaml Code diff --git a/src/cascadia/TerminalSettingsEditor/NewTabMenu.cpp b/src/cascadia/TerminalSettingsEditor/NewTabMenu.cpp index 901b809f34..25b7f8ef77 100644 --- a/src/cascadia/TerminalSettingsEditor/NewTabMenu.cpp +++ b/src/cascadia/TerminalSettingsEditor/NewTabMenu.cpp @@ -22,8 +22,6 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { InitializeComponent(); - _entryTemplateSelector = Resources().Lookup(box_value(L"NewTabMenuEntryTemplateSelector")).as(); - // 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). diff --git a/src/cascadia/TerminalSettingsEditor/NewTabMenu.h b/src/cascadia/TerminalSettingsEditor/NewTabMenu.h index cb54fd11b9..f305c44c88 100644 --- a/src/cascadia/TerminalSettingsEditor/NewTabMenu.h +++ b/src/cascadia/TerminalSettingsEditor/NewTabMenu.h @@ -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); diff --git a/src/cascadia/TerminalSettingsEditor/NewTabMenu.xaml b/src/cascadia/TerminalSettingsEditor/NewTabMenu.xaml index 54acab56e1..fb3273a969 100644 --- a/src/cascadia/TerminalSettingsEditor/NewTabMenu.xaml +++ b/src/cascadia/TerminalSettingsEditor/NewTabMenu.xaml @@ -321,7 +321,7 @@ - + diff --git a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw index 0396faf3bc..7949d017ca 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw @@ -684,6 +684,10 @@ Actions 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. + + Extensions + Header for the "extensions" menu item. This navigates to a page that lets you see and modify extensions for the app. + Background opacity 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. @@ -2344,6 +2348,49 @@ This option is managed by enterprise policy and cannot be changed here. This is displayed in concordance with Globals_StartOnUserLogin if the enterprise administrator has taken control of this setting. + + Active Extensions + + + Modified Profiles + + + Added Profiles + + + Added Color Schemes + + + Learn more about extensions + + + Navigate to profile + + + Navigate to profile + + + Navigate to color scheme + + + Navigate to color scheme + + + Current User + Label for the installation scope of an extension. + + + All Users + Label for the installation scope of an extension + + + Scope + Header for the installation scope of the extension + + + NEW + Text is used on an info badge for new navigation items. Must be all caps. + Learn more about regular expressions diff --git a/src/cascadia/TerminalSettingsEditor/SettingContainerStyle.xaml b/src/cascadia/TerminalSettingsEditor/SettingContainerStyle.xaml index c4d3c7a580..e5ac30b9bb 100644 --- a/src/cascadia/TerminalSettingsEditor/SettingContainerStyle.xaml +++ b/src/cascadia/TerminalSettingsEditor/SettingContainerStyle.xaml @@ -134,6 +134,7 @@ + + - @@ -228,6 +234,45 @@ + + + FindFragmentsAndMergeIntoUserSettings must be called after MergeInboxIntoUserSettings. - loader.FindFragmentsAndMergeIntoUserSettings(); + loader.FindFragmentsAndMergeIntoUserSettings(false /*generateExtensionPackages*/); loader.FinalizeLayering(); // DisableDeletedProfiles returns true whenever we encountered any new generated/dynamic profiles. diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp index 8a699eb865..55af170355 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp @@ -92,6 +92,14 @@ winrt::com_ptr GlobalAppSettings::Copy() const globals->_NewTabMenu->Append(get_self(entry)->Copy()); } } + if (_DisabledProfileSources) + { + globals->_DisabledProfileSources = winrt::single_threaded_vector(); + for (const auto& src : *_DisabledProfileSources) + { + globals->_DisabledProfileSources->Append(src); + } + } for (const auto& parent : _parents) { diff --git a/src/cascadia/TerminalSettingsModel/IDynamicProfileGenerator.h b/src/cascadia/TerminalSettingsModel/IDynamicProfileGenerator.h index db57944b10..9144bcb325 100644 --- a/src/cascadia/TerminalSettingsModel/IDynamicProfileGenerator.h +++ b/src/cascadia/TerminalSettingsModel/IDynamicProfileGenerator.h @@ -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>& profiles) const = 0; }; }; diff --git a/src/cascadia/TerminalSettingsModel/PowershellCoreProfileGenerator.cpp b/src/cascadia/TerminalSettingsModel/PowershellCoreProfileGenerator.cpp index 14878c8278..01e4c491bd 100644 --- a/src/cascadia/TerminalSettingsModel/PowershellCoreProfileGenerator.cpp +++ b/src/cascadia/TerminalSettingsModel/PowershellCoreProfileGenerator.cpp @@ -15,12 +15,14 @@ #include #include #include +#include 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: diff --git a/src/cascadia/TerminalSettingsModel/PowershellCoreProfileGenerator.h b/src/cascadia/TerminalSettingsModel/PowershellCoreProfileGenerator.h index eaec9dacea..d473292e15 100644 --- a/src/cascadia/TerminalSettingsModel/PowershellCoreProfileGenerator.h +++ b/src/cascadia/TerminalSettingsModel/PowershellCoreProfileGenerator.h @@ -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>& profiles) const override; }; }; diff --git a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw index b1c24a54d0..a0126304df 100644 --- a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw @@ -1,17 +1,17 @@ - @@ -740,4 +740,24 @@ Open current working directory - + + WSL Distribution Profile Generator + The display name of a dynamic profile generator for WSL distros + + + PowerShell Profile Generator + The display name of a dynamic profile generator for PowerShell + + + Azure Cloud Shell Profile Generator + The display name of a dynamic profile generator for Azure Cloud Shell + + + Visual Studio Profile Generator + The display name of a dynamic profile generator for Visual Studio + + + SSH Host Profile Generator + The display name of a dynamic profile generator for SSH hosts + + \ No newline at end of file diff --git a/src/cascadia/TerminalSettingsModel/SshHostGenerator.cpp b/src/cascadia/TerminalSettingsModel/SshHostGenerator.cpp index 30dc19039f..45711c711e 100644 --- a/src/cascadia/TerminalSettingsModel/SshHostGenerator.cpp +++ b/src/cascadia/TerminalSettingsModel/SshHostGenerator.cpp @@ -7,11 +7,13 @@ #include "../../inc/DefaultSettings.h" #include "DynamicProfileUtils.h" +#include 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: diff --git a/src/cascadia/TerminalSettingsModel/SshHostGenerator.h b/src/cascadia/TerminalSettingsModel/SshHostGenerator.h index a355b0c430..d83e62535b 100644 --- a/src/cascadia/TerminalSettingsModel/SshHostGenerator.h +++ b/src/cascadia/TerminalSettingsModel/SshHostGenerator.h @@ -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>& profiles) const override; private: diff --git a/src/cascadia/TerminalSettingsModel/VisualStudioGenerator.cpp b/src/cascadia/TerminalSettingsModel/VisualStudioGenerator.cpp index 2fcfb1cc52..69feb6174e 100644 --- a/src/cascadia/TerminalSettingsModel/VisualStudioGenerator.cpp +++ b/src/cascadia/TerminalSettingsModel/VisualStudioGenerator.cpp @@ -6,16 +6,28 @@ #include "VisualStudioGenerator.h" #include "VsDevCmdGenerator.h" #include "VsDevShellGenerator.h" +#include 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>& profiles) const { const auto instances = VsSetupConfiguration::QueryInstances(); diff --git a/src/cascadia/TerminalSettingsModel/VisualStudioGenerator.h b/src/cascadia/TerminalSettingsModel/VisualStudioGenerator.h index ef36284db7..c89e7c6301 100644 --- a/src/cascadia/TerminalSettingsModel/VisualStudioGenerator.h +++ b/src/cascadia/TerminalSettingsModel/VisualStudioGenerator.h @@ -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>& profiles) const override; class IVisualStudioProfileGenerator diff --git a/src/cascadia/TerminalSettingsModel/WslDistroGenerator.cpp b/src/cascadia/TerminalSettingsModel/WslDistroGenerator.cpp index cb1d220a90..2e0c81e5a5 100644 --- a/src/cascadia/TerminalSettingsModel/WslDistroGenerator.cpp +++ b/src/cascadia/TerminalSettingsModel/WslDistroGenerator.cpp @@ -8,10 +8,13 @@ #include "../../inc/DefaultSettings.h" #include "DynamicProfileUtils.h" +#include 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 makeProfile(const std::wstring& distName) { const auto WSLDistro{ CreateDynamicProfile(distName) }; @@ -65,7 +78,7 @@ static winrt::com_ptr 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; } diff --git a/src/cascadia/TerminalSettingsModel/WslDistroGenerator.h b/src/cascadia/TerminalSettingsModel/WslDistroGenerator.h index b46aac8203..123734523f 100644 --- a/src/cascadia/TerminalSettingsModel/WslDistroGenerator.h +++ b/src/cascadia/TerminalSettingsModel/WslDistroGenerator.h @@ -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>& profiles) const override; }; };