// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. #include "pch.h" #include "MainPage.h" #include "MainPage.g.cpp" #include "Launch.h" #include "Interaction.h" #include "Compatibility.h" #include "Rendering.h" #include "RenderingViewModel.h" #include "Actions.h" #include "ProfileViewModel.h" #include "GlobalAppearance.h" #include "GlobalAppearanceViewModel.h" #include "ColorSchemes.h" #include "AddProfile.h" #include "InteractionViewModel.h" #include "LaunchViewModel.h" #include "NewTabMenuViewModel.h" #include "..\types\inc\utils.hpp" #include <..\WinRTUtils\inc\Utils.h> #include #include namespace winrt { namespace MUX = Microsoft::UI::Xaml; namespace WUX = Windows::UI::Xaml; } using namespace winrt::Windows::Foundation; using namespace winrt::Windows::UI::Xaml; using namespace winrt::Microsoft::Terminal::Settings::Model; using namespace winrt::Windows::UI::Core; using namespace winrt::Windows::System; using namespace winrt::Windows::UI::Xaml::Controls; using namespace winrt::Windows::Foundation::Collections; static const std::wstring_view openJsonTag{ L"OpenJson_Nav" }; static const std::wstring_view launchTag{ L"Launch_Nav" }; static const std::wstring_view interactionTag{ L"Interaction_Nav" }; 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 globalProfileTag{ L"GlobalProfile_Nav" }; static const std::wstring_view addProfileTag{ L"AddProfile" }; static const std::wstring_view colorSchemesTag{ L"ColorSchemes_Nav" }; static const std::wstring_view globalAppearanceTag{ L"GlobalAppearance_Nav" }; namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { static Editor::ProfileViewModel _viewModelForProfile(const Model::Profile& profile, const Model::CascadiaSettings& appSettings, const Windows::UI::Core::CoreDispatcher& dispatcher) { return winrt::make(profile, appSettings, dispatcher); } MainPage::MainPage(const CascadiaSettings& settings) : _settingsSource{ settings }, _settingsClone{ settings.Copy() } { InitializeComponent(); _UpdateBackgroundForMica(); _newTabMenuPageVM = winrt::make(_settingsClone); _ntmViewModelChangedRevoker = _newTabMenuPageVM.PropertyChanged(winrt::auto_revoke, [this](auto&&, const PropertyChangedEventArgs& args) { const auto settingName{ args.PropertyName() }; if (settingName == L"CurrentFolder") { if (const auto& currentFolder = _newTabMenuPageVM.CurrentFolder()) { const auto crumb = winrt::make(box_value(currentFolder), currentFolder.Name(), BreadcrumbSubPage::NewTabMenu_Folder); _breadcrumbs.Append(crumb); SettingsMainPage_ScrollViewer().ScrollToVerticalOffset(0); } else { // If we don't have a current folder, we're at the root of the NTM _breadcrumbs.Clear(); const auto crumb = winrt::make(box_value(newTabMenuTag), RS_(L"Nav_NewTabMenu/Content"), BreadcrumbSubPage::None); _breadcrumbs.Append(crumb); } contentFrame().Navigate(xaml_typename(), _newTabMenuPageVM); } }); _colorSchemesPageVM = winrt::make(_settingsClone); _colorSchemesPageViewModelChangedRevoker = _colorSchemesPageVM.PropertyChanged(winrt::auto_revoke, [=](auto&&, const PropertyChangedEventArgs& args) { const auto settingName{ args.PropertyName() }; if (settingName == L"CurrentPage") { const auto currentScheme = _colorSchemesPageVM.CurrentScheme(); if (_colorSchemesPageVM.CurrentPage() == ColorSchemesSubPage::EditColorScheme && currentScheme) { contentFrame().Navigate(xaml_typename(), currentScheme); const auto crumb = winrt::make(box_value(colorSchemesTag), currentScheme.Name(), BreadcrumbSubPage::ColorSchemes_Edit); _breadcrumbs.Append(crumb); } else if (_colorSchemesPageVM.CurrentPage() == ColorSchemesSubPage::Base) { _Navigate(winrt::hstring{ colorSchemesTag }, BreadcrumbSubPage::None); } } else if (settingName == L"CurrentSchemeName") { // this is not technically a setting, it is the ColorSchemesPageVM telling us the breadcrumb item needs to be updated because of a rename _breadcrumbs.RemoveAtEnd(); const auto crumb = winrt::make(box_value(colorSchemesTag), _colorSchemesPageVM.CurrentScheme().Name(), BreadcrumbSubPage::ColorSchemes_Edit); _breadcrumbs.Append(crumb); } }); // 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(); Automation::AutomationProperties::SetHelpText(SaveButton(), RS_(L"Settings_SaveSettingsButton/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip")); Automation::AutomationProperties::SetHelpText(ResetButton(), RS_(L"Settings_ResetSettingsButton/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip")); Automation::AutomationProperties::SetHelpText(OpenJsonNavItem(), RS_(L"Nav_OpenJSON/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip")); _breadcrumbs = single_threaded_observable_vector(); } // Method Description: // - Update the Settings UI with a new CascadiaSettings to bind to // Arguments: // - settings - the new settings source // Return value: // - void MainPage::UpdateSettings(const Model::CascadiaSettings& settings) { _settingsSource = settings; _settingsClone = settings.Copy(); _UpdateBackgroundForMica(); // Deduce information about the currently selected item IInspectable lastBreadcrumb; const auto size = _breadcrumbs.Size(); if (size > 0) { lastBreadcrumb = _breadcrumbs.GetAt(size - 1); } // Collect only the first items out of the menu item source, the static // ones that we don't want to regenerate. // // By manipulating a MenuItemsSource this way, rather than manipulating the // MenuItems directly, we avoid a crash in WinUI. // // By making the vector only _originalNumItems big to start, GetMany // will only fill that number of elements out of the current source. std::vector menuItemsSTL(_originalNumItems, nullptr); _menuItemSource.GetMany(0, menuItemsSTL); // now, just stick them back in. _menuItemSource.ReplaceAll(menuItemsSTL); // Repopulate profile-related menu items _InitializeProfilesList(); // Update the Nav State with the new version of the settings _colorSchemesPageVM.UpdateSettings(_settingsClone); _newTabMenuPageVM.UpdateSettings(_settingsClone); // We'll update the profile in the _profilesNavState whenever we actually navigate to one // now that the menuItems are repopulated, // refresh the current page using the breadcrumb data we collected before the refresh if (const auto& crumb{ lastBreadcrumb.try_as() }; crumb && crumb->Tag()) { for (const auto& item : _menuItemSource) { if (const auto& menuItem{ item.try_as() }) { if (const auto& tag{ menuItem.Tag() }) { if (const auto& stringTag{ tag.try_as() }) { if (const auto& breadcrumbStringTag{ crumb->Tag().try_as() }) { if (stringTag == breadcrumbStringTag) { // found the one that was selected before the refresh SettingsNav().SelectedItem(item); _Navigate(*stringTag, crumb->SubPage()); return; } } else if (const auto& breadcrumbFolderEntry{ crumb->Tag().try_as() }) { if (stringTag == newTabMenuTag) { // navigate to the NewTabMenu page, // _Navigate() will handle trying to find the right subpage SettingsNav().SelectedItem(item); _Navigate(breadcrumbFolderEntry, BreadcrumbSubPage::NewTabMenu_Folder); return; } } } else if (const auto& profileTag{ tag.try_as() }) { if (const auto& breadcrumbProfileTag{ crumb->Tag().try_as() }) { if (profileTag->OriginalProfileGuid() == breadcrumbProfileTag->OriginalProfileGuid()) { // found the one that was selected before the refresh SettingsNav().SelectedItem(item); _Navigate(*profileTag, crumb->SubPage()); return; } } } } } } } // Couldn't find the selected item, fallback to first menu item // This happens when the selected item was a profile which doesn't exist in the new configuration // We can use menuItemsSTL here because the only things they miss are profile entries. const auto& firstItem{ _menuItemSource.GetAt(0).as() }; SettingsNav().SelectedItem(firstItem); _Navigate(unbox_value(firstItem.Tag()), BreadcrumbSubPage::None); } void MainPage::SetHostingWindow(uint64_t hostingWindow) noexcept { _hostingHwnd.emplace(reinterpret_cast(hostingWindow)); // Now that we have a HWND, update our own BG to account for if that // window is using mica or not. _UpdateBackgroundForMica(); } bool MainPage::TryPropagateHostingWindow(IInspectable object) noexcept { if (_hostingHwnd) { if (auto initializeWithWindow{ object.try_as() }) { return SUCCEEDED(initializeWithWindow->Initialize(*_hostingHwnd)); } } return false; } // Method Description: // - Creates a new profile and navigates to it in the Settings UI // Arguments: // - profileGuid: the guid of the profile we want to duplicate, // can be empty to indicate that we should create a fresh profile void MainPage::_AddProfileHandler(winrt::guid profileGuid) { uint32_t insertIndex; auto selectedItem{ SettingsNav().SelectedItem() }; if (_menuItemSource) { _menuItemSource.IndexOf(selectedItem, insertIndex); } if (profileGuid != winrt::guid{}) { // if we were given a non-empty guid, we want to duplicate the corresponding profile const auto profile = _settingsClone.FindProfile(profileGuid); if (profile) { const auto duplicated = _settingsClone.DuplicateProfile(profile); _CreateAndNavigateToNewProfile(insertIndex, duplicated); } } else { // we were given an empty guid, create a new profile _CreateAndNavigateToNewProfile(insertIndex, nullptr); } } uint64_t MainPage::GetHostingWindow() const noexcept { return reinterpret_cast(_hostingHwnd.value_or(nullptr)); } // Function Description: // - Called when the NavigationView is loaded. Navigates to the first item in the NavigationView, if no item is selected // Arguments: // - // Return Value: // - void MainPage::SettingsNav_Loaded(const IInspectable&, const RoutedEventArgs&) { if (SettingsNav().SelectedItem() == nullptr) { const auto initialItem = SettingsNav().MenuItems().GetAt(0); SettingsNav().SelectedItem(initialItem); // Manually navigate because setting the selected item programmatically doesn't trigger ItemInvoked. if (const auto tag = initialItem.as().Tag()) { _Navigate(unbox_value(tag), BreadcrumbSubPage::None); } } } // Function Description: // - Called when NavigationView items are invoked. Navigates to the corresponding page. // Arguments: // - args - additional event info from invoking the NavViewItem // Return Value: // - void MainPage::SettingsNav_ItemInvoked(const MUX::Controls::NavigationView&, const MUX::Controls::NavigationViewItemInvokedEventArgs& args) { if (const auto clickedItemContainer = args.InvokedItemContainer()) { if (clickedItemContainer.IsSelected()) { // Clicked on the selected item. // Don't navigate to the same page again. return; } else { // If we are navigating to a new page, scroll to the top SettingsMainPage_ScrollViewer().ScrollToVerticalOffset(0); } if (const auto navString = clickedItemContainer.Tag().try_as()) { if (*navString == openJsonTag) { const auto window = CoreWindow::GetForCurrentThread(); const auto rAltState = window.GetKeyState(VirtualKey::RightMenu); const auto lAltState = window.GetKeyState(VirtualKey::LeftMenu); const auto altPressed = WI_IsFlagSet(lAltState, CoreVirtualKeyStates::Down) || WI_IsFlagSet(rAltState, CoreVirtualKeyStates::Down); const auto target = altPressed ? SettingsTarget::DefaultsFile : SettingsTarget::SettingsFile; OpenJson.raise(nullptr, target); return; } _Navigate(*navString, BreadcrumbSubPage::None); } else if (const auto profile = clickedItemContainer.Tag().try_as()) { // Navigate to a page with the given profile _Navigate(profile, BreadcrumbSubPage::None); } } } void MainPage::_PreNavigateHelper() { _profileViewModelChangedRevoker.revoke(); _breadcrumbs.Clear(); } void MainPage::_SetupProfileEventHandling(const Editor::ProfileViewModel profile) { // Add an event handler to navigate to Profiles_Appearance or Profiles_Advanced // Some notes on this: // - At first we tried putting another frame inside Profiles.xaml and having that // frame default to showing Profiles_Base. This allowed the logic for navigation // to Profiles_Advanced/Profiles_Appearance to live within Profiles.cpp. // - However, the header for the SUI lives in MainPage.xaml (because that's where // the whole NavigationView is) and so the BreadcrumbBar needs to be in MainPage.xaml. // We decided that it's better for the owner of the BreadcrumbBar to also be responsible // for navigation, so the navigation to Profiles_Advanced/Profiles_Appearance from // Profiles_Base got moved here. // If this is the base layer, the breadcrumb tag should be the globalProfileTag instead of the // ProfileViewModel, because the navigation menu item for this profile is the globalProfileTag. // See MainPage::UpdateSettings for why this matters const auto breadcrumbTag = profile.IsBaseLayer() ? box_value(globalProfileTag) : box_value(profile); const auto breadcrumbText = profile.IsBaseLayer() ? RS_(L"Nav_ProfileDefaults/Content") : profile.Name(); _profileViewModelChangedRevoker = profile.PropertyChanged(winrt::auto_revoke, [=](auto&&, const PropertyChangedEventArgs& args) { const auto settingName{ args.PropertyName() }; if (settingName == L"CurrentPage") { const auto currentPage = profile.CurrentPage(); if (currentPage == ProfileSubPage::Base) { contentFrame().Navigate(xaml_typename(), winrt::make(profile, *this)); _breadcrumbs.Clear(); const auto crumb = winrt::make(breadcrumbTag, breadcrumbText, BreadcrumbSubPage::None); _breadcrumbs.Append(crumb); } else if (currentPage == ProfileSubPage::Appearance) { contentFrame().Navigate(xaml_typename(), winrt::make(profile, *this)); const auto crumb = winrt::make(breadcrumbTag, RS_(L"Profile_Appearance/Header"), BreadcrumbSubPage::Profile_Appearance); _breadcrumbs.Append(crumb); SettingsMainPage_ScrollViewer().ScrollToVerticalOffset(0); } else if (currentPage == ProfileSubPage::Terminal) { contentFrame().Navigate(xaml_typename(), profile); const auto crumb = winrt::make(breadcrumbTag, RS_(L"Profile_Terminal/Header"), BreadcrumbSubPage::Profile_Terminal); _breadcrumbs.Append(crumb); SettingsMainPage_ScrollViewer().ScrollToVerticalOffset(0); } else if (currentPage == ProfileSubPage::Advanced) { contentFrame().Navigate(xaml_typename(), winrt::make(profile, *this)); const auto crumb = winrt::make(breadcrumbTag, RS_(L"Profile_Advanced/Header"), BreadcrumbSubPage::Profile_Advanced); _breadcrumbs.Append(crumb); SettingsMainPage_ScrollViewer().ScrollToVerticalOffset(0); } } }); } void MainPage::_Navigate(hstring clickedItemTag, BreadcrumbSubPage subPage) { _PreNavigateHelper(); if (clickedItemTag == launchTag) { contentFrame().Navigate(xaml_typename(), winrt::make(_settingsClone)); const auto crumb = winrt::make(box_value(clickedItemTag), RS_(L"Nav_Launch/Content"), BreadcrumbSubPage::None); _breadcrumbs.Append(crumb); } else if (clickedItemTag == interactionTag) { contentFrame().Navigate(xaml_typename(), winrt::make(_settingsClone.GlobalSettings())); const auto crumb = winrt::make(box_value(clickedItemTag), RS_(L"Nav_Interaction/Content"), BreadcrumbSubPage::None); _breadcrumbs.Append(crumb); } else if (clickedItemTag == renderingTag) { contentFrame().Navigate(xaml_typename(), winrt::make(_settingsClone)); const auto crumb = winrt::make(box_value(clickedItemTag), RS_(L"Nav_Rendering/Content"), BreadcrumbSubPage::None); _breadcrumbs.Append(crumb); } else if (clickedItemTag == compatibilityTag) { contentFrame().Navigate(xaml_typename(), winrt::make(_settingsClone.GlobalSettings())); const auto crumb = winrt::make(box_value(clickedItemTag), RS_(L"Nav_Compatibility/Content"), BreadcrumbSubPage::None); _breadcrumbs.Append(crumb); } else if (clickedItemTag == actionsTag) { contentFrame().Navigate(xaml_typename(), winrt::make(_settingsClone)); const auto crumb = winrt::make(box_value(clickedItemTag), RS_(L"Nav_Actions/Content"), BreadcrumbSubPage::None); _breadcrumbs.Append(crumb); } else if (clickedItemTag == newTabMenuTag) { if (_newTabMenuPageVM.CurrentFolder()) { // Setting CurrentFolder triggers the PropertyChanged event, // which will navigate to the correct page and update the breadcrumbs appropriately _newTabMenuPageVM.CurrentFolder(nullptr); } else { // Navigate to the NewTabMenu page contentFrame().Navigate(xaml_typename(), _newTabMenuPageVM); const auto crumb = winrt::make(box_value(clickedItemTag), RS_(L"Nav_NewTabMenu/Content"), BreadcrumbSubPage::None); _breadcrumbs.Append(crumb); } } else if (clickedItemTag == globalProfileTag) { auto profileVM{ _viewModelForProfile(_settingsClone.ProfileDefaults(), _settingsClone, Dispatcher()) }; profileVM.SetupAppearances(_colorSchemesPageVM.AllColorSchemes()); profileVM.IsBaseLayer(true); _SetupProfileEventHandling(profileVM); contentFrame().Navigate(xaml_typename(), winrt::make(profileVM, *this)); const auto crumb = winrt::make(box_value(clickedItemTag), RS_(L"Nav_ProfileDefaults/Content"), BreadcrumbSubPage::None); _breadcrumbs.Append(crumb); // If we were given a label, make sure we are on the correct sub-page if (subPage == BreadcrumbSubPage::Profile_Appearance) { profileVM.CurrentPage(ProfileSubPage::Appearance); } else if (subPage == BreadcrumbSubPage::Profile_Terminal) { profileVM.CurrentPage(ProfileSubPage::Terminal); } else if (subPage == BreadcrumbSubPage::Profile_Advanced) { profileVM.CurrentPage(ProfileSubPage::Advanced); } } else if (clickedItemTag == colorSchemesTag) { const auto crumb = winrt::make(box_value(clickedItemTag), RS_(L"Nav_ColorSchemes/Content"), BreadcrumbSubPage::None); _breadcrumbs.Append(crumb); contentFrame().Navigate(xaml_typename(), _colorSchemesPageVM); if (subPage == BreadcrumbSubPage::ColorSchemes_Edit) { _colorSchemesPageVM.CurrentPage(ColorSchemesSubPage::EditColorScheme); } } else if (clickedItemTag == globalAppearanceTag) { contentFrame().Navigate(xaml_typename(), winrt::make(_settingsClone.GlobalSettings())); const auto crumb = winrt::make(box_value(clickedItemTag), RS_(L"Nav_Appearance/Content"), BreadcrumbSubPage::None); _breadcrumbs.Append(crumb); } else if (clickedItemTag == addProfileTag) { auto addProfileState{ winrt::make(_settingsClone) }; addProfileState.AddNew({ get_weak(), &MainPage::_AddProfileHandler }); contentFrame().Navigate(xaml_typename(), addProfileState); const auto crumb = winrt::make(box_value(clickedItemTag), RS_(L"Nav_AddNewProfile/Content"), BreadcrumbSubPage::None); _breadcrumbs.Append(crumb); } } // Method Description: // - updates the content frame to present a view of the profile page // - NOTE: this does not update the selected item. // Arguments: // - profile - the profile object we are getting a view of void MainPage::_Navigate(const Editor::ProfileViewModel& profile, BreadcrumbSubPage subPage) { _PreNavigateHelper(); _SetupProfileEventHandling(profile); if (profile.Orphaned()) { contentFrame().Navigate(xaml_typename(), winrt::make(profile, *this)); const auto crumb = winrt::make(box_value(profile), profile.Name(), BreadcrumbSubPage::None); _breadcrumbs.Append(crumb); profile.CurrentPage(ProfileSubPage::Base); return; } contentFrame().Navigate(xaml_typename(), winrt::make(profile, *this)); const auto crumb = winrt::make(box_value(profile), profile.Name(), BreadcrumbSubPage::None); _breadcrumbs.Append(crumb); // Set the profile's 'CurrentPage' to the correct one, if this requires further navigation, the // event handler will do it if (subPage == BreadcrumbSubPage::None) { profile.CurrentPage(ProfileSubPage::Base); } else if (subPage == BreadcrumbSubPage::Profile_Appearance) { profile.CurrentPage(ProfileSubPage::Appearance); } else if (subPage == BreadcrumbSubPage::Profile_Terminal) { profile.CurrentPage(ProfileSubPage::Terminal); } else if (subPage == BreadcrumbSubPage::Profile_Advanced) { profile.CurrentPage(ProfileSubPage::Advanced); } } void MainPage::_Navigate(const Editor::NewTabMenuEntryViewModel& ntmEntryVM, BreadcrumbSubPage subPage) { _PreNavigateHelper(); contentFrame().Navigate(xaml_typename(), _newTabMenuPageVM); const auto crumb = winrt::make(box_value(newTabMenuTag), RS_(L"Nav_NewTabMenu/Content"), BreadcrumbSubPage::None); _breadcrumbs.Append(crumb); if (subPage == BreadcrumbSubPage::None) { _newTabMenuPageVM.CurrentFolder(nullptr); } else if (const auto& folderEntryVM = ntmEntryVM.try_as(); subPage == BreadcrumbSubPage::NewTabMenu_Folder && folderEntryVM) { // The given ntmEntryVM doesn't exist anymore since the whole tree had to be recreated. // Instead, let's look for a match by name and navigate to it. if (const auto& folderPath = _newTabMenuPageVM.FindFolderPathByName(folderEntryVM.Name()); folderPath.Size() > 0) { for (const auto& step : folderPath) { // Take advantage of the PropertyChanged event to navigate // to the correct folder and build the breadcrumbs as we go _newTabMenuPageVM.CurrentFolder(step); } } else { // If we couldn't find a reasonable match, just go back to the root _newTabMenuPageVM.CurrentFolder(nullptr); } } } void MainPage::SaveButton_Click(const IInspectable& /*sender*/, const RoutedEventArgs& /*args*/) { _settingsClone.LogSettingChanges(false); if (!_settingsClone.WriteSettingsToDisk()) { ShowLoadWarningsDialog.raise(*this, _settingsClone.Warnings()); } } void MainPage::ResetButton_Click(const IInspectable& /*sender*/, const RoutedEventArgs& /*args*/) { UpdateSettings(_settingsSource); } void MainPage::BreadcrumbBar_ItemClicked(const Microsoft::UI::Xaml::Controls::BreadcrumbBar& /*sender*/, const Microsoft::UI::Xaml::Controls::BreadcrumbBarItemClickedEventArgs& args) { if (gsl::narrow_cast(args.Index()) < (_breadcrumbs.Size() - 1)) { const auto tag = args.Item().as()->Tag(); const auto subPage = args.Item().as()->SubPage(); if (const auto profileViewModel = tag.try_as()) { _Navigate(*profileViewModel, subPage); } else if (const auto ntmEntryViewModel = tag.try_as()) { _Navigate(*ntmEntryViewModel, subPage); } else { _Navigate(tag.as(), subPage); } } } void MainPage::_InitializeProfilesList() { const auto& itemSource{ SettingsNav().MenuItemsSource() }; if (!itemSource) { // There wasn't a MenuItemsSource set yet? The only way that's // possible is if we haven't used // _MoveXamlParsedNavItemsIntoItemSource to move the hardcoded menu // entries from XAML into our runtime menu item source. Do that now. _MoveXamlParsedNavItemsIntoItemSource(); } // Manually create a NavigationViewItem for each profile // and keep a reference to them in a map so that we // can easily modify the correct one when the associated // profile changes. for (const auto& profile : _settingsClone.AllProfiles()) { if (!profile.Deleted()) { auto profileVM = _viewModelForProfile(profile, _settingsClone, Dispatcher()); profileVM.SetupAppearances(_colorSchemesPageVM.AllColorSchemes()); auto navItem = _CreateProfileNavViewItem(profileVM); _menuItemSource.Append(navItem); } } // Top off (the end of the nav view) with the Add Profile item MUX::Controls::NavigationViewItem addProfileItem; addProfileItem.Content(box_value(RS_(L"Nav_AddNewProfile/Content"))); addProfileItem.Tag(box_value(addProfileTag)); FontIcon icon; // This is the "Add" symbol icon.Glyph(L"\xE710"); addProfileItem.Icon(icon); _menuItemSource.Append(addProfileItem); } // BODGY // Does the very wacky business of moving all our MenuItems that we // hardcoded in XAML into a runtime MenuItemsSource. We'll then use _that_ // MenuItemsSource as the source for our nav view entries instead. This // lets us hardcode the initial entries in precompiled XAML, but then adjust // the items at runtime. Without using a MenuItemsSource, the NavView just // crashes when items are removed (see GH#13673) void MainPage::_MoveXamlParsedNavItemsIntoItemSource() { if (SettingsNav().MenuItemsSource()) { // We've already copied over the original items to a source. We can // just skip this now. return; } auto menuItems{ SettingsNav().MenuItems() }; _originalNumItems = menuItems.Size(); // Remove all the existing items, and move them to a separate vector // that we'll use as a MenuItemsSource. By doing this, we avoid a WinUI // bug (MUX#6302) where modifying the NavView.Items() directly causes a // crash. By leaving these static entries in XAML, we maintain the // benefit of instantiating them from the XBF, rather than at runtime. // // --> Copy it into an STL vector to simplify our code and reduce COM overhead. auto original = std::vector{ _originalNumItems, nullptr }; menuItems.GetMany(0, original); _menuItemSource = winrt::single_threaded_observable_vector(std::move(original)); SettingsNav().MenuItemsSource(_menuItemSource); } void MainPage::_CreateAndNavigateToNewProfile(const uint32_t index, const Model::Profile& profile) { const auto newProfile{ profile ? profile : _settingsClone.CreateNewProfile() }; const auto profileViewModel{ _viewModelForProfile(newProfile, _settingsClone, Dispatcher()) }; profileViewModel.SetupAppearances(_colorSchemesPageVM.AllColorSchemes()); const auto navItem{ _CreateProfileNavViewItem(profileViewModel) }; if (_menuItemSource) { _menuItemSource.InsertAt(index, navItem); } // Select and navigate to the new profile SettingsNav().SelectedItem(navItem); _Navigate(profileViewModel, BreadcrumbSubPage::None); } static MUX::Controls::InfoBadge _createGlyphIconBadge(wil::zwstring_view glyph) { MUX::Controls::InfoBadge badge; MUX::Controls::FontIconSource icon; icon.FontFamily(winrt::Windows::UI::Xaml::Media::FontFamily{ L"Segoe Fluent Icons, Segoe MDL2 Assets" }); icon.FontSize(12); icon.Glyph(glyph); badge.IconSource(icon); return badge; } MUX::Controls::NavigationViewItem MainPage::_CreateProfileNavViewItem(const Editor::ProfileViewModel& profile) { MUX::Controls::NavigationViewItem profileNavItem; profileNavItem.Content(box_value(profile.Name())); profileNavItem.Tag(box_value(profile)); profileNavItem.Icon(UI::IconPathConverter::IconWUX(profile.EvaluatedIcon())); if (profile.Orphaned()) { profileNavItem.InfoBadge(_createGlyphIconBadge(L"\xE7BA") /* Warning Triangle */); } else if (profile.Hidden()) { profileNavItem.InfoBadge(_createGlyphIconBadge(L"\xED1A") /* Hide */); } // Update the menu item when the icon/name changes auto weakMenuItem{ make_weak(profileNavItem) }; profile.PropertyChanged([weakMenuItem](const auto&, const WUX::Data::PropertyChangedEventArgs& args) { if (auto menuItem{ weakMenuItem.get() }) { const auto& tag{ menuItem.Tag().as() }; if (args.PropertyName() == L"Icon" || args.PropertyName() == L"EvaluatedIcon") { menuItem.Icon(UI::IconPathConverter::IconWUX(tag.EvaluatedIcon())); } else if (args.PropertyName() == L"Name") { menuItem.Content(box_value(tag.Name())); } else if (args.PropertyName() == L"Hidden") { menuItem.InfoBadge(tag.Hidden() ? _createGlyphIconBadge(L"\xED1A") /* Hide */ : nullptr); } } }); // Add an event handler for when the user wants to delete a profile. profile.DeleteProfileRequested({ this, &MainPage::_DeleteProfile }); return profileNavItem; } void MainPage::_DeleteProfile(const IInspectable /*sender*/, const Editor::DeleteProfileEventArgs& args) { // Delete profile from settings model const auto guid{ args.ProfileGuid() }; auto profileList{ _settingsClone.AllProfiles() }; for (uint32_t i = 0; i < profileList.Size(); ++i) { if (profileList.GetAt(i).Guid() == guid) { profileList.RemoveAt(i); break; } } // remove selected item uint32_t index; auto selectedItem{ SettingsNav().SelectedItem() }; if (_menuItemSource) { _menuItemSource.IndexOf(selectedItem, index); _menuItemSource.RemoveAt(index); // navigate to the profile next to this one const auto newSelectedItem{ _menuItemSource.GetAt(index < _menuItemSource.Size() - 1 ? index : index - 1) }; SettingsNav().SelectedItem(newSelectedItem); const auto newTag = newSelectedItem.as().Tag(); if (const auto profileViewModel = newTag.try_as()) { profileViewModel->FocusDeleteButton(true); _Navigate(*profileViewModel, BreadcrumbSubPage::None); } else { _Navigate(newTag.as(), BreadcrumbSubPage::None); } // Since we are navigating to a new profile after deletion, scroll up to the top SettingsMainPage_ScrollViewer().ChangeView(nullptr, 0.0, nullptr); } } IObservableVector MainPage::Breadcrumbs() noexcept { return _breadcrumbs; } winrt::Windows::UI::Xaml::Media::Brush MainPage::BackgroundBrush() { return SettingsNav().Background(); } // If the theme asks for Mica, then drop out our background, so that we // can have mica too. void MainPage::_UpdateBackgroundForMica() { // If we're in high contrast mode, don't override the theme. if (Windows::UI::ViewManagement::AccessibilitySettings accessibilitySettings; accessibilitySettings.HighContrast()) { return; } bool isMicaAvailable = false; // Check to see if our hosting window supports Mica at all. We'll check // to see if the window has Mica enabled - if it does, then we can // assume that it supports Mica. // // We're doing this instead of checking if we're on Windows build 22621 // or higher. if (_hostingHwnd.has_value()) { int attribute = DWMSBT_NONE; const auto hr = DwmGetWindowAttribute(*_hostingHwnd, DWMWA_SYSTEMBACKDROP_TYPE, &attribute, sizeof(attribute)); if (SUCCEEDED(hr)) { isMicaAvailable = attribute == DWMSBT_MAINWINDOW; } } const auto& theme = _settingsSource.GlobalSettings().CurrentTheme(); const bool hasThemeForSettings{ theme.Settings() != nullptr }; const auto& appTheme = theme.RequestedTheme(); const auto& requestedTheme = (hasThemeForSettings) ? theme.Settings().RequestedTheme() : appTheme; RequestedTheme(requestedTheme); // Mica gets it's appearance from the app's theme, not necessarily the // Page's theme. In the case of dark app, light settings, mica will be a // dark color, and the text will also be dark, making the UI _very_ hard // to read. (and similarly in the inverse situation). // // To mitigate this, don't set the transparent background in the case // that our theme is different than the app's. const bool actuallyUseMica = isMicaAvailable && (appTheme == requestedTheme); const auto bgKey = (theme.Window() != nullptr && theme.Window().UseMica()) && actuallyUseMica ? L"SettingsPageMicaBackground" : L"SettingsPageBackground"; // remember to use ThemeLookup to get the actual correct color for the // currently requested theme. if (const auto bgColor = ThemeLookup(Resources(), requestedTheme, winrt::box_value(bgKey))) { SettingsNav().Background(winrt::WUX::Media::SolidColorBrush(winrt::unbox_value(bgColor))); } } }