516 lines
23 KiB
C++

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