Add New Tab Menu Customization to Settings UI (#18015)

## Summary of the Pull Request
Adds customization for the New Tab Menu to the settings UI.

- Settings Model changes:
- The Settings UI generally works by creating a copy of the entire
settings model objects on which we apply the changes to. Turns out, we
completely left the NewTabMenu out of that process. So I went ahead and
implemented it.
   -  `FolderEntry`
- `FolderEntry` exposes `Entries()` (used by the new tab menu to figure
out what to actually render) and `RawEntries()` (the actual JSON data
deserialized into settings model objects). I went ahead and exposed
`RawEntries()` since we'll need to apply changes to it to then
serialize.
- View Model:
- `NewTabMenuViewModel` is the main view model that interacts with the
page. It maintains the current view of items and applies changes to the
settings model.
- `NewTabMenuEntryViewModel` and all of the other `_EntryViewModel`
classes are wrappers for the settings model NTM entries.
- `FolderTreeViewEntry` encapsulates `FolderEntryViewModel`. It allows
us to construct a `TreeView` of just folders.
- View changes and additions:
   - Added FontIconGlyph to the SettingContainer
   - Added a New Tab Menu item to the navigation view
- Adding entries: a stack of SettingContainers is used here. We use the
new `FontIconGlyph` to make this look nice!
- Reordering entries: drag and drop is supported! This might not work in
admin mode though, and we can't drag and drop into folders. Buttons were
added to make this keyboard accessible.
- To move entries into a folder, a button was added which then displays
a TreeView of all folders.
   - Multiple entries can be moved to a folder or deleted at once!
   - Breadcrumbs are used for folders
- When a folder is entered, additional controls are displayed to
customize that folder.
 
## Verification
-  a11y pass
-  keyboard accessible
- scenarios:
   -  add entries (except actions)
   -  changes propagated to settings model (aka "saving works")
   -  reorder entries
   -  move entries to an existing folder
   -  delete multiple entries
   -  delete individual entries
   -  display entries (including actions)

## Follow-ups
- [ ] add support for adding and editing action entries
- [ ] when we discard changes or save, it would be cool if we could stay
on the same page
- [ ] allow customizing the folder entry _before_ adding it (current
workaround is to add it, then edit it)
- [ ] improve UI for setting icon (reuse UI from #17965)
This commit is contained in:
Carlos Zamora 2024-12-03 15:07:13 -08:00 committed by GitHub
parent 5c55144c28
commit 0d846aeb4d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 2581 additions and 285 deletions

View File

@ -56,6 +56,7 @@ Powerline
ptys
pwn
pwshw
QOL
qof
qps
quickfix
@ -73,6 +74,7 @@ shcha
similaritytolerance
slnt
stakeholders
subpage
sustainability
sxn
TLDR

View File

@ -67,6 +67,7 @@
<Thickness x:Key="StandardControlMargin">0,24,0,0</Thickness>
<x:Double x:Key="StandardBoxMinWidth">250</x:Double>
<x:Double x:Key="StandardControlMaxWidth">1000</x:Double>
<Thickness x:Key="SettingStackMargin">13,0,13,48</Thickness>
<!--
This is for styling the entire items control used on the
@ -80,13 +81,13 @@
<!-- Used to stack a group of settings -->
<Style x:Key="SettingsStackStyle"
TargetType="StackPanel">
<Setter Property="Margin" Value="13,0,13,48" />
<Setter Property="Margin" Value="{StaticResource SettingStackMargin}" />
</Style>
<!-- Used to stack a group of settings inside a pivot -->
<Style x:Key="PivotStackStyle"
TargetType="StackPanel">
<Setter Property="Margin" Value="0,0,13,48" />
<Setter Property="Margin" Value="{StaticResource SettingStackMargin}" />
</Style>
<!-- Combo Box -->
@ -255,6 +256,17 @@
</Setter>
</Style>
<Style x:Key="ExtraSmallButtonStyle"
BasedOn="{StaticResource BrowseButtonStyle}"
TargetType="Button">
<Setter Property="Height" Value="25" />
<Setter Property="Width" Value="25" />
<Setter Property="Padding" Value="0" />
<Setter Property="Margin" Value="0" />
<Setter Property="MinHeight" Value="0" />
<Setter Property="MinWidth" Value="0" />
</Style>
<Style x:Key="SmallButtonStyle"
BasedOn="{StaticResource BrowseButtonStyle}"
TargetType="Button">
@ -288,6 +300,17 @@
<Setter Property="Padding" Value="5" />
</Style>
<Style x:Key="DeleteExtraSmallButtonStyle"
BasedOn="{StaticResource DeleteButtonStyle}"
TargetType="Button">
<Setter Property="Height" Value="25" />
<Setter Property="Width" Value="25" />
<Setter Property="Padding" Value="0" />
<Setter Property="Margin" Value="0" />
<Setter Property="MinHeight" Value="0" />
<Setter Property="MinWidth" Value="0" />
</Style>
<Style x:Key="IconButtonTextBlockStyle"
TargetType="TextBlock">
<Setter Property="Margin" Value="10,0,0,0" />

View File

@ -17,6 +17,7 @@
#include "AddProfile.h"
#include "InteractionViewModel.h"
#include "LaunchViewModel.h"
#include "NewTabMenuViewModel.h"
#include "..\types\inc\utils.hpp"
#include <..\WinRTUtils\inc\Utils.h>
@ -42,6 +43,7 @@ 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" };
@ -61,6 +63,28 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
InitializeComponent();
_UpdateBackgroundForMica();
_newTabMenuPageVM = winrt::make<NewTabMenuViewModel>(_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<Breadcrumb>(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<Breadcrumb>(box_value(newTabMenuTag), RS_(L"Nav_NewTabMenu/Content"), BreadcrumbSubPage::None);
_breadcrumbs.Append(crumb);
}
contentFrame().Navigate(xaml_typename<Editor::NewTabMenu>(), _newTabMenuPageVM);
}
});
_colorSchemesPageVM = winrt::make<ColorSchemesPageViewModel>(_settingsClone);
_colorSchemesPageViewModelChangedRevoker = _colorSchemesPageVM.PropertyChanged(winrt::auto_revoke, [=](auto&&, const PropertyChangedEventArgs& args) {
const auto settingName{ args.PropertyName() };
@ -136,12 +160,13 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
_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<Breadcrumb>() })
if (const auto& crumb{ lastBreadcrumb.try_as<Breadcrumb>() }; crumb && crumb->Tag())
{
for (const auto& item : _menuItemSource)
{
@ -161,6 +186,17 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
return;
}
}
else if (const auto& breadcrumbFolderEntry{ crumb->Tag().try_as<Editor::FolderEntryViewModel>() })
{
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<ProfileViewModel>() })
{
@ -393,6 +429,22 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
const auto crumb = winrt::make<Breadcrumb>(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<Editor::NewTabMenu>(), _newTabMenuPageVM);
const auto crumb = winrt::make<Breadcrumb>(box_value(clickedItemTag), RS_(L"Nav_NewTabMenu/Content"), BreadcrumbSubPage::None);
_breadcrumbs.Append(crumb);
}
}
else if (clickedItemTag == globalProfileTag)
{
auto profileVM{ _viewModelForProfile(_settingsClone.ProfileDefaults(), _settingsClone) };
@ -490,6 +542,39 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
}
}
void MainPage::_Navigate(const Editor::NewTabMenuEntryViewModel& ntmEntryVM, BreadcrumbSubPage subPage)
{
_PreNavigateHelper();
contentFrame().Navigate(xaml_typename<Editor::NewTabMenu>(), _newTabMenuPageVM);
const auto crumb = winrt::make<Breadcrumb>(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<Editor::FolderEntryViewModel>(); 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::OpenJsonTapped(const IInspectable& /*sender*/, const Windows::UI::Xaml::Input::TappedRoutedEventArgs& /*args*/)
{
const auto window = CoreWindow::GetForCurrentThread();
@ -532,6 +617,10 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
_Navigate(*profileViewModel, subPage);
}
else if (const auto ntmEntryViewModel = tag.try_as<NewTabMenuEntryViewModel>())
{
_Navigate(*ntmEntryViewModel, subPage);
}
else
{
_Navigate(tag.as<hstring>(), subPage);

View File

@ -69,14 +69,17 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
void _PreNavigateHelper();
void _Navigate(hstring clickedItemTag, BreadcrumbSubPage subPage);
void _Navigate(const Editor::ProfileViewModel& profile, BreadcrumbSubPage subPage);
void _Navigate(const Editor::NewTabMenuEntryViewModel& ntmEntryVM, BreadcrumbSubPage subPage);
void _UpdateBackgroundForMica();
void _MoveXamlParsedNavItemsIntoItemSource();
winrt::Microsoft::Terminal::Settings::Editor::ColorSchemesPageViewModel _colorSchemesPageVM{ nullptr };
winrt::Microsoft::Terminal::Settings::Editor::NewTabMenuViewModel _newTabMenuPageVM{ 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;
};
}

View File

@ -19,7 +19,8 @@ namespace Microsoft.Terminal.Settings.Editor
Profile_Appearance,
Profile_Terminal,
Profile_Advanced,
ColorSchemes_Edit
ColorSchemes_Edit,
NewTabMenu_Folder
};
runtimeclass Breadcrumb : Windows.Foundation.IStringable

View File

@ -148,6 +148,13 @@
</muxc:NavigationViewItem.Icon>
</muxc:NavigationViewItem>
<muxc:NavigationViewItem x:Uid="Nav_NewTabMenu"
Tag="NewTabMenu_Nav">
<muxc:NavigationViewItem.Icon>
<FontIcon Glyph="&#xE71d;" />
</muxc:NavigationViewItem.Icon>
</muxc:NavigationViewItem>
<muxc:NavigationViewItemHeader x:Uid="Nav_Profiles" />
<muxc:NavigationViewItem x:Name="BaseLayerMenuItem"

View File

@ -73,6 +73,9 @@
<ClInclude Include="Launch.h">
<DependentUpon>Launch.xaml</DependentUpon>
</ClInclude>
<ClInclude Include="NewTabMenu.h">
<DependentUpon>NewTabMenu.xaml</DependentUpon>
</ClInclude>
<ClInclude Include="pch.h" />
<ClInclude Include="MainPage.h">
<DependentUpon>MainPage.xaml</DependentUpon>
@ -109,6 +112,10 @@
<DependentUpon>LaunchViewModel.idl</DependentUpon>
<SubType>Code</SubType>
</ClInclude>
<ClInclude Include="NewTabMenuViewModel.h">
<DependentUpon>NewTabMenuViewModel.idl</DependentUpon>
<SubType>Code</SubType>
</ClInclude>
<ClInclude Include="Profiles_Base.h">
<DependentUpon>Profiles_Base.xaml</DependentUpon>
<SubType>Code</SubType>
@ -174,6 +181,9 @@
<Page Include="Launch.xaml">
<SubType>Designer</SubType>
</Page>
<Page Include="NewTabMenu.xaml">
<SubType>Designer</SubType>
</Page>
<Page Include="MainPage.xaml">
<SubType>Designer</SubType>
</Page>
@ -233,6 +243,9 @@
<ClCompile Include="Launch.cpp">
<DependentUpon>Launch.xaml</DependentUpon>
</ClCompile>
<ClCompile Include="NewTabMenu.cpp">
<DependentUpon>NewTabMenu.xaml</DependentUpon>
</ClCompile>
<ClCompile Include="pch.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>
</ClCompile>
@ -272,6 +285,10 @@
<DependentUpon>LaunchViewModel.idl</DependentUpon>
<SubType>Code</SubType>
</ClCompile>
<ClCompile Include="NewTabMenuViewModel.cpp">
<DependentUpon>NewTabMenuViewModel.idl</DependentUpon>
<SubType>Code</SubType>
</ClCompile>
<ClCompile Include="Profiles_Base.cpp">
<DependentUpon>Profiles_Base.xaml</DependentUpon>
<SubType>Code</SubType>
@ -338,6 +355,10 @@
<DependentUpon>Launch.xaml</DependentUpon>
<SubType>Code</SubType>
</Midl>
<Midl Include="NewTabMenu.idl">
<DependentUpon>NewTabMenu.xaml</DependentUpon>
<SubType>Code</SubType>
</Midl>
<Midl Include="Interaction.idl">
<DependentUpon>Interaction.xaml</DependentUpon>
<SubType>Code</SubType>
@ -361,6 +382,7 @@
<Midl Include="InteractionViewModel.idl" />
<Midl Include="GlobalAppearanceViewModel.idl" />
<Midl Include="LaunchViewModel.idl" />
<Midl Include="NewTabMenuViewModel.idl" />
<Midl Include="Profiles_Base.idl">
<DependentUpon>Profiles_Base.xaml</DependentUpon>
<SubType>Code</SubType>

View File

@ -27,6 +27,7 @@
<Midl Include="LaunchViewModel.idl" />
<Midl Include="EnumEntry.idl" />
<Midl Include="SettingContainer.idl" />
<Midl Include="NewTabMenuViewModel.idl" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
@ -49,5 +50,6 @@
<Page Include="SettingContainerStyle.xaml" />
<Page Include="AddProfile.xaml" />
<Page Include="KeyChordListener.xaml" />
<Page Include="NewTabMenu.xaml" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,203 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "NewTabMenu.h"
#include "NewTabMenu.g.cpp"
#include "NewTabMenuEntryTemplateSelector.g.cpp"
#include "EnumEntry.h"
#include "NewTabMenuViewModel.h"
#include <LibraryResources.h>
using namespace winrt::Windows::UI::Xaml;
using namespace winrt::Windows::UI::Xaml::Navigation;
using namespace winrt::Windows::Foundation;
using namespace winrt::Microsoft::Terminal::Settings::Model;
namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
NewTabMenu::NewTabMenu()
{
InitializeComponent();
_entryTemplateSelector = Resources().Lookup(box_value(L"NewTabMenuEntryTemplateSelector")).as<Editor::NewTabMenuEntryTemplateSelector>();
// Ideally, we'd bind IsEnabled to something like mtu:Converters.isEmpty(NewTabMenuListView.SelectedItems.Size) in the XAML,
// but the XAML compiler can't find NewTabMenuListView when we try that. Rather than copying the list of selected items over
// to the view model, we'll just do this instead (much simpler).
NewTabMenuListView().SelectionChanged([this](auto&&, auto&&) {
const auto list = NewTabMenuListView();
MoveToFolderButton().IsEnabled(list.SelectedItems().Size() > 0);
DeleteMultipleButton().IsEnabled(list.SelectedItems().Size() > 0);
});
Automation::AutomationProperties::SetName(MoveToFolderButton(), RS_(L"NewTabMenu_MoveToFolderTextBlock/Text"));
Automation::AutomationProperties::SetName(DeleteMultipleButton(), RS_(L"NewTabMenu_DeleteMultipleTextBlock/Text"));
Automation::AutomationProperties::SetName(AddProfileComboBox(), RS_(L"NewTabMenu_AddProfile/Header"));
Automation::AutomationProperties::SetName(AddProfileButton(), RS_(L"NewTabMenu_AddProfileButton/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip"));
Automation::AutomationProperties::SetName(AddSeparatorButton(), RS_(L"NewTabMenu_AddSeparatorButton/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip"));
Automation::AutomationProperties::SetName(AddFolderButton(), RS_(L"NewTabMenu_AddFolderButton/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip"));
Automation::AutomationProperties::SetName(AddMatchProfilesButton(), RS_(L"NewTabMenu_AddMatchProfilesTextBlock/Text"));
Automation::AutomationProperties::SetName(AddRemainingProfilesButton(), RS_(L"NewTabMenu_AddRemainingProfilesButton/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip"));
}
void NewTabMenu::OnNavigatedTo(const NavigationEventArgs& e)
{
_ViewModel = e.Parameter().as<Editor::NewTabMenuViewModel>();
}
void NewTabMenu::FolderPickerDialog_Opened(const IInspectable& /*sender*/, const Controls::ContentDialogOpenedEventArgs& /*e*/)
{
// Ideally, we'd bind IsPrimaryButtonEnabled to something like mtu:Converters.isEmpty(FolderTree.SelectedItems.Size) in the XAML.
// Similar to above, the XAML compiler can't find FolderTree when we try that.
// To make matters worse, SelectionChanged doesn't exist for WinUI 2's TreeView.
// Let's just select the first item and call it a day.
_ViewModel.GenerateFolderTree();
_ViewModel.CurrentFolderTreeViewSelectedItem(_ViewModel.FolderTree().First().Current());
}
void NewTabMenu::FolderPickerDialog_PrimaryButtonClick(const IInspectable& /*sender*/, const Controls::ContentDialogButtonClickEventArgs& /*e*/)
{
// copy selected items first (it updates as we move entries)
std::vector<Editor::NewTabMenuEntryViewModel> entries;
for (const auto& item : NewTabMenuListView().SelectedItems())
{
entries.push_back(item.as<Editor::NewTabMenuEntryViewModel>());
}
// now actually move them
_ViewModel.RequestMoveEntriesToFolder(single_threaded_vector<Editor::NewTabMenuEntryViewModel>(std::move(entries)), FolderTreeView().SelectedItem().as<Editor::FolderTreeViewEntry>().FolderEntryVM());
}
void NewTabMenu::EditEntry_Clicked(const IInspectable& sender, const RoutedEventArgs& /*e*/)
{
const auto folderVM = sender.as<FrameworkElement>().DataContext().as<Editor::FolderEntryViewModel>();
_ViewModel.CurrentFolder(folderVM);
}
void NewTabMenu::ReorderEntry_Clicked(const IInspectable& sender, const RoutedEventArgs& /*e*/)
{
const auto btn = sender.as<Controls::Button>();
const auto entry = btn.DataContext().as<Editor::NewTabMenuEntryViewModel>();
const auto direction = unbox_value<hstring>(btn.Tag());
_ViewModel.RequestReorderEntry(entry, direction == L"Up");
}
void NewTabMenu::DeleteEntry_Clicked(const IInspectable& sender, const RoutedEventArgs& /*e*/)
{
const auto entry = sender.as<Controls::Button>().DataContext().as<Editor::NewTabMenuEntryViewModel>();
_ViewModel.RequestDeleteEntry(entry);
}
safe_void_coroutine NewTabMenu::MoveMultiple_Click(const IInspectable& /*sender*/, const RoutedEventArgs& /*e*/)
{
co_await FindName(L"FolderPickerDialog").as<Controls::ContentDialog>().ShowAsync();
}
void NewTabMenu::DeleteMultiple_Click(const IInspectable& /*sender*/, const RoutedEventArgs& /*e*/)
{
// copy selected items first (it updates as we delete entries)
std::vector<Editor::NewTabMenuEntryViewModel> entries;
for (const auto& item : NewTabMenuListView().SelectedItems())
{
entries.push_back(item.as<Editor::NewTabMenuEntryViewModel>());
}
// now actually delete them
for (const auto& vm : entries)
{
_ViewModel.RequestDeleteEntry(vm);
}
}
void NewTabMenu::AddProfileButton_Clicked(const IInspectable& /*sender*/, const RoutedEventArgs& /*e*/)
{
_ScrollToEntry(_ViewModel.RequestAddSelectedProfileEntry());
}
void NewTabMenu::AddSeparatorButton_Clicked(const IInspectable& /*sender*/, const RoutedEventArgs& /*e*/)
{
_ScrollToEntry(_ViewModel.RequestAddSeparatorEntry());
}
void NewTabMenu::AddFolderButton_Clicked(const IInspectable& /*sender*/, const RoutedEventArgs& /*e*/)
{
_ScrollToEntry(_ViewModel.RequestAddFolderEntry());
}
void NewTabMenu::AddMatchProfilesButton_Clicked(const IInspectable& /*sender*/, const RoutedEventArgs& /*e*/)
{
_ScrollToEntry(_ViewModel.RequestAddProfileMatcherEntry());
}
void NewTabMenu::AddRemainingProfilesButton_Clicked(const IInspectable& /*sender*/, const RoutedEventArgs& /*e*/)
{
_ScrollToEntry(_ViewModel.RequestAddRemainingProfilesEntry());
}
// As a QOL improvement, we scroll to the newly added entry.
// Calling ScrollIntoView() on its own causes the items to briefly disappear.
// Calling UpdateLayout() beforehand remedies this issue.
void NewTabMenu::_ScrollToEntry(const Editor::NewTabMenuEntryViewModel& entry)
{
const auto& listView = NewTabMenuListView();
listView.UpdateLayout();
listView.ScrollIntoView(entry);
}
void NewTabMenu::AddFolderNameTextBox_KeyDown(const IInspectable& /*sender*/, const Input::KeyRoutedEventArgs& e)
{
if (e.Key() == Windows::System::VirtualKey::Enter)
{
// We need to manually set the FolderName here because the TextBox's TextChanged event hasn't fired yet.
if (const auto folderName = FolderNameTextBox().Text(); !folderName.empty())
{
_ViewModel.AddFolderName(folderName);
const auto entry = _ViewModel.RequestAddFolderEntry();
NewTabMenuListView().ScrollIntoView(entry);
}
}
}
void NewTabMenu::AddFolderNameTextBox_TextChanged(const IInspectable& sender, const Controls::TextChangedEventArgs& /*e*/)
{
const auto isTextEmpty = sender.as<Controls::TextBox>().Text().empty();
AddFolderButton().IsEnabled(!isTextEmpty);
}
DataTemplate NewTabMenuEntryTemplateSelector::SelectTemplateCore(const IInspectable& item, const DependencyObject& /*container*/)
{
return SelectTemplateCore(item);
}
DataTemplate NewTabMenuEntryTemplateSelector::SelectTemplateCore(const IInspectable& item)
{
if (const auto entryVM = item.try_as<Editor::NewTabMenuEntryViewModel>())
{
switch (entryVM.Type())
{
case Model::NewTabMenuEntryType::Profile:
return ProfileEntryTemplate();
case Model::NewTabMenuEntryType::Action:
return ActionEntryTemplate();
case Model::NewTabMenuEntryType::Separator:
return SeparatorEntryTemplate();
case Model::NewTabMenuEntryType::Folder:
return FolderEntryTemplate();
case Model::NewTabMenuEntryType::MatchProfiles:
return MatchProfilesEntryTemplate();
case Model::NewTabMenuEntryType::RemainingProfiles:
return RemainingProfilesEntryTemplate();
case Model::NewTabMenuEntryType::Invalid:
default:
assert(false);
return nullptr;
}
}
assert(false);
return nullptr;
}
}

View File

@ -0,0 +1,72 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#include "NewTabMenu.g.h"
#include "NewTabMenuEntryTemplateSelector.g.h"
#include "Utils.h"
namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
struct NewTabMenu : public HasScrollViewer<NewTabMenu>, NewTabMenuT<NewTabMenu>
{
public:
NewTabMenu();
void OnNavigatedTo(const winrt::Windows::UI::Xaml::Navigation::NavigationEventArgs& e);
// FolderPickerDialog handlers
void FolderPickerDialog_Opened(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::Controls::ContentDialogOpenedEventArgs& e);
void FolderPickerDialog_PrimaryButtonClick(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::Controls::ContentDialogButtonClickEventArgs& e);
// NTM Entry handlers
void EditEntry_Clicked(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::RoutedEventArgs& e);
void ReorderEntry_Clicked(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::RoutedEventArgs& e);
void DeleteEntry_Clicked(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::RoutedEventArgs& e);
// Multiple Entry handlers
safe_void_coroutine MoveMultiple_Click(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::RoutedEventArgs& e);
void DeleteMultiple_Click(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::RoutedEventArgs& e);
// New Entry handlers
void AddProfileButton_Clicked(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::RoutedEventArgs& e);
void AddSeparatorButton_Clicked(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::RoutedEventArgs& e);
void AddFolderButton_Clicked(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::RoutedEventArgs& e);
void AddMatchProfilesButton_Clicked(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::RoutedEventArgs& e);
void AddRemainingProfilesButton_Clicked(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::RoutedEventArgs& e);
void AddFolderNameTextBox_KeyDown(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::Input::KeyRoutedEventArgs& e);
void AddFolderNameTextBox_TextChanged(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::Controls::TextChangedEventArgs& e);
WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler);
WINRT_OBSERVABLE_PROPERTY(Editor::NewTabMenuViewModel, ViewModel, _PropertyChangedHandlers, nullptr);
private:
Editor::NewTabMenuEntryTemplateSelector _entryTemplateSelector{ nullptr };
Editor::NewTabMenuEntryViewModel _draggedEntry{ nullptr };
void _ScrollToEntry(const Editor::NewTabMenuEntryViewModel& entry);
};
struct NewTabMenuEntryTemplateSelector : public NewTabMenuEntryTemplateSelectorT<NewTabMenuEntryTemplateSelector>
{
public:
NewTabMenuEntryTemplateSelector() = 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, ProfileEntryTemplate, nullptr);
WINRT_PROPERTY(Windows::UI::Xaml::DataTemplate, ActionEntryTemplate, nullptr);
WINRT_PROPERTY(Windows::UI::Xaml::DataTemplate, SeparatorEntryTemplate, nullptr);
WINRT_PROPERTY(Windows::UI::Xaml::DataTemplate, FolderEntryTemplate, nullptr);
WINRT_PROPERTY(Windows::UI::Xaml::DataTemplate, MatchProfilesEntryTemplate, nullptr);
WINRT_PROPERTY(Windows::UI::Xaml::DataTemplate, RemainingProfilesEntryTemplate, nullptr);
};
}
namespace winrt::Microsoft::Terminal::Settings::Editor::factory_implementation
{
BASIC_FACTORY(NewTabMenu);
BASIC_FACTORY(NewTabMenuEntryTemplateSelector);
}

View File

@ -0,0 +1,25 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import "NewTabMenuViewModel.idl";
namespace Microsoft.Terminal.Settings.Editor
{
[default_interface] runtimeclass NewTabMenu : Windows.UI.Xaml.Controls.Page
{
NewTabMenu();
NewTabMenuViewModel ViewModel { get; };
}
[default_interface] runtimeclass NewTabMenuEntryTemplateSelector : Windows.UI.Xaml.Controls.DataTemplateSelector
{
NewTabMenuEntryTemplateSelector();
Windows.UI.Xaml.DataTemplate ProfileEntryTemplate;
Windows.UI.Xaml.DataTemplate ActionEntryTemplate;
Windows.UI.Xaml.DataTemplate SeparatorEntryTemplate;
Windows.UI.Xaml.DataTemplate FolderEntryTemplate;
Windows.UI.Xaml.DataTemplate MatchProfilesEntryTemplate;
Windows.UI.Xaml.DataTemplate RemainingProfilesEntryTemplate;
}
}

View File

@ -0,0 +1,466 @@
<!--
Copyright (c) Microsoft Corporation. All rights reserved. Licensed under
the MIT License. See LICENSE in the project root for license information.
-->
<Page x:Class="Microsoft.Terminal.Settings.Editor.NewTabMenu"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Microsoft.Terminal.Settings.Editor"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:model="using:Microsoft.Terminal.Settings.Model"
xmlns:mtu="using:Microsoft.Terminal.UI"
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
mc:Ignorable="d">
<Page.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="CommonResources.xaml" />
</ResourceDictionary.MergedDictionaries>
<!-- Wrapper for NewTabMenuEntry that adds buttons to the ListView entries -->
<Style x:Key="NewTabMenuEntryControlsWrapper"
TargetType="ContentControl">
<Setter Property="IsTabStop" Value="False" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ContentControl">
<Grid XYFocusKeyboardNavigation="Enabled">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ContentPresenter Grid.Column="0"
Content="{TemplateBinding Content}" />
<StackPanel Grid.Column="1"
Orientation="Horizontal"
Spacing="5">
<!-- Reorder: Up -->
<Button x:Uid="NewTabMenuEntry_ReorderUp"
Click="ReorderEntry_Clicked"
DataContext="{TemplateBinding DataContext}"
Style="{StaticResource ExtraSmallButtonStyle}"
Tag="Up">
<FontIcon FontSize="{StaticResource StandardIconSize}"
Glyph="&#xF0AD;" />
</Button>
<!-- Reorder: Down -->
<Button x:Uid="NewTabMenuEntry_ReorderDown"
Click="ReorderEntry_Clicked"
DataContext="{TemplateBinding DataContext}"
Style="{StaticResource ExtraSmallButtonStyle}"
Tag="Down">
<FontIcon FontSize="{StaticResource StandardIconSize}"
Glyph="&#xF0AE;" />
</Button>
<!-- Delete Entry -->
<Button x:Uid="NewTabMenuEntry_Delete"
Click="DeleteEntry_Clicked"
DataContext="{TemplateBinding DataContext}"
Style="{StaticResource DeleteExtraSmallButtonStyle}">
<FontIcon FontSize="{StaticResource StandardIconSize}"
Glyph="&#xE74D;" />
</Button>
</StackPanel>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<DataTemplate x:Key="ProfileEntryTemplate"
x:DataType="local:ProfileEntryViewModel">
<ContentControl AutomationProperties.Name="{x:Bind ProfileEntry.Profile.Name, Mode=OneWay}"
DataContext="{Binding Mode=OneWay}"
Style="{StaticResource NewTabMenuEntryControlsWrapper}">
<StackPanel Orientation="Horizontal"
Spacing="10">
<IconSourceElement Width="16"
Height="16"
VerticalAlignment="Center"
IconSource="{x:Bind mtu:IconPathConverter.IconSourceWUX(ProfileEntry.Profile.EvaluatedIcon), Mode=OneTime}" />
<TextBlock VerticalAlignment="Center"
Text="{x:Bind ProfileEntry.Profile.Name, Mode=OneWay}" />
</StackPanel>
</ContentControl>
</DataTemplate>
<DataTemplate x:Key="ActionEntryTemplate"
x:DataType="local:ActionEntryViewModel">
<ContentControl AutomationProperties.Name="{x:Bind DisplayText, Mode=OneWay}"
DataContext="{Binding Mode=OneWay}"
Style="{StaticResource NewTabMenuEntryControlsWrapper}">
<StackPanel Orientation="Horizontal">
<IconSourceElement Width="16"
Height="16"
Margin="0,0,10,0"
VerticalAlignment="Center"
IconSource="{x:Bind mtu:IconPathConverter.IconSourceWUX(Icon), Mode=OneWay}"
Visibility="{x:Bind mtu:Converters.StringNotEmptyToVisibility(Icon), Mode=OneWay}" />
<TextBlock VerticalAlignment="Center"
Text="{x:Bind DisplayText, Mode=OneWay}" />
</StackPanel>
</ContentControl>
</DataTemplate>
<DataTemplate x:Key="SeparatorEntryTemplate"
x:DataType="local:SeparatorEntryViewModel">
<ContentControl x:Uid="NewTabMenuEntry_SeparatorItem"
DataContext="{Binding Mode=OneWay}"
Style="{StaticResource NewTabMenuEntryControlsWrapper}">
<TextBlock x:Uid="NewTabMenuEntry_Separator"
VerticalAlignment="Center"
FontStyle="Italic" />
</ContentControl>
</DataTemplate>
<DataTemplate x:Key="FolderEntryTemplate"
x:DataType="local:FolderEntryViewModel">
<!--
Most of this was copied from NewTabMenuEntryControlsWrapper.
We really just added the Edit button here.
-->
<Grid AutomationProperties.Name="{x:Bind Name, Mode=OneWay}"
XYFocusKeyboardNavigation="Enabled">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0"
Orientation="Horizontal">
<IconSourceElement Width="16"
Height="16"
Margin="0,0,10,0"
VerticalAlignment="Center"
IconSource="{x:Bind mtu:IconPathConverter.IconSourceWUX(Icon), Mode=OneWay}"
Visibility="{x:Bind mtu:Converters.StringNotEmptyToVisibility(Icon), Mode=OneWay}" />
<TextBlock VerticalAlignment="Center"
Text="{x:Bind Name, Mode=OneWay}" />
</StackPanel>
<StackPanel Grid.Column="1"
Orientation="Horizontal"
Spacing="5">
<Button x:Uid="NewTabMenuEntry_EditFolder"
Click="EditEntry_Clicked"
DataContext="{Binding Mode=OneWay}"
Style="{StaticResource ExtraSmallButtonStyle}">
<FontIcon FontSize="{StaticResource StandardIconSize}"
Glyph="&#xE70F;" />
</Button>
<Button x:Uid="NewTabMenuEntry_ReorderUp"
Click="ReorderEntry_Clicked"
DataContext="{Binding Mode=OneWay}"
Style="{StaticResource ExtraSmallButtonStyle}"
Tag="Up">
<FontIcon FontSize="{StaticResource StandardIconSize}"
Glyph="&#xF0AD;" />
</Button>
<Button x:Uid="NewTabMenuEntry_ReorderDown"
Click="ReorderEntry_Clicked"
DataContext="{Binding Mode=OneWay}"
Style="{StaticResource ExtraSmallButtonStyle}"
Tag="Down">
<FontIcon FontSize="{StaticResource StandardIconSize}"
Glyph="&#xF0AE;" />
</Button>
<Button x:Uid="NewTabMenuEntry_Delete"
Click="DeleteEntry_Clicked"
DataContext="{Binding Mode=OneWay}"
Style="{StaticResource DeleteExtraSmallButtonStyle}">
<FontIcon FontSize="{StaticResource StandardIconSize}"
Glyph="&#xE74D;" />
</Button>
</StackPanel>
</Grid>
</DataTemplate>
<DataTemplate x:Key="MatchProfilesEntryTemplate"
x:DataType="local:MatchProfilesEntryViewModel">
<ContentControl AutomationProperties.Name="{x:Bind DisplayText, Mode=OneWay}"
DataContext="{Binding Mode=OneWay}"
Style="{StaticResource NewTabMenuEntryControlsWrapper}">
<TextBlock VerticalAlignment="Center"
FontStyle="Italic"
Text="{x:Bind DisplayText, Mode=OneWay}" />
</ContentControl>
</DataTemplate>
<DataTemplate x:Key="RemainingProfilesEntryTemplate"
x:DataType="local:RemainingProfilesEntryViewModel">
<ContentControl x:Uid="NewTabMenuEntry_RemainingProfilesItem"
DataContext="{Binding Mode=OneWay}"
Style="{StaticResource NewTabMenuEntryControlsWrapper}">
<TextBlock x:Uid="NewTabMenuEntry_RemainingProfiles"
VerticalAlignment="Center"
FontStyle="Italic" />
</ContentControl>
</DataTemplate>
<local:NewTabMenuEntryTemplateSelector x:Key="NewTabMenuEntryTemplateSelector"
ActionEntryTemplate="{StaticResource ActionEntryTemplate}"
FolderEntryTemplate="{StaticResource FolderEntryTemplate}"
MatchProfilesEntryTemplate="{StaticResource MatchProfilesEntryTemplate}"
ProfileEntryTemplate="{StaticResource ProfileEntryTemplate}"
RemainingProfilesEntryTemplate="{StaticResource RemainingProfilesEntryTemplate}"
SeparatorEntryTemplate="{StaticResource SeparatorEntryTemplate}" />
</ResourceDictionary>
</Page.Resources>
<Grid Margin="{StaticResource SettingStackMargin}"
RowSpacing="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Folder Picker Dialog: used to select a folder to move entries to -->
<ContentDialog x:Name="FolderPickerDialog"
x:Uid="NewTabMenu_FolderPickerDialog"
x:Load="False"
DefaultButton="Primary"
Opened="FolderPickerDialog_Opened"
PrimaryButtonClick="FolderPickerDialog_PrimaryButtonClick">
<muxc:TreeView x:Name="FolderTreeView"
ItemsSource="{x:Bind ViewModel.FolderTree, Mode=OneWay}"
SelectedItem="{x:Bind ViewModel.CurrentFolderTreeViewSelectedItem, Mode=TwoWay}"
SelectionMode="Single">
<muxc:TreeView.ItemTemplate>
<DataTemplate x:DataType="local:FolderTreeViewEntry">
<muxc:TreeViewItem AutomationProperties.Name="{x:Bind Name, Mode=OneWay}"
IsExpanded="True"
ItemsSource="{x:Bind Children, Mode=OneWay}">
<StackPanel AutomationProperties.Name="{x:Bind Name, Mode=OneWay}"
Orientation="Horizontal">
<IconSourceElement Width="16"
Height="16"
Margin="0,0,10,0"
VerticalAlignment="Center"
IconSource="{x:Bind mtu:IconPathConverter.IconSourceWUX(Icon), Mode=OneWay}"
Visibility="{x:Bind mtu:Converters.StringNotEmptyToVisibility(Icon), Mode=OneWay}" />
<TextBlock VerticalAlignment="Center"
Text="{x:Bind Name, Mode=OneWay}" />
</StackPanel>
</muxc:TreeViewItem>
</DataTemplate>
</muxc:TreeView.ItemTemplate>
</muxc:TreeView>
</ContentDialog>
<!-- New Tab Menu Content -->
<StackPanel Grid.Row="0"
MaxWidth="{StaticResource StandardControlMaxWidth}"
Spacing="10">
<Border Height="300"
MaxWidth="{StaticResource StandardControlMaxWidth}"
Margin="0,12,0,0"
BorderBrush="{ThemeResource SystemControlForegroundBaseMediumLowBrush}"
BorderThickness="1"
CornerRadius="{ThemeResource ControlCornerRadius}">
<ListView x:Name="NewTabMenuListView"
AllowDrop="True"
CanDragItems="True"
CanReorderItems="True"
ItemTemplateSelector="{StaticResource NewTabMenuEntryTemplateSelector}"
ItemsSource="{x:Bind ViewModel.CurrentView, Mode=OneWay}"
SelectionMode="Multiple" />
</Border>
<!-- General Controls -->
<StackPanel Orientation="Horizontal"
Spacing="10">
<Button x:Name="MoveToFolderButton"
Click="MoveMultiple_Click"
IsEnabled="False">
<StackPanel Orientation="Horizontal">
<FontIcon FontSize="{StaticResource StandardIconSize}"
Glyph="&#xE8DE;" />
<TextBlock x:Uid="NewTabMenu_MoveToFolderTextBlock"
Margin="10,0,0,0" />
</StackPanel>
</Button>
<Button x:Name="DeleteMultipleButton"
Click="DeleteMultiple_Click"
IsEnabled="False"
Style="{StaticResource DeleteButtonStyle}">
<StackPanel Orientation="Horizontal">
<FontIcon FontSize="{StaticResource StandardIconSize}"
Glyph="&#xE74D;" />
<TextBlock x:Uid="NewTabMenu_DeleteMultipleTextBlock"
Margin="10,0,0,0" />
</StackPanel>
</Button>
</StackPanel>
</StackPanel>
<!-- Folder View Controls -->
<StackPanel Grid.Row="1"
MaxWidth="{StaticResource StandardControlMaxWidth}"
Visibility="{x:Bind ViewModel.IsFolderView, Mode=OneWay}">
<TextBlock x:Uid="NewTabMenu_CurrentFolderTextBlock"
Style="{StaticResource TextBlockSubHeaderStyle}" />
<!-- TODO CARLOS: Icon -->
<!-- Once PR #17965 merges, we can add that kind of control to set an icon -->
<!-- Name -->
<local:SettingContainer x:Uid="NewTabMenu_CurrentFolderName"
Grid.Row="0"
CurrentValue="{x:Bind ViewModel.CurrentFolderName, Mode=OneWay}"
Style="{StaticResource ExpanderSettingContainerStyle}">
<TextBox Style="{StaticResource TextBoxSettingStyle}"
Text="{x:Bind ViewModel.CurrentFolderName, Mode=TwoWay}" />
</local:SettingContainer>
<!-- Inlining -->
<local:SettingContainer x:Uid="NewTabMenu_CurrentFolderInlining"
Grid.Row="1">
<ToggleSwitch IsOn="{x:Bind ViewModel.CurrentFolderInlining, Mode=TwoWay}"
Style="{StaticResource ToggleSwitchInExpanderStyle}" />
</local:SettingContainer>
<!-- Allow Empty -->
<local:SettingContainer x:Uid="NewTabMenu_CurrentFolderAllowEmpty"
Grid.Row="2">
<ToggleSwitch IsOn="{x:Bind ViewModel.CurrentFolderAllowEmpty, Mode=TwoWay}"
Style="{StaticResource ToggleSwitchInExpanderStyle}" />
</local:SettingContainer>
</StackPanel>
<!-- Add Entries -->
<StackPanel Grid.Row="2">
<TextBlock x:Uid="NewTabMenu_AddEntriesTextBlock"
Style="{StaticResource TextBlockSubHeaderStyle}" />
<!-- Add Profile -->
<local:SettingContainer x:Uid="NewTabMenu_AddProfile"
FontIconGlyph="&#xE756;">
<StackPanel Orientation="Horizontal"
Spacing="5">
<!-- Select profile to add -->
<ComboBox x:Name="AddProfileComboBox"
MinWidth="{StaticResource StandardBoxMinWidth}"
ItemsSource="{x:Bind ViewModel.AvailableProfiles, Mode=OneWay}"
SelectedItem="{x:Bind ViewModel.SelectedProfile, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="model:Profile">
<Grid HorizontalAlignment="Stretch"
ColumnSpacing="8">
<Grid.ColumnDefinitions>
<!-- icon -->
<ColumnDefinition Width="16" />
<!-- profile name -->
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<IconSourceElement Grid.Column="0"
Width="16"
Height="16"
IconSource="{x:Bind mtu:IconPathConverter.IconSourceWUX(EvaluatedIcon), Mode=OneTime}" />
<TextBlock Grid.Column="1"
Text="{x:Bind Name}" />
</Grid>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<Button x:Name="AddProfileButton"
x:Uid="NewTabMenu_AddProfileButton"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Click="AddProfileButton_Clicked">
<Button.Content>
<FontIcon FontSize="{StaticResource StandardIconSize}"
Glyph="&#xE710;" />
</Button.Content>
</Button>
</StackPanel>
</local:SettingContainer>
<!-- Add Separator -->
<local:SettingContainer x:Uid="NewTabMenu_AddSeparator"
FontIconGlyph="&#xE76f;">
<Button x:Name="AddSeparatorButton"
x:Uid="NewTabMenu_AddSeparatorButton"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Click="AddSeparatorButton_Clicked">
<Button.Content>
<FontIcon FontSize="{StaticResource StandardIconSize}"
Glyph="&#xE710;" />
</Button.Content>
</Button>
</local:SettingContainer>
<!-- Add Folder -->
<local:SettingContainer x:Uid="NewTabMenu_AddFolder"
FontIconGlyph="&#xF12B;">
<StackPanel Orientation="Horizontal"
Spacing="5">
<TextBox x:Name="FolderNameTextBox"
x:Uid="NewTabMenu_AddFolder_FolderName"
MinWidth="{StaticResource StandardBoxMinWidth}"
KeyDown="AddFolderNameTextBox_KeyDown"
Text="{x:Bind ViewModel.AddFolderName, Mode=TwoWay}"
TextChanged="AddFolderNameTextBox_TextChanged" />
<Button x:Name="AddFolderButton"
x:Uid="NewTabMenu_AddFolderButton"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Click="AddFolderButton_Clicked"
IsEnabled="False">
<Button.Content>
<FontIcon FontSize="{StaticResource StandardIconSize}"
Glyph="&#xE710;" />
</Button.Content>
</Button>
</StackPanel>
</local:SettingContainer>
<!-- Add Match Profiles -->
<local:SettingContainer x:Uid="NewTabMenu_AddMatchProfiles"
FontIconGlyph="&#xE748;"
Style="{StaticResource ExpanderSettingContainerStyle}">
<StackPanel Spacing="10">
<TextBox x:Uid="NewTabMenu_AddMatchProfiles_Name"
Text="{x:Bind ViewModel.ProfileMatcherName, Mode=TwoWay}" />
<TextBox x:Uid="NewTabMenu_AddMatchProfiles_Source"
Text="{x:Bind ViewModel.ProfileMatcherSource, Mode=TwoWay}" />
<TextBox x:Uid="NewTabMenu_AddMatchProfiles_Commandline"
Text="{x:Bind ViewModel.ProfileMatcherCommandline, Mode=TwoWay}" />
<Button x:Name="AddMatchProfilesButton"
Click="AddMatchProfilesButton_Clicked">
<Button.Content>
<StackPanel Orientation="Horizontal">
<FontIcon FontSize="{StaticResource StandardIconSize}"
Glyph="&#xE710;" />
<TextBlock x:Uid="NewTabMenu_AddMatchProfilesTextBlock"
Style="{StaticResource IconButtonTextBlockStyle}" />
</StackPanel>
</Button.Content>
</Button>
</StackPanel>
</local:SettingContainer>
<!-- Add Remaining Profiles -->
<local:SettingContainer x:Uid="NewTabMenu_AddRemainingProfiles"
FontIconGlyph="&#xE902;">
<Button x:Name="AddRemainingProfilesButton"
x:Uid="NewTabMenu_AddRemainingProfilesButton"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Click="AddRemainingProfilesButton_Clicked"
IsEnabled="{x:Bind ViewModel.IsRemainingProfilesEntryMissing, Mode=OneWay}">
<Button.Content>
<FontIcon FontSize="{StaticResource StandardIconSize}"
Glyph="&#xE710;" />
</Button.Content>
</Button>
</local:SettingContainer>
</StackPanel>
</Grid>
</Page>

View File

@ -0,0 +1,812 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "NewTabMenuViewModel.h"
#include <LibraryResources.h>
#include "NewTabMenuViewModel.g.cpp"
#include "FolderTreeViewEntry.g.cpp"
#include "NewTabMenuEntryViewModel.g.cpp"
#include "ProfileEntryViewModel.g.cpp"
#include "ActionEntryViewModel.g.cpp"
#include "SeparatorEntryViewModel.g.cpp"
#include "FolderEntryViewModel.g.cpp"
#include "MatchProfilesEntryViewModel.g.cpp"
#include "RemainingProfilesEntryViewModel.g.cpp"
using namespace winrt::Windows::UI::Xaml::Navigation;
using namespace winrt::Windows::Foundation;
using namespace winrt::Windows::Foundation::Collections;
using namespace winrt::Microsoft::Terminal::Settings::Model;
using namespace winrt::Windows::UI::Xaml::Data;
namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
static IObservableVector<Editor::NewTabMenuEntryViewModel> _ConvertToViewModelEntries(const IVector<Model::NewTabMenuEntry>& settingsModelEntries, const Model::CascadiaSettings& settings)
{
std::vector<Editor::NewTabMenuEntryViewModel> result{};
if (!settingsModelEntries)
{
return single_threaded_observable_vector<Editor::NewTabMenuEntryViewModel>(std::move(result));
}
for (const auto& entry : settingsModelEntries)
{
switch (entry.Type())
{
case NewTabMenuEntryType::Profile:
{
// If the Profile isn't set, this is an invalid entry. Skip it.
if (const auto& profileEntry = entry.as<Model::ProfileEntry>(); profileEntry.Profile())
{
result.push_back(make<ProfileEntryViewModel>(profileEntry));
}
break;
}
case NewTabMenuEntryType::Action:
{
if (const auto& actionEntry = entry.as<Model::ActionEntry>())
{
result.push_back(make<ActionEntryViewModel>(actionEntry, settings));
}
break;
}
case NewTabMenuEntryType::Separator:
{
if (const auto& separatorEntry = entry.as<Model::SeparatorEntry>())
{
result.push_back(make<SeparatorEntryViewModel>(separatorEntry));
}
break;
}
case NewTabMenuEntryType::Folder:
{
if (const auto& folderEntry = entry.as<Model::FolderEntry>())
{
// The ctor will convert the children of the folder to view models
result.push_back(make<FolderEntryViewModel>(folderEntry, settings));
}
break;
}
case NewTabMenuEntryType::MatchProfiles:
{
if (const auto& matchProfilesEntry = entry.as<Model::MatchProfilesEntry>())
{
result.push_back(make<MatchProfilesEntryViewModel>(matchProfilesEntry));
}
break;
}
case NewTabMenuEntryType::RemainingProfiles:
{
if (const auto& remainingProfilesEntry = entry.as<Model::RemainingProfilesEntry>())
{
result.push_back(make<RemainingProfilesEntryViewModel>(remainingProfilesEntry));
}
break;
}
case NewTabMenuEntryType::Invalid:
default:
break;
}
}
return single_threaded_observable_vector<Editor::NewTabMenuEntryViewModel>(std::move(result));
}
bool NewTabMenuViewModel::IsRemainingProfilesEntryMissing() const
{
return _IsRemainingProfilesEntryMissing(_rootEntries);
}
bool NewTabMenuViewModel::_IsRemainingProfilesEntryMissing(const IVector<Editor::NewTabMenuEntryViewModel>& entries)
{
for (const auto& entry : entries)
{
switch (entry.Type())
{
case NewTabMenuEntryType::RemainingProfiles:
{
return false;
}
case NewTabMenuEntryType::Folder:
{
if (!_IsRemainingProfilesEntryMissing(entry.as<Editor::FolderEntryViewModel>().Entries()))
{
return false;
}
break;
}
default:
break;
}
}
return true;
}
bool NewTabMenuViewModel::IsFolderView() const noexcept
{
return _CurrentFolder != nullptr;
}
NewTabMenuViewModel::NewTabMenuViewModel(Model::CascadiaSettings settings)
{
UpdateSettings(settings);
// Add a property changed handler to our own property changed event.
// This propagates changes from the settings model to anybody listening to our
// unique view model members.
PropertyChanged([this](auto&&, const PropertyChangedEventArgs& args) {
const auto viewModelProperty{ args.PropertyName() };
if (viewModelProperty == L"AvailableProfiles")
{
_NotifyChanges(L"SelectedProfile");
}
else if (viewModelProperty == L"CurrentFolder")
{
if (_CurrentFolder)
{
CurrentFolderName(_CurrentFolder.Name());
_CurrentFolder.PropertyChanged({ this, &NewTabMenuViewModel::_FolderPropertyChanged });
}
_NotifyChanges(L"IsFolderView", L"CurrentView");
}
});
}
void NewTabMenuViewModel::_FolderPropertyChanged(const IInspectable& /*sender*/, const Windows::UI::Xaml::Data::PropertyChangedEventArgs& args)
{
const auto viewModelProperty{ args.PropertyName() };
if (viewModelProperty == L"Name")
{
// FolderTree needs to be updated when a folder is renamed
_folderTreeCache = nullptr;
}
}
hstring NewTabMenuViewModel::CurrentFolderName() const
{
if (!_CurrentFolder)
{
return {};
}
return _CurrentFolder.Name();
}
void NewTabMenuViewModel::CurrentFolderName(const hstring& value)
{
if (_CurrentFolder && _CurrentFolder.Name() != value)
{
_CurrentFolder.Name(value);
_NotifyChanges(L"CurrentFolderName");
}
}
bool NewTabMenuViewModel::CurrentFolderInlining() const
{
if (!_CurrentFolder)
{
return {};
}
return _CurrentFolder.Inlining();
}
void NewTabMenuViewModel::CurrentFolderInlining(bool value)
{
if (_CurrentFolder && _CurrentFolder.Inlining() != value)
{
_CurrentFolder.Inlining(value);
_NotifyChanges(L"CurrentFolderInlining");
}
}
bool NewTabMenuViewModel::CurrentFolderAllowEmpty() const
{
if (!_CurrentFolder)
{
return {};
}
return _CurrentFolder.AllowEmpty();
}
void NewTabMenuViewModel::CurrentFolderAllowEmpty(bool value)
{
if (_CurrentFolder && _CurrentFolder.AllowEmpty() != value)
{
_CurrentFolder.AllowEmpty(value);
_NotifyChanges(L"CurrentFolderAllowEmpty");
}
}
Windows::Foundation::Collections::IObservableVector<Editor::NewTabMenuEntryViewModel> NewTabMenuViewModel::CurrentView() const
{
if (!_CurrentFolder)
{
return _rootEntries;
}
return _CurrentFolder.Entries();
}
static bool _FindFolderPathByName(const IVector<Editor::NewTabMenuEntryViewModel>& entries, const hstring& name, std::vector<Editor::FolderEntryViewModel>& result)
{
for (const auto& entry : entries)
{
if (const auto& folderVM = entry.try_as<Editor::FolderEntryViewModel>())
{
result.push_back(folderVM);
if (folderVM.Name() == name)
{
// Found the folder
return true;
}
else if (_FindFolderPathByName(folderVM.Entries(), name, result))
{
// Found the folder in the children of this folder
return true;
}
else
{
// This folder and its descendants are not the folder we're looking for
result.pop_back();
}
}
}
return false;
}
IVector<Editor::FolderEntryViewModel> NewTabMenuViewModel::FindFolderPathByName(const hstring& name)
{
std::vector<Editor::FolderEntryViewModel> entries;
_FindFolderPathByName(_rootEntries, name, entries);
return single_threaded_vector<Editor::FolderEntryViewModel>(std::move(entries));
}
void NewTabMenuViewModel::UpdateSettings(const Model::CascadiaSettings& settings)
{
_Settings = settings;
_NotifyChanges(L"AvailableProfiles");
SelectedProfile(AvailableProfiles().GetAt(0));
_rootEntries = _ConvertToViewModelEntries(_Settings.GlobalSettings().NewTabMenu(), _Settings);
_rootEntriesChangedRevoker = _rootEntries.VectorChanged(winrt::auto_revoke, [this](auto&&, const IVectorChangedEventArgs& args) {
switch (args.CollectionChange())
{
case CollectionChange::Reset:
{
// fully replace settings model with view model structure
std::vector<Model::NewTabMenuEntry> modelEntries;
for (const auto& entry : _rootEntries)
{
modelEntries.push_back(NewTabMenuEntryViewModel::GetModel(entry));
}
_Settings.GlobalSettings().NewTabMenu(single_threaded_vector<Model::NewTabMenuEntry>(std::move(modelEntries)));
return;
}
case CollectionChange::ItemInserted:
{
const auto& insertedEntryVM = _rootEntries.GetAt(args.Index());
const auto& insertedEntry = NewTabMenuEntryViewModel::GetModel(insertedEntryVM);
_Settings.GlobalSettings().NewTabMenu().InsertAt(args.Index(), insertedEntry);
return;
}
case CollectionChange::ItemRemoved:
{
_Settings.GlobalSettings().NewTabMenu().RemoveAt(args.Index());
return;
}
case CollectionChange::ItemChanged:
{
const auto& modifiedEntry = _rootEntries.GetAt(args.Index());
_Settings.GlobalSettings().NewTabMenu().SetAt(args.Index(), NewTabMenuEntryViewModel::GetModel(modifiedEntry));
return;
}
}
});
}
void NewTabMenuViewModel::RequestReorderEntry(const Editor::NewTabMenuEntryViewModel& vm, bool goingUp)
{
uint32_t idx;
if (CurrentView().IndexOf(vm, idx))
{
if (goingUp && idx > 0)
{
CurrentView().RemoveAt(idx);
CurrentView().InsertAt(idx - 1, vm);
}
else if (!goingUp && idx < CurrentView().Size() - 1)
{
CurrentView().RemoveAt(idx);
CurrentView().InsertAt(idx + 1, vm);
}
}
}
void NewTabMenuViewModel::RequestDeleteEntry(const Editor::NewTabMenuEntryViewModel& vm)
{
uint32_t idx;
if (CurrentView().IndexOf(vm, idx))
{
CurrentView().RemoveAt(idx);
if (vm.try_as<Editor::FolderEntryViewModel>())
{
_folderTreeCache = nullptr;
}
}
}
void NewTabMenuViewModel::RequestMoveEntriesToFolder(const Windows::Foundation::Collections::IVector<Editor::NewTabMenuEntryViewModel>& entries, const Editor::FolderEntryViewModel& destinationFolder)
{
auto destination{ destinationFolder == nullptr ? _rootEntries : destinationFolder.Entries() };
for (auto&& e : entries)
{
// Don't move the folder into itself (just skip over it)
if (e == destinationFolder)
{
continue;
}
// Remove entry from the current layer,
// and add it to the destination folder
RequestDeleteEntry(e);
destination.Append(e);
}
}
Editor::NewTabMenuEntryViewModel NewTabMenuViewModel::RequestAddSelectedProfileEntry()
{
if (_SelectedProfile)
{
Model::ProfileEntry profileEntry;
profileEntry.Profile(_SelectedProfile);
const auto& entryVM = make<ProfileEntryViewModel>(profileEntry);
CurrentView().Append(entryVM);
_PrintAll();
return entryVM;
}
return nullptr;
}
Editor::NewTabMenuEntryViewModel NewTabMenuViewModel::RequestAddSeparatorEntry()
{
Model::SeparatorEntry separatorEntry;
const auto& entryVM = make<SeparatorEntryViewModel>(separatorEntry);
CurrentView().Append(entryVM);
_PrintAll();
return entryVM;
}
Editor::NewTabMenuEntryViewModel NewTabMenuViewModel::RequestAddFolderEntry()
{
Model::FolderEntry folderEntry;
folderEntry.Name(_AddFolderName);
const auto& entryVM = make<FolderEntryViewModel>(folderEntry, _Settings);
CurrentView().Append(entryVM);
// Reset state after adding the entry
AddFolderName({});
_folderTreeCache = nullptr;
_PrintAll();
return entryVM;
}
Editor::NewTabMenuEntryViewModel NewTabMenuViewModel::RequestAddProfileMatcherEntry()
{
Model::MatchProfilesEntry matchProfilesEntry;
matchProfilesEntry.Name(_ProfileMatcherName);
matchProfilesEntry.Source(_ProfileMatcherSource);
matchProfilesEntry.Commandline(_ProfileMatcherCommandline);
const auto& entryVM = make<MatchProfilesEntryViewModel>(matchProfilesEntry);
CurrentView().Append(entryVM);
// Clear the fields after adding the entry
ProfileMatcherName({});
ProfileMatcherSource({});
ProfileMatcherCommandline({});
_PrintAll();
return entryVM;
}
Editor::NewTabMenuEntryViewModel NewTabMenuViewModel::RequestAddRemainingProfilesEntry()
{
Model::RemainingProfilesEntry remainingProfilesEntry;
const auto& entryVM = make<RemainingProfilesEntryViewModel>(remainingProfilesEntry);
CurrentView().Append(entryVM);
_NotifyChanges(L"IsRemainingProfilesEntryMissing");
_PrintAll();
return entryVM;
}
void NewTabMenuViewModel::GenerateFolderTree()
{
if (!_folderTreeCache)
{
// Add the root folder
auto root = winrt::make<FolderTreeViewEntry>(nullptr);
for (const auto&& entry : _rootEntries)
{
if (entry.Type() == NewTabMenuEntryType::Folder)
{
root.Children().Append(winrt::make<FolderTreeViewEntry>(entry.as<Editor::FolderEntryViewModel>()));
}
}
std::vector<Editor::FolderTreeViewEntry> folderTreeCache;
folderTreeCache.emplace_back(std::move(root));
_folderTreeCache = single_threaded_observable_vector<Editor::FolderTreeViewEntry>(std::move(folderTreeCache));
_NotifyChanges(L"FolderTree");
}
}
Collections::IObservableVector<Editor::FolderTreeViewEntry> NewTabMenuViewModel::FolderTree() const
{
// We could do this...
// if (!_folderTreeCache){ GenerateFolderTree(); }
// But FolderTree() gets called when we open the page.
// Instead, we generate the tree as needed using GenerateFolderTree()
// which caches the tree.
return _folderTreeCache;
}
// This recursively constructs the FolderTree
FolderTreeViewEntry::FolderTreeViewEntry(Editor::FolderEntryViewModel folderEntry) :
_folderEntry{ folderEntry },
_Children{ single_threaded_observable_vector<Editor::FolderTreeViewEntry>() }
{
if (!_folderEntry)
{
return;
}
for (const auto&& entry : _folderEntry.Entries())
{
if (entry.Type() == NewTabMenuEntryType::Folder)
{
_Children.Append(winrt::make<FolderTreeViewEntry>(entry.as<Editor::FolderEntryViewModel>()));
}
}
}
hstring FolderTreeViewEntry::Name() const
{
if (!_folderEntry)
{
return RS_(L"NewTabMenu_RootFolderName");
}
return _folderEntry.Name();
}
hstring FolderTreeViewEntry::Icon() const
{
if (!_folderEntry)
{
return {};
}
return _folderEntry.Icon();
}
void NewTabMenuViewModel::_PrintAll()
{
#ifdef _DEBUG
OutputDebugString(L"---Model:---\n");
_PrintModel(_Settings.GlobalSettings().NewTabMenu());
OutputDebugString(L"\n");
OutputDebugString(L"---VM:---\n");
_PrintVM(_rootEntries);
OutputDebugString(L"\n");
#endif
}
#ifdef _DEBUG
void NewTabMenuViewModel::_PrintModel(Windows::Foundation::Collections::IVector<Model::NewTabMenuEntry> list, std::wstring prefix)
{
if (!list)
{
return;
}
for (auto&& e : list)
{
_PrintModel(e, prefix);
}
}
void NewTabMenuViewModel::_PrintModel(const Model::NewTabMenuEntry& e, std::wstring prefix)
{
switch (e.Type())
{
case NewTabMenuEntryType::Profile:
{
const auto& pe = e.as<Model::ProfileEntry>();
OutputDebugString(fmt::format(L"{}Profile: {}\n", prefix, pe.Profile().Name()).c_str());
break;
}
case NewTabMenuEntryType::Action:
{
const auto& actionEntry = e.as<Model::ActionEntry>();
OutputDebugString(fmt::format(L"{}Action: {}\n", prefix, actionEntry.ActionId()).c_str());
break;
}
case NewTabMenuEntryType::Separator:
{
OutputDebugString(fmt::format(L"{}Separator\n", prefix).c_str());
break;
}
case NewTabMenuEntryType::Folder:
{
const auto& fe = e.as<Model::FolderEntry>();
OutputDebugString(fmt::format(L"{}Folder: {}\n", prefix, fe.Name()).c_str());
_PrintModel(fe.RawEntries(), prefix + L" ");
break;
}
case NewTabMenuEntryType::MatchProfiles:
{
const auto& matchProfilesEntry = e.as<Model::MatchProfilesEntry>();
OutputDebugString(fmt::format(L"{}MatchProfiles: {}\n", prefix, matchProfilesEntry.Name()).c_str());
break;
}
case NewTabMenuEntryType::RemainingProfiles:
{
OutputDebugString(fmt::format(L"{}RemainingProfiles\n", prefix).c_str());
break;
}
default:
break;
}
}
void NewTabMenuViewModel::_PrintVM(Windows::Foundation::Collections::IVector<Editor::NewTabMenuEntryViewModel> list, std::wstring prefix)
{
if (!list)
{
return;
}
for (auto&& e : list)
{
_PrintVM(e, prefix);
}
}
void NewTabMenuViewModel::_PrintVM(const Editor::NewTabMenuEntryViewModel& e, std::wstring prefix)
{
switch (e.Type())
{
case NewTabMenuEntryType::Profile:
{
const auto& pe = e.as<Editor::ProfileEntryViewModel>();
OutputDebugString(fmt::format(L"{}Profile: {}\n", prefix, pe.ProfileEntry().Profile().Name()).c_str());
break;
}
case NewTabMenuEntryType::Action:
{
const auto& actionEntry = e.as<Editor::ActionEntryViewModel>();
OutputDebugString(fmt::format(L"{}Action: {}\n", prefix, actionEntry.ActionEntry().ActionId()).c_str());
break;
}
case NewTabMenuEntryType::Separator:
{
OutputDebugString(fmt::format(L"{}Separator\n", prefix).c_str());
break;
}
case NewTabMenuEntryType::Folder:
{
const auto& fe = e.as<Editor::FolderEntryViewModel>();
OutputDebugString(fmt::format(L"{}Folder: {}\n", prefix, fe.Name()).c_str());
_PrintVM(fe.Entries(), prefix + L" ");
break;
}
case NewTabMenuEntryType::MatchProfiles:
{
const auto& matchProfilesEntry = e.as<Editor::MatchProfilesEntryViewModel>();
OutputDebugString(fmt::format(L"{}MatchProfiles: {}\n", prefix, matchProfilesEntry.DisplayText()).c_str());
break;
}
case NewTabMenuEntryType::RemainingProfiles:
{
OutputDebugString(fmt::format(L"{}RemainingProfiles\n", prefix).c_str());
break;
}
default:
break;
}
}
#endif
NewTabMenuEntryViewModel::NewTabMenuEntryViewModel(const NewTabMenuEntryType type) noexcept :
_Type{ type }
{
}
Model::NewTabMenuEntry NewTabMenuEntryViewModel::GetModel(const Editor::NewTabMenuEntryViewModel& viewModel)
{
switch (viewModel.Type())
{
case NewTabMenuEntryType::Profile:
{
const auto& projVM = viewModel.as<Editor::ProfileEntryViewModel>();
return get_self<ProfileEntryViewModel>(projVM)->ProfileEntry();
}
case NewTabMenuEntryType::Action:
{
const auto& projVM = viewModel.as<Editor::ActionEntryViewModel>();
return get_self<ActionEntryViewModel>(projVM)->ActionEntry();
}
case NewTabMenuEntryType::Separator:
{
const auto& projVM = viewModel.as<Editor::SeparatorEntryViewModel>();
return get_self<SeparatorEntryViewModel>(projVM)->SeparatorEntry();
}
case NewTabMenuEntryType::Folder:
{
const auto& projVM = viewModel.as<Editor::FolderEntryViewModel>();
return get_self<FolderEntryViewModel>(projVM)->FolderEntry();
}
case NewTabMenuEntryType::MatchProfiles:
{
const auto& projVM = viewModel.as<Editor::MatchProfilesEntryViewModel>();
return get_self<MatchProfilesEntryViewModel>(projVM)->MatchProfilesEntry();
}
case NewTabMenuEntryType::RemainingProfiles:
{
const auto& projVM = viewModel.as<Editor::RemainingProfilesEntryViewModel>();
return get_self<RemainingProfilesEntryViewModel>(projVM)->RemainingProfilesEntry();
}
case NewTabMenuEntryType::Invalid:
default:
return nullptr;
}
}
ProfileEntryViewModel::ProfileEntryViewModel(Model::ProfileEntry profileEntry) :
ProfileEntryViewModelT<ProfileEntryViewModel, NewTabMenuEntryViewModel>(Model::NewTabMenuEntryType::Profile),
_ProfileEntry{ profileEntry }
{
}
ActionEntryViewModel::ActionEntryViewModel(Model::ActionEntry actionEntry, Model::CascadiaSettings settings) :
ActionEntryViewModelT<ActionEntryViewModel, NewTabMenuEntryViewModel>(Model::NewTabMenuEntryType::Action),
_ActionEntry{ actionEntry },
_Settings{ settings }
{
}
hstring ActionEntryViewModel::DisplayText() const
{
assert(_Settings);
const auto actionID = _ActionEntry.ActionId();
if (const auto& action = _Settings.ActionMap().GetActionByID(actionID))
{
return action.Name();
}
return hstring{ fmt::format(L"{}: {}", RS_(L"NewTabMenu_ActionNotFound"), actionID) };
}
hstring ActionEntryViewModel::Icon() const
{
assert(_Settings);
const auto actionID = _ActionEntry.ActionId();
if (const auto& action = _Settings.ActionMap().GetActionByID(actionID))
{
return action.IconPath();
}
return {};
}
SeparatorEntryViewModel::SeparatorEntryViewModel(Model::SeparatorEntry separatorEntry) :
SeparatorEntryViewModelT<SeparatorEntryViewModel, NewTabMenuEntryViewModel>(Model::NewTabMenuEntryType::Separator),
_SeparatorEntry{ separatorEntry }
{
}
FolderEntryViewModel::FolderEntryViewModel(Model::FolderEntry folderEntry) :
FolderEntryViewModel(folderEntry, nullptr) {}
FolderEntryViewModel::FolderEntryViewModel(Model::FolderEntry folderEntry, Model::CascadiaSettings settings) :
FolderEntryViewModelT<FolderEntryViewModel, NewTabMenuEntryViewModel>(Model::NewTabMenuEntryType::Folder),
_FolderEntry{ folderEntry },
_Settings{ settings }
{
_Entries = _ConvertToViewModelEntries(_FolderEntry.RawEntries(), _Settings);
_entriesChangedRevoker = _Entries.VectorChanged(winrt::auto_revoke, [this](auto&&, const IVectorChangedEventArgs& args) {
switch (args.CollectionChange())
{
case CollectionChange::Reset:
{
// fully replace settings model with _Entries
std::vector<Model::NewTabMenuEntry> modelEntries;
for (const auto& entry : _Entries)
{
modelEntries.push_back(NewTabMenuEntryViewModel::GetModel(entry));
}
_FolderEntry.RawEntries(single_threaded_vector<Model::NewTabMenuEntry>(std::move(modelEntries)));
return;
}
case CollectionChange::ItemInserted:
{
const auto& insertedEntryVM = _Entries.GetAt(args.Index());
const auto& insertedEntry = NewTabMenuEntryViewModel::GetModel(insertedEntryVM);
if (!_FolderEntry.RawEntries())
{
_FolderEntry.RawEntries(single_threaded_vector<Model::NewTabMenuEntry>());
}
_FolderEntry.RawEntries().InsertAt(args.Index(), insertedEntry);
return;
}
case CollectionChange::ItemRemoved:
{
_FolderEntry.RawEntries().RemoveAt(args.Index());
return;
}
case CollectionChange::ItemChanged:
{
const auto& modifiedEntry = _Entries.GetAt(args.Index());
_FolderEntry.RawEntries().SetAt(args.Index(), NewTabMenuEntryViewModel::GetModel(modifiedEntry));
return;
}
}
});
}
bool FolderEntryViewModel::Inlining() const
{
return _FolderEntry.Inlining() == FolderEntryInlining::Auto;
}
void FolderEntryViewModel::Inlining(bool value)
{
const auto valueAsEnum = value ? FolderEntryInlining::Auto : FolderEntryInlining::Never;
if (_FolderEntry.Inlining() != valueAsEnum)
{
_FolderEntry.Inlining(valueAsEnum);
_NotifyChanges(L"Inlining");
}
};
MatchProfilesEntryViewModel::MatchProfilesEntryViewModel(Model::MatchProfilesEntry matchProfilesEntry) :
MatchProfilesEntryViewModelT<MatchProfilesEntryViewModel, NewTabMenuEntryViewModel>(Model::NewTabMenuEntryType::MatchProfiles),
_MatchProfilesEntry{ matchProfilesEntry }
{
}
hstring MatchProfilesEntryViewModel::DisplayText() const
{
std::wstring displayText;
if (const auto profileName = _MatchProfilesEntry.Name(); !profileName.empty())
{
fmt::format_to(std::back_inserter(displayText), FMT_COMPILE(L"profile: {}, "), profileName);
}
if (const auto commandline = _MatchProfilesEntry.Commandline(); !commandline.empty())
{
fmt::format_to(std::back_inserter(displayText), FMT_COMPILE(L"commandline: {}, "), commandline);
}
if (const auto source = _MatchProfilesEntry.Source(); !source.empty())
{
fmt::format_to(std::back_inserter(displayText), FMT_COMPILE(L"source: {}, "), source);
}
// Chop off the last ", "
displayText.resize(displayText.size() - 2);
return winrt::hstring{ displayText };
}
RemainingProfilesEntryViewModel::RemainingProfilesEntryViewModel(Model::RemainingProfilesEntry remainingProfilesEntry) :
RemainingProfilesEntryViewModelT<RemainingProfilesEntryViewModel, NewTabMenuEntryViewModel>(Model::NewTabMenuEntryType::RemainingProfiles),
_RemainingProfilesEntry{ remainingProfilesEntry }
{
}
}

View File

@ -0,0 +1,183 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#include "NewTabMenuViewModel.g.h"
#include "FolderTreeViewEntry.g.h"
#include "NewTabMenuEntryViewModel.g.h"
#include "ProfileEntryViewModel.g.h"
#include "ActionEntryViewModel.g.h"
#include "SeparatorEntryViewModel.g.h"
#include "FolderEntryViewModel.g.h"
#include "MatchProfilesEntryViewModel.g.h"
#include "RemainingProfilesEntryViewModel.g.h"
#include "ProfileViewModel.h"
#include "ViewModelHelpers.h"
#include "Utils.h"
namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
struct NewTabMenuViewModel : NewTabMenuViewModelT<NewTabMenuViewModel>, ViewModelHelper<NewTabMenuViewModel>
{
public:
NewTabMenuViewModel(Model::CascadiaSettings settings);
void UpdateSettings(const Model::CascadiaSettings& settings);
void GenerateFolderTree();
Windows::Foundation::Collections::IVector<Editor::FolderEntryViewModel> FindFolderPathByName(const hstring& name);
bool IsRemainingProfilesEntryMissing() const;
bool IsFolderView() const noexcept;
void RequestReorderEntry(const Editor::NewTabMenuEntryViewModel& vm, bool goingUp);
void RequestDeleteEntry(const Editor::NewTabMenuEntryViewModel& vm);
void RequestMoveEntriesToFolder(const Windows::Foundation::Collections::IVector<Editor::NewTabMenuEntryViewModel>& entries, const Editor::FolderEntryViewModel& destinationFolder);
Editor::NewTabMenuEntryViewModel RequestAddSelectedProfileEntry();
Editor::NewTabMenuEntryViewModel RequestAddSeparatorEntry();
Editor::NewTabMenuEntryViewModel RequestAddFolderEntry();
Editor::NewTabMenuEntryViewModel RequestAddProfileMatcherEntry();
Editor::NewTabMenuEntryViewModel RequestAddRemainingProfilesEntry();
hstring CurrentFolderName() const;
void CurrentFolderName(const hstring& value);
bool CurrentFolderInlining() const;
void CurrentFolderInlining(bool value);
bool CurrentFolderAllowEmpty() const;
void CurrentFolderAllowEmpty(bool value);
Windows::Foundation::Collections::IObservableVector<Model::Profile> AvailableProfiles() const { return _Settings.AllProfiles(); }
Windows::Foundation::Collections::IObservableVector<Editor::FolderTreeViewEntry> FolderTree() const;
Windows::Foundation::Collections::IObservableVector<Editor::NewTabMenuEntryViewModel> CurrentView() const;
VIEW_MODEL_OBSERVABLE_PROPERTY(Editor::FolderEntryViewModel, CurrentFolder, nullptr);
VIEW_MODEL_OBSERVABLE_PROPERTY(Editor::FolderTreeViewEntry, CurrentFolderTreeViewSelectedItem, nullptr);
// Bound to the UI to create new entries
VIEW_MODEL_OBSERVABLE_PROPERTY(Model::Profile, SelectedProfile, nullptr);
VIEW_MODEL_OBSERVABLE_PROPERTY(hstring, ProfileMatcherName);
VIEW_MODEL_OBSERVABLE_PROPERTY(hstring, ProfileMatcherSource);
VIEW_MODEL_OBSERVABLE_PROPERTY(hstring, ProfileMatcherCommandline);
VIEW_MODEL_OBSERVABLE_PROPERTY(hstring, AddFolderName);
private:
Model::CascadiaSettings _Settings{ nullptr };
Windows::Foundation::Collections::IObservableVector<Editor::NewTabMenuEntryViewModel> _rootEntries;
Windows::Foundation::Collections::IObservableVector<Editor::FolderTreeViewEntry> _folderTreeCache;
Windows::Foundation::Collections::IObservableVector<Editor::NewTabMenuEntryViewModel>::VectorChanged_revoker _rootEntriesChangedRevoker;
static bool _IsRemainingProfilesEntryMissing(const Windows::Foundation::Collections::IVector<Editor::NewTabMenuEntryViewModel>& entries);
void _FolderPropertyChanged(const IInspectable& sender, const Windows::UI::Xaml::Data::PropertyChangedEventArgs& args);
void _PrintAll();
#ifdef _DEBUG
void _PrintModel(Windows::Foundation::Collections::IVector<Model::NewTabMenuEntry> list, std::wstring prefix = L"");
void _PrintModel(const Model::NewTabMenuEntry& e, std::wstring prefix = L"");
void _PrintVM(Windows::Foundation::Collections::IVector<Editor::NewTabMenuEntryViewModel> list, std::wstring prefix = L"");
void _PrintVM(const Editor::NewTabMenuEntryViewModel& vm, std::wstring prefix = L"");
#endif
};
struct FolderTreeViewEntry : FolderTreeViewEntryT<FolderTreeViewEntry>
{
public:
FolderTreeViewEntry(Editor::FolderEntryViewModel folderEntry);
hstring Name() const;
hstring Icon() const;
Editor::FolderEntryViewModel FolderEntryVM() { return _folderEntry; }
WINRT_PROPERTY(Windows::Foundation::Collections::IObservableVector<Microsoft::Terminal::Settings::Editor::FolderTreeViewEntry>, Children);
private:
Editor::FolderEntryViewModel _folderEntry;
};
struct NewTabMenuEntryViewModel : NewTabMenuEntryViewModelT<NewTabMenuEntryViewModel>, ViewModelHelper<NewTabMenuEntryViewModel>
{
public:
static Model::NewTabMenuEntry GetModel(const Editor::NewTabMenuEntryViewModel& viewModel);
VIEW_MODEL_OBSERVABLE_PROPERTY(Model::NewTabMenuEntryType, Type, Model::NewTabMenuEntryType::Invalid);
protected:
explicit NewTabMenuEntryViewModel(const Model::NewTabMenuEntryType type) noexcept;
};
struct ProfileEntryViewModel : ProfileEntryViewModelT<ProfileEntryViewModel, NewTabMenuEntryViewModel>
{
public:
ProfileEntryViewModel(Model::ProfileEntry profileEntry);
VIEW_MODEL_OBSERVABLE_PROPERTY(Model::ProfileEntry, ProfileEntry, nullptr);
};
struct ActionEntryViewModel : ActionEntryViewModelT<ActionEntryViewModel, NewTabMenuEntryViewModel>
{
public:
ActionEntryViewModel(Model::ActionEntry actionEntry, Model::CascadiaSettings settings);
hstring DisplayText() const;
hstring Icon() const;
VIEW_MODEL_OBSERVABLE_PROPERTY(Model::ActionEntry, ActionEntry, nullptr);
private:
Model::CascadiaSettings _Settings;
};
struct SeparatorEntryViewModel : SeparatorEntryViewModelT<SeparatorEntryViewModel, NewTabMenuEntryViewModel>
{
public:
SeparatorEntryViewModel(Model::SeparatorEntry separatorEntry);
VIEW_MODEL_OBSERVABLE_PROPERTY(Model::SeparatorEntry, SeparatorEntry, nullptr);
};
struct FolderEntryViewModel : FolderEntryViewModelT<FolderEntryViewModel, NewTabMenuEntryViewModel>
{
public:
FolderEntryViewModel(Model::FolderEntry folderEntry, Model::CascadiaSettings settings);
explicit FolderEntryViewModel(Model::FolderEntry folderEntry);
bool Inlining() const;
void Inlining(bool value);
GETSET_OBSERVABLE_PROJECTED_SETTING(_FolderEntry, Name);
GETSET_OBSERVABLE_PROJECTED_SETTING(_FolderEntry, Icon);
GETSET_OBSERVABLE_PROJECTED_SETTING(_FolderEntry, AllowEmpty);
VIEW_MODEL_OBSERVABLE_PROPERTY(Windows::Foundation::Collections::IObservableVector<Editor::NewTabMenuEntryViewModel>, Entries);
VIEW_MODEL_OBSERVABLE_PROPERTY(Model::FolderEntry, FolderEntry, nullptr);
private:
Windows::Foundation::Collections::IObservableVector<Editor::NewTabMenuEntryViewModel>::VectorChanged_revoker _entriesChangedRevoker;
Model::CascadiaSettings _Settings;
};
struct MatchProfilesEntryViewModel : MatchProfilesEntryViewModelT<MatchProfilesEntryViewModel, NewTabMenuEntryViewModel>
{
public:
MatchProfilesEntryViewModel(Model::MatchProfilesEntry matchProfilesEntry);
hstring DisplayText() const;
VIEW_MODEL_OBSERVABLE_PROPERTY(Model::MatchProfilesEntry, MatchProfilesEntry, nullptr);
};
struct RemainingProfilesEntryViewModel : RemainingProfilesEntryViewModelT<RemainingProfilesEntryViewModel, NewTabMenuEntryViewModel>
{
public:
RemainingProfilesEntryViewModel(Model::RemainingProfilesEntry remainingProfilesEntry);
VIEW_MODEL_OBSERVABLE_PROPERTY(Model::RemainingProfilesEntry, RemainingProfilesEntry, nullptr);
};
};
namespace winrt::Microsoft::Terminal::Settings::Editor::factory_implementation
{
BASIC_FACTORY(NewTabMenuViewModel);
BASIC_FACTORY(FolderTreeViewEntry);
BASIC_FACTORY(ProfileEntryViewModel);
BASIC_FACTORY(ActionEntryViewModel);
BASIC_FACTORY(SeparatorEntryViewModel);
BASIC_FACTORY(FolderEntryViewModel);
BASIC_FACTORY(MatchProfilesEntryViewModel);
BASIC_FACTORY(RemainingProfilesEntryViewModel);
}

View File

@ -0,0 +1,103 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import "ProfileViewModel.idl";
namespace Microsoft.Terminal.Settings.Editor
{
[default_interface] runtimeclass FolderTreeViewEntry
{
FolderTreeViewEntry(FolderEntryViewModel folderEntry);
String Name { get; };
String Icon { get; };
FolderEntryViewModel FolderEntryVM { get; };
IObservableVector<Microsoft.Terminal.Settings.Editor.FolderTreeViewEntry> Children { get; };
}
runtimeclass NewTabMenuViewModel : Windows.UI.Xaml.Data.INotifyPropertyChanged
{
NewTabMenuViewModel(Microsoft.Terminal.Settings.Model.CascadiaSettings settings);
void UpdateSettings(Microsoft.Terminal.Settings.Model.CascadiaSettings settings);
void GenerateFolderTree();
Windows.Foundation.Collections.IVector<FolderEntryViewModel> FindFolderPathByName(String name);
FolderEntryViewModel CurrentFolder;
Boolean IsFolderView { get; };
FolderTreeViewEntry CurrentFolderTreeViewSelectedItem;
Boolean IsRemainingProfilesEntryMissing { get; };
IObservableVector<NewTabMenuEntryViewModel> CurrentView { get; };
IObservableVector<Microsoft.Terminal.Settings.Model.Profile> AvailableProfiles { get; };
IObservableVector<FolderTreeViewEntry> FolderTree { get; };
Microsoft.Terminal.Settings.Model.Profile SelectedProfile;
String CurrentFolderName;
Boolean CurrentFolderInlining;
Boolean CurrentFolderAllowEmpty;
String ProfileMatcherName;
String ProfileMatcherSource;
String ProfileMatcherCommandline;
String AddFolderName;
void RequestReorderEntry(NewTabMenuEntryViewModel vm, Boolean goingUp);
void RequestDeleteEntry(NewTabMenuEntryViewModel vm);
void RequestMoveEntriesToFolder(IVector<NewTabMenuEntryViewModel> entries, FolderEntryViewModel folderEntry);
NewTabMenuEntryViewModel RequestAddSelectedProfileEntry();
NewTabMenuEntryViewModel RequestAddSeparatorEntry();
NewTabMenuEntryViewModel RequestAddFolderEntry();
NewTabMenuEntryViewModel RequestAddProfileMatcherEntry();
NewTabMenuEntryViewModel RequestAddRemainingProfilesEntry();
}
[default_interface] unsealed runtimeclass NewTabMenuEntryViewModel : Windows.UI.Xaml.Data.INotifyPropertyChanged
{
Microsoft.Terminal.Settings.Model.NewTabMenuEntryType Type;
}
[default_interface] runtimeclass ProfileEntryViewModel : Microsoft.Terminal.Settings.Editor.NewTabMenuEntryViewModel
{
ProfileEntryViewModel(Microsoft.Terminal.Settings.Model.ProfileEntry profileEntry);
Microsoft.Terminal.Settings.Model.ProfileEntry ProfileEntry { get; };
}
[default_interface] runtimeclass ActionEntryViewModel : Microsoft.Terminal.Settings.Editor.NewTabMenuEntryViewModel
{
ActionEntryViewModel(Microsoft.Terminal.Settings.Model.ActionEntry actionEntry, Microsoft.Terminal.Settings.Model.CascadiaSettings settings);
Microsoft.Terminal.Settings.Model.ActionEntry ActionEntry { get; };
String DisplayText { get; };
String Icon { get; };
}
[default_interface] runtimeclass SeparatorEntryViewModel : Microsoft.Terminal.Settings.Editor.NewTabMenuEntryViewModel
{
SeparatorEntryViewModel(Microsoft.Terminal.Settings.Model.SeparatorEntry separatorEntry);
}
[default_interface] runtimeclass FolderEntryViewModel : Microsoft.Terminal.Settings.Editor.NewTabMenuEntryViewModel
{
FolderEntryViewModel(Microsoft.Terminal.Settings.Model.FolderEntry folderEntry, Microsoft.Terminal.Settings.Model.CascadiaSettings settings);
String Name;
String Icon;
Boolean Inlining;
Boolean AllowEmpty;
IObservableVector<Microsoft.Terminal.Settings.Editor.NewTabMenuEntryViewModel> Entries;
}
[default_interface] runtimeclass MatchProfilesEntryViewModel : Microsoft.Terminal.Settings.Editor.NewTabMenuEntryViewModel
{
MatchProfilesEntryViewModel(Microsoft.Terminal.Settings.Model.MatchProfilesEntry matchProfilesEntry);
String DisplayText { get; };
}
[default_interface] runtimeclass RemainingProfilesEntryViewModel : Microsoft.Terminal.Settings.Editor.NewTabMenuEntryViewModel
{
RemainingProfilesEntryViewModel(Microsoft.Terminal.Settings.Model.RemainingProfilesEntry remainingProfilesEntry);
}
}

View File

@ -1929,6 +1929,178 @@
<value>Non-monospace fonts:</value>
<comment>This is a label that is followed by a list of proportional fonts.</comment>
</data>
<data name="Nav_NewTabMenu.Content" xml:space="preserve">
<value>New Tab Menu</value>
<comment>Header for the "new tab menu" menu item. This navigates to a page that lets you see and modify settings related to the app's new tab menu (i.e. profile ordering, nested folders, dividers, etc.)</comment>
</data>
<data name="NewTabMenuEntry_Separator.Text" xml:space="preserve">
<value>&lt;Separator&gt;</value>
<comment>{Locked="&lt;"}, {Locked="&gt;"} Text label for an entry that represents a visual separator in a list.</comment>
</data>
<data name="NewTabMenuEntry_RemainingProfiles.Text" xml:space="preserve">
<value>&lt;Remaining profiles&gt;</value>
<comment>{Locked="&lt;"}{Locked="&gt;"} Text label for an entry that represents inserting any remaining profiles that have not been inserted.</comment>
</data>
<data name="NewTabMenuEntry_RemainingProfilesItem.[using:Windows.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>&lt;Remaining profiles&gt;</value>
<comment>{Locked="&lt;"}{Locked="&gt;"} Text label for an entry that represents inserting any remaining profiles that have not been inserted. Should match "NewTabMenuEntry_RemainingProfiles.Text".</comment>
</data>
<data name="NewTabMenu_AddProfile.Header" xml:space="preserve">
<value>Profile</value>
<comment>Header for a control that adds a terminal profile to the new tab menu.</comment>
</data>
<data name="NewTabMenu_AddMatchProfiles.Header" xml:space="preserve">
<value>Profile matcher</value>
<comment>Header for a control that adds a terminal profile matcher to the new tab menu. This entry adds profiles that match the given parameters.</comment>
</data>
<data name="NewTabMenu_AddRemainingProfiles.Header" xml:space="preserve">
<value>Remaining profiles</value>
<comment>Header for a control that adds any remaining profiles to the new tab menu.</comment>
</data>
<data name="NewTabMenu_AddMatchProfiles.HelpText" xml:space="preserve">
<value>Add a group of profiles that match at least one of the defined properties</value>
<comment>Additional information for a control that adds a terminal profile matcher to the new tab menu. Presented near "NewTabMenu_AddMatchProfiles".</comment>
</data>
<data name="NewTabMenu_AddRemainingProfiles.HelpText" xml:space="preserve">
<value>There can only be one "remaining profiles" entry</value>
<comment>Additional information for a control that adds any remaining profiles to the new tab menu. Presented near "NewTabMenu_AddRemainingProfiles".</comment>
</data>
<data name="NewTabMenu_AddSeparator.Header" xml:space="preserve">
<value>Separator</value>
<comment>Header for a control that adds a separator to the new tab menu.</comment>
</data>
<data name="NewTabMenu_AddFolder.Header" xml:space="preserve">
<value>Folder</value>
<comment>Header for a control that adds a folder to the new tab menu.</comment>
</data>
<data name="NewTabMenu_AddMatchProfiles_Name.Header" xml:space="preserve">
<value>Profile name</value>
<comment>Header for a text box used to define a regex for the names of profiles to add.</comment>
</data>
<data name="NewTabMenu_AddMatchProfiles_Source.Header" xml:space="preserve">
<value>Profile source</value>
<comment>Header for a text box used to define a regex for the sources of profiles to add.</comment>
</data>
<data name="NewTabMenu_AddMatchProfiles_Commandline.Header" xml:space="preserve">
<value>Commandline</value>
<comment>Header for a text box used to define a regex for the commandlines of profiles to add.</comment>
</data>
<data name="NewTabMenu_AddMatchProfilesTextBlock.Text" xml:space="preserve">
<value>Add profile matcher</value>
<comment>Label for a button confirming to add the profile matcher to the new tab menu as an entry.</comment>
</data>
<data name="NewTabMenu_AddFolder_FolderName.PlaceholderText" xml:space="preserve">
<value>Folder name</value>
<comment>Placeholder text for a text box control used to set the name of the folder.</comment>
</data>
<data name="NewTabMenu_DeleteMultipleTextBlock.Text" xml:space="preserve">
<value>Delete selected entries</value>
<comment>Label for a button that can be used to delete any new tab menu entries that are currently selected</comment>
</data>
<data name="NewTabMenu_MoveToFolderTextBlock.Text" xml:space="preserve">
<value>Move selected entries to folder...</value>
<comment>Label for a button that can be used to move any new tab menu entries that are currently selected into an existing folder</comment>
</data>
<data name="NewTabMenu_FolderPickerDialog.Title" xml:space="preserve">
<value>Move to folder</value>
<comment>Title displayed on a content dialog directing the user to pick a folder to move the selected entries to.</comment>
</data>
<data name="NewTabMenu_FolderPickerDialog.PrimaryButtonText" xml:space="preserve">
<value>OK</value>
<comment>Button label for the folder picker content dialog. Used as confirmation to pick the selected folder.</comment>
</data>
<data name="NewTabMenu_FolderPickerDialog.SecondaryButtonText" xml:space="preserve">
<value>Cancel</value>
<comment>Text label for the secondary button on the folder picker content dialog. When clicked, the operation of picking a folder is cancelled by the user.</comment>
</data>
<data name="NewTabMenu_RootFolderName" xml:space="preserve">
<value>&lt;root&gt;</value>
<comment>{Locked="&lt;"}{Locked="&gt;"} Text label for the name of the "root" folder. This is used to allow the user to select the root as a destination folder.</comment>
</data>
<data name="NewTabMenu_CurrentFolderTextBlock.Text" xml:space="preserve">
<value>Current Folder Properties</value>
<comment>Header for a group of controls that can be used to modify the current folder entry's properties.</comment>
</data>
<data name="NewTabMenu_AddEntriesTextBlock.Text" xml:space="preserve">
<value>Add Entry</value>
<comment>Header for a group of controls that can be used to add an entry to the new tab menu</comment>
</data>
<data name="NewTabMenu_CurrentFolderName.Header" xml:space="preserve">
<value>Folder Name</value>
<comment>Header for a control that allows the user to modify the name of the current folder entry.</comment>
</data>
<data name="NewTabMenu_CurrentFolderInlining.Header" xml:space="preserve">
<value>Allow inlining</value>
<comment>Header for a control that allows the nested entries to be presented inline rather than with a folder.</comment>
</data>
<data name="NewTabMenu_CurrentFolderInlining.HelpText" xml:space="preserve">
<value>When enabled, if the folder only has a single entry, the entry will show directly and no folder will be rendered.</value>
<comment>Additional text displayed near "NewTabMenu_CurrentFolderInlining.Header".</comment>
</data>
<data name="NewTabMenu_CurrentFolderAllowEmpty.Header" xml:space="preserve">
<value>Allow empty</value>
<comment>Header for a control that allows the current folder entry to be empty.</comment>
</data>
<data name="NewTabMenu_CurrentFolderAllowEmpty.HelpText" xml:space="preserve">
<value>When enabled, if the folder has no entries, it will still be displayed. Otherwise, the folder will not be rendered.</value>
<comment>Additional text displayed near "NewTabMenu_CurrentFolderAllowEmpty.Header".</comment>
</data>
<data name="NewTabMenu_ActionNotFound" xml:space="preserve">
<value>Action ID not found</value>
<comment>Displayed text for an entry who's action identifier wasn't found. The action ID is presented in the format "Action ID not found: &lt;actionID&gt;"</comment>
</data>
<data name="NewTabMenuEntry_ReorderUp.[using:Windows.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Move up</value>
<comment>Accessible name for a button that reorders the entry to be moved up when clicked. Should match "NewTabMenuEntry_ReorderUp.[using:Windows.UI.Xaml.Controls]ToolTipService.ToolTip".</comment>
</data>
<data name="NewTabMenuEntry_ReorderUp.[using:Windows.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>Move up</value>
<comment>Accessible name for a button that reorders the entry to be moved up when clicked. Should match "NewTabMenuEntry_ReorderUp.[using:Windows.UI.Xaml.Automation]AutomationProperties.Name".</comment>
</data>
<data name="NewTabMenuEntry_ReorderDown.[using:Windows.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Move down</value>
<comment>Accessible name for a button that reorders the entry to be moved down when clicked. Should match "NewTabMenuEntry_ReorderDown.[using:Windows.UI.Xaml.Controls]ToolTipService.ToolTip".</comment>
</data>
<data name="NewTabMenuEntry_EditFolder.[using:Windows.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Edit folder</value>
<comment>Accessible name for a button that begins editing the folder when clicked. Should match "NewTabMenuEntry_EditFolder.[using:Windows.UI.Xaml.Controls]ToolTipService.ToolTip".</comment>
</data>
<data name="NewTabMenuEntry_ReorderDown.[using:Windows.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>Move down</value>
<comment>Accessible name for a button that reorders the entry to be moved down when clicked. Should match "NewTabMenuEntry_ReorderDown.[using:Windows.UI.Xaml.Automation]AutomationProperties.Name".</comment>
</data>
<data name="NewTabMenuEntry_EditFolder.[using:Windows.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>Edit folder</value>
<comment>Accessible name for a button that begins editing the folder when clicked. Should match "NewTabMenuEntry_EditFolder.[using:Windows.UI.Xaml.Automation]AutomationProperties.Name".</comment>
</data>
<data name="NewTabMenuEntry_Delete.[using:Windows.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Delete</value>
<comment>Accessible name for a button that deletes the entry when clicked. Should match "NewTabMenuEntry_Delete.[using:Windows.UI.Xaml.Controls]ToolTipService.ToolTip"</comment>
</data>
<data name="NewTabMenuEntry_Delete.[using:Windows.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>Delete</value>
<comment>Accessible name for a button that deletes the entry when clicked. Should match "NewTabMenuEntry_Delete.[using:Windows.UI.Xaml.Automation]AutomationProperties.Name"</comment>
</data>
<data name="NewTabMenu_AddProfileButton.[using:Windows.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>Add selected profile</value>
<comment>Tooltip for a button that adds the selected profile to the new tab menu.</comment>
</data>
<data name="NewTabMenu_AddSeparatorButton.[using:Windows.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>Add separator</value>
<comment>Tooltip for a button that adds a separator to the new tab menu.</comment>
</data>
<data name="NewTabMenu_AddFolderButton.[using:Windows.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>Add folder</value>
<comment>Tooltip for a button that adds a folder to the new tab menu.</comment>
</data>
<data name="NewTabMenu_AddRemainingProfilesButton.[using:Windows.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>Add remaining profiles</value>
<comment>Tooltip for a button that adds an entry that represents the remaining profiles to the new tab menu.</comment>
</data>
<data name="NewTabMenuEntry_SeparatorItem.[using:Windows.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>&lt;Separator&gt;</value>
<comment>{Locked="&lt;"}{Locked="&gt;"}Accessible name for an entry that represents a visual separator in a list. Should match "NewTabMenuEntry_Separator.Text".</comment>
</data>
<data name="Profile_AnswerbackMessage.Header" xml:space="preserve">
<value>ENQ (Request Terminal Status) response</value>
<comment>{Locked=ENQ}{Locked="Request Terminal Status"} Header for a control to determine the response to the ENQ escape sequence. This is represented using a text box.</comment>

View File

@ -12,6 +12,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
DependencyProperty SettingContainer::_HeaderProperty{ nullptr };
DependencyProperty SettingContainer::_HelpTextProperty{ nullptr };
DependencyProperty SettingContainer::_FontIconGlyphProperty{ nullptr };
DependencyProperty SettingContainer::_CurrentValueProperty{ nullptr };
DependencyProperty SettingContainer::_HasSettingValueProperty{ nullptr };
DependencyProperty SettingContainer::_SettingOverrideSourceProperty{ nullptr };
@ -45,6 +46,15 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
xaml_typename<Editor::SettingContainer>(),
PropertyMetadata{ box_value(L"") });
}
if (!_FontIconGlyphProperty)
{
_FontIconGlyphProperty =
DependencyProperty::Register(
L"FontIconGlyph",
xaml_typename<hstring>(),
xaml_typename<Editor::SettingContainer>(),
PropertyMetadata{ box_value(L"") });
}
if (!_CurrentValueProperty)
{
_CurrentValueProperty =

View File

@ -35,6 +35,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
DEPENDENCY_PROPERTY(Windows::Foundation::IInspectable, Header);
DEPENDENCY_PROPERTY(hstring, HelpText);
DEPENDENCY_PROPERTY(hstring, FontIconGlyph);
DEPENDENCY_PROPERTY(hstring, CurrentValue);
DEPENDENCY_PROPERTY(bool, HasSettingValue);
DEPENDENCY_PROPERTY(bool, StartExpanded);

View File

@ -15,6 +15,9 @@ namespace Microsoft.Terminal.Settings.Editor
String HelpText;
static Windows.UI.Xaml.DependencyProperty HelpTextProperty { get; };
String FontIconGlyph;
static Windows.UI.Xaml.DependencyProperty FontIconGlyphProperty { get; };
String CurrentValue;
static Windows.UI.Xaml.DependencyProperty CurrentValueProperty { get; };

View File

@ -189,10 +189,15 @@
<Grid AutomationProperties.Name="{TemplateBinding Header}"
Style="{StaticResource NonExpanderGrid}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Style="{StaticResource StackPanelInExpanderStyle}">
<FontIcon Grid.Column="0"
Margin="0,0,10,0"
Glyph="{TemplateBinding FontIconGlyph}" />
<StackPanel Grid.Column="1"
Style="{StaticResource StackPanelInExpanderStyle}">
<StackPanel Orientation="Horizontal">
<TextBlock Style="{StaticResource SettingsPageItemHeaderStyle}"
Text="{TemplateBinding Header}" />
@ -206,7 +211,7 @@
Style="{StaticResource SettingsPageItemDescriptionStyle}"
Text="{TemplateBinding HelpText}" />
</StackPanel>
<ContentPresenter Grid.Column="1"
<ContentPresenter Grid.Column="2"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Content="{TemplateBinding Content}" />
@ -233,10 +238,15 @@
<muxc:Expander.Header>
<Grid MinHeight="64">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Style="{StaticResource StackPanelInExpanderStyle}">
<FontIcon Grid.Column="0"
Margin="0,0,10,0"
Glyph="{TemplateBinding FontIconGlyph}" />
<StackPanel Grid.Column="1"
Style="{StaticResource StackPanelInExpanderStyle}">
<StackPanel Orientation="Horizontal">
<TextBlock Style="{StaticResource SettingsPageItemHeaderStyle}"
Text="{TemplateBinding Header}" />
@ -250,7 +260,7 @@
Style="{StaticResource SettingsPageItemDescriptionStyle}"
Text="{TemplateBinding HelpText}" />
</StackPanel>
<TextBlock Grid.Column="1"
<TextBlock Grid.Column="2"
MaxWidth="250"
Margin="0,0,-16,0"
HorizontalAlignment="Right"

View File

@ -37,22 +37,24 @@ protected:
winrt::event<::winrt::Windows::UI::Xaml::Data::PropertyChangedEventHandler> _propertyChangedHandlers;
};
#define GETSET_OBSERVABLE_PROJECTED_SETTING(target, name) \
public: \
auto name() const \
{ \
return target.name(); \
}; \
template<typename T> \
void name(const T& value) \
{ \
if (target.name() != value) \
{ \
target.name(value); \
_NotifyChanges(L"Has" #name, L## #name); \
} \
}
#define _BASE_OBSERVABLE_PROJECTED_SETTING(target, name) \
public: \
auto name() const \
{ \
return target.name(); \
}; \
template<typename T> \
void name(const T& value) \
{ \
const auto t = target; \
if (t.name() != value) \
{ \
t.name(value); \
_NotifyChanges(L"Has" #name, L## #name); \
} \
} \
GETSET_OBSERVABLE_PROJECTED_SETTING(target, name) \
bool Has##name() const \
{ \
return target.Has##name(); \

View File

@ -8,32 +8,43 @@
#include "ActionEntry.g.cpp"
using namespace Microsoft::Terminal::Settings::Model;
using namespace winrt::Microsoft::Terminal::Settings::Model::implementation;
static constexpr std::string_view ActionIdKey{ "id" };
static constexpr std::string_view IconKey{ "icon" };
ActionEntry::ActionEntry() noexcept :
ActionEntryT<ActionEntry, NewTabMenuEntry>(NewTabMenuEntryType::Action)
namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{
}
Json::Value ActionEntry::ToJson() const
{
auto json = NewTabMenuEntry::ToJson();
JsonUtils::SetValueForKey(json, ActionIdKey, _ActionId);
JsonUtils::SetValueForKey(json, IconKey, _Icon);
return json;
}
winrt::com_ptr<NewTabMenuEntry> ActionEntry::FromJson(const Json::Value& json)
{
auto entry = winrt::make_self<ActionEntry>();
JsonUtils::GetValueForKey(json, ActionIdKey, entry->_ActionId);
JsonUtils::GetValueForKey(json, IconKey, entry->_Icon);
return entry;
ActionEntry::ActionEntry() noexcept :
ActionEntryT<ActionEntry, NewTabMenuEntry>(NewTabMenuEntryType::Action)
{
}
Json::Value ActionEntry::ToJson() const
{
auto json = NewTabMenuEntry::ToJson();
JsonUtils::SetValueForKey(json, ActionIdKey, _ActionId);
JsonUtils::SetValueForKey(json, IconKey, _Icon);
return json;
}
winrt::com_ptr<NewTabMenuEntry> ActionEntry::FromJson(const Json::Value& json)
{
auto entry = winrt::make_self<ActionEntry>();
JsonUtils::GetValueForKey(json, ActionIdKey, entry->_ActionId);
JsonUtils::GetValueForKey(json, IconKey, entry->_Icon);
return entry;
}
Model::NewTabMenuEntry ActionEntry::Copy() const
{
auto entry = winrt::make_self<ActionEntry>();
entry->_ActionId = _ActionId;
entry->_Icon = _Icon;
return *entry;
}
}

View File

@ -24,6 +24,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
public:
ActionEntry() noexcept;
Model::NewTabMenuEntry Copy() const;
Json::Value ToJson() const override;
static com_ptr<NewTabMenuEntry> FromJson(const Json::Value& json);

View File

@ -9,7 +9,6 @@
#include "FolderEntry.g.cpp"
using namespace Microsoft::Terminal::Settings::Model;
using namespace winrt::Microsoft::Terminal::Settings::Model::implementation;
using namespace winrt::Windows::Foundation::Collections;
static constexpr std::string_view NameKey{ "name" };
@ -18,107 +17,128 @@ static constexpr std::string_view EntriesKey{ "entries" };
static constexpr std::string_view InliningKey{ "inline" };
static constexpr std::string_view AllowEmptyKey{ "allowEmpty" };
FolderEntry::FolderEntry() noexcept :
FolderEntry{ winrt::hstring{} }
namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{
}
FolderEntry::FolderEntry(const winrt::hstring& name) noexcept :
FolderEntryT<FolderEntry, NewTabMenuEntry>(NewTabMenuEntryType::Folder),
_Name{ name }
{
}
Json::Value FolderEntry::ToJson() const
{
auto json = NewTabMenuEntry::ToJson();
JsonUtils::SetValueForKey(json, NameKey, _Name);
JsonUtils::SetValueForKey(json, IconKey, _Icon);
JsonUtils::SetValueForKey(json, EntriesKey, _Entries);
JsonUtils::SetValueForKey(json, InliningKey, _Inlining);
JsonUtils::SetValueForKey(json, AllowEmptyKey, _AllowEmpty);
return json;
}
winrt::com_ptr<NewTabMenuEntry> FolderEntry::FromJson(const Json::Value& json)
{
auto entry = winrt::make_self<FolderEntry>();
JsonUtils::GetValueForKey(json, NameKey, entry->_Name);
JsonUtils::GetValueForKey(json, IconKey, entry->_Icon);
JsonUtils::GetValueForKey(json, EntriesKey, entry->_Entries);
JsonUtils::GetValueForKey(json, InliningKey, entry->_Inlining);
JsonUtils::GetValueForKey(json, AllowEmptyKey, entry->_AllowEmpty);
return entry;
}
// A FolderEntry should only expose the entries to actually render to WinRT,
// to keep the logic for collapsing/expanding more centralised.
using NewTabMenuEntryModel = winrt::Microsoft::Terminal::Settings::Model::NewTabMenuEntry;
IVector<NewTabMenuEntryModel> FolderEntry::Entries() const
{
// We filter the full list of entries from JSON to just include the
// non-empty ones.
IVector<NewTabMenuEntryModel> result{ winrt::single_threaded_vector<NewTabMenuEntryModel>() };
if (_Entries == nullptr)
FolderEntry::FolderEntry() noexcept :
FolderEntry{ winrt::hstring{} }
{
}
FolderEntry::FolderEntry(const winrt::hstring& name) noexcept :
FolderEntryT<FolderEntry, NewTabMenuEntry>(NewTabMenuEntryType::Folder),
_Name{ name }
{
}
Json::Value FolderEntry::ToJson() const
{
auto json = NewTabMenuEntry::ToJson();
JsonUtils::SetValueForKey(json, NameKey, _Name);
JsonUtils::SetValueForKey(json, IconKey, _Icon);
JsonUtils::SetValueForKey(json, EntriesKey, _RawEntries);
JsonUtils::SetValueForKey(json, InliningKey, _Inlining);
JsonUtils::SetValueForKey(json, AllowEmptyKey, _AllowEmpty);
return json;
}
winrt::com_ptr<NewTabMenuEntry> FolderEntry::FromJson(const Json::Value& json)
{
auto entry = winrt::make_self<FolderEntry>();
JsonUtils::GetValueForKey(json, NameKey, entry->_Name);
JsonUtils::GetValueForKey(json, IconKey, entry->_Icon);
JsonUtils::GetValueForKey(json, EntriesKey, entry->_RawEntries);
JsonUtils::GetValueForKey(json, InliningKey, entry->_Inlining);
JsonUtils::GetValueForKey(json, AllowEmptyKey, entry->_AllowEmpty);
return entry;
}
// A FolderEntry should only expose the entries to actually render to WinRT,
// to keep the logic for collapsing/expanding more centralised.
IVector<Model::NewTabMenuEntry> FolderEntry::Entries() const
{
// We filter the full list of entries from JSON to just include the
// non-empty ones.
IVector<Model::NewTabMenuEntry> result{ winrt::single_threaded_vector<Model::NewTabMenuEntry>() };
if (_RawEntries == nullptr)
{
return result;
}
for (const auto& entry : _RawEntries)
{
if (entry == nullptr)
{
continue;
}
switch (entry.Type())
{
case NewTabMenuEntryType::Invalid:
continue;
// A profile is filtered out if it is not valid, so if it was not resolved
case NewTabMenuEntryType::Profile:
{
const auto profileEntry = entry.as<Model::ProfileEntry>();
if (profileEntry.Profile() == nullptr)
{
continue;
}
break;
}
// Any profile collection is filtered out if there are no results
case NewTabMenuEntryType::RemainingProfiles:
case NewTabMenuEntryType::MatchProfiles:
{
const auto profileCollectionEntry = entry.as<Model::ProfileCollectionEntry>();
if (profileCollectionEntry.Profiles().Size() == 0)
{
continue;
}
break;
}
// A folder is filtered out if it has an effective size of 0 (calling
// this filtering method recursively), and if it is not allowed to be
// empty, or if it should auto-inline.
case NewTabMenuEntryType::Folder:
{
const auto folderEntry = entry.as<Model::FolderEntry>();
if (folderEntry.Entries().Size() == 0 && (!folderEntry.AllowEmpty() || folderEntry.Inlining() == FolderEntryInlining::Auto))
{
continue;
}
break;
}
}
result.Append(entry);
}
return result;
}
for (const auto& entry : _Entries)
Model::NewTabMenuEntry FolderEntry::Copy() const
{
if (entry == nullptr)
{
continue;
}
auto entry = winrt::make_self<FolderEntry>();
entry->_Name = _Name;
entry->_Icon = _Icon;
entry->_Inlining = _Inlining;
entry->_AllowEmpty = _AllowEmpty;
switch (entry.Type())
if (_RawEntries)
{
case NewTabMenuEntryType::Invalid:
continue;
// A profile is filtered out if it is not valid, so if it was not resolved
case NewTabMenuEntryType::Profile:
{
const auto profileEntry = entry.as<ProfileEntry>();
if (profileEntry.Profile() == nullptr)
entry->_RawEntries = winrt::single_threaded_vector<Model::NewTabMenuEntry>();
for (const auto& e : _RawEntries)
{
continue;
entry->_RawEntries.Append(get_self<NewTabMenuEntry>(e)->Copy());
}
break;
}
// Any profile collection is filtered out if there are no results
case NewTabMenuEntryType::RemainingProfiles:
case NewTabMenuEntryType::MatchProfiles:
{
const auto profileCollectionEntry = entry.as<ProfileCollectionEntry>();
if (profileCollectionEntry.Profiles().Size() == 0)
{
continue;
}
break;
}
// A folder is filtered out if it has an effective size of 0 (calling
// this filtering method recursively), and if it is not allowed to be
// empty, or if it should auto-inline.
case NewTabMenuEntryType::Folder:
{
const auto folderEntry = entry.as<Model::FolderEntry>();
if (folderEntry.Entries().Size() == 0 && (!folderEntry.AllowEmpty() || folderEntry.Inlining() == FolderEntryInlining::Auto))
{
continue;
}
break;
}
}
result.Append(entry);
return *entry;
}
return result;
}

View File

@ -26,6 +26,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
FolderEntry() noexcept;
explicit FolderEntry(const winrt::hstring& name) noexcept;
Model::NewTabMenuEntry Copy() const override;
Json::Value ToJson() const override;
static com_ptr<NewTabMenuEntry> FromJson(const Json::Value& json);
@ -34,18 +36,12 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
// Therefore, we will store the JSON entries list internally, and then expose only the
// entries to be rendered to WinRT.
winrt::Windows::Foundation::Collections::IVector<Model::NewTabMenuEntry> Entries() const;
winrt::Windows::Foundation::Collections::IVector<Model::NewTabMenuEntry> RawEntries() const
{
return _Entries;
};
WINRT_PROPERTY(winrt::hstring, Name);
WINRT_PROPERTY(winrt::hstring, Icon);
WINRT_PROPERTY(FolderEntryInlining, Inlining, FolderEntryInlining::Never);
WINRT_PROPERTY(bool, AllowEmpty, false);
private:
winrt::Windows::Foundation::Collections::IVector<Model::NewTabMenuEntry> _Entries{};
WINRT_PROPERTY(winrt::Windows::Foundation::Collections::IVector<Model::NewTabMenuEntry>, RawEntries);
};
}

View File

@ -84,6 +84,14 @@ winrt::com_ptr<GlobalAppSettings> GlobalAppSettings::Copy() const
globals->_themes.Insert(kv.Key(), *themeImpl->Copy());
}
}
if (_NewTabMenu)
{
globals->_NewTabMenu = winrt::single_threaded_vector<Model::NewTabMenuEntry>();
for (const auto& entry : *_NewTabMenu)
{
globals->_NewTabMenu->Append(get_self<NewTabMenuEntry>(entry)->Copy());
}
}
for (const auto& parent : _parents)
{

View File

@ -8,66 +8,77 @@
#include "MatchProfilesEntry.g.cpp"
using namespace Microsoft::Terminal::Settings::Model;
using namespace winrt::Microsoft::Terminal::Settings::Model::implementation;
static constexpr std::string_view NameKey{ "name" };
static constexpr std::string_view CommandlineKey{ "commandline" };
static constexpr std::string_view SourceKey{ "source" };
MatchProfilesEntry::MatchProfilesEntry() noexcept :
MatchProfilesEntryT<MatchProfilesEntry, ProfileCollectionEntry>(NewTabMenuEntryType::MatchProfiles)
namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{
}
Json::Value MatchProfilesEntry::ToJson() const
{
auto json = NewTabMenuEntry::ToJson();
JsonUtils::SetValueForKey(json, NameKey, _Name);
JsonUtils::SetValueForKey(json, CommandlineKey, _Commandline);
JsonUtils::SetValueForKey(json, SourceKey, _Source);
return json;
}
winrt::com_ptr<NewTabMenuEntry> MatchProfilesEntry::FromJson(const Json::Value& json)
{
auto entry = winrt::make_self<MatchProfilesEntry>();
JsonUtils::GetValueForKey(json, NameKey, entry->_Name);
JsonUtils::GetValueForKey(json, CommandlineKey, entry->_Commandline);
JsonUtils::GetValueForKey(json, SourceKey, entry->_Source);
return entry;
}
bool MatchProfilesEntry::MatchesProfile(const Model::Profile& profile)
{
// We use an optional here instead of a simple bool directly, since there is no
// sensible default value for the desired semantics: the first property we want
// to match on should always be applied (so one would set "true" as a default),
// but if none of the properties are set, the default return value should be false
// since this entry type is expected to behave like a positive match/whitelist.
//
// The easiest way to deal with this neatly is to use an optional, then for any
// property to match we consider a null value to be "true", and for the return
// value of the function we consider the null value to be "false".
auto isMatching = std::optional<bool>{};
if (!_Name.empty())
MatchProfilesEntry::MatchProfilesEntry() noexcept :
MatchProfilesEntryT<MatchProfilesEntry, ProfileCollectionEntry>(NewTabMenuEntryType::MatchProfiles)
{
isMatching = { isMatching.value_or(true) && _Name == profile.Name() };
}
if (!_Source.empty())
Json::Value MatchProfilesEntry::ToJson() const
{
isMatching = { isMatching.value_or(true) && _Source == profile.Source() };
auto json = NewTabMenuEntry::ToJson();
JsonUtils::SetValueForKey(json, NameKey, _Name);
JsonUtils::SetValueForKey(json, CommandlineKey, _Commandline);
JsonUtils::SetValueForKey(json, SourceKey, _Source);
return json;
}
if (!_Commandline.empty())
winrt::com_ptr<NewTabMenuEntry> MatchProfilesEntry::FromJson(const Json::Value& json)
{
isMatching = { isMatching.value_or(true) && _Commandline == profile.Commandline() };
auto entry = winrt::make_self<MatchProfilesEntry>();
JsonUtils::GetValueForKey(json, NameKey, entry->_Name);
JsonUtils::GetValueForKey(json, CommandlineKey, entry->_Commandline);
JsonUtils::GetValueForKey(json, SourceKey, entry->_Source);
return entry;
}
return isMatching.value_or(false);
bool MatchProfilesEntry::MatchesProfile(const Model::Profile& profile)
{
// We use an optional here instead of a simple bool directly, since there is no
// sensible default value for the desired semantics: the first property we want
// to match on should always be applied (so one would set "true" as a default),
// but if none of the properties are set, the default return value should be false
// since this entry type is expected to behave like a positive match/whitelist.
//
// The easiest way to deal with this neatly is to use an optional, then for any
// property to match we consider a null value to be "true", and for the return
// value of the function we consider the null value to be "false".
auto isMatching = std::optional<bool>{};
if (!_Name.empty())
{
isMatching = { isMatching.value_or(true) && _Name == profile.Name() };
}
if (!_Source.empty())
{
isMatching = { isMatching.value_or(true) && _Source == profile.Source() };
}
if (!_Commandline.empty())
{
isMatching = { isMatching.value_or(true) && _Commandline == profile.Commandline() };
}
return isMatching.value_or(false);
}
Model::NewTabMenuEntry MatchProfilesEntry::Copy() const
{
auto entry = winrt::make_self<MatchProfilesEntry>();
entry->_Name = _Name;
entry->_Commandline = _Commandline;
entry->_Source = _Source;
return *entry;
}
}

View File

@ -25,6 +25,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
public:
MatchProfilesEntry() noexcept;
Model::NewTabMenuEntry Copy() const override;
Json::Value ToJson() const override;
static com_ptr<NewTabMenuEntry> FromJson(const Json::Value& json);

View File

@ -15,47 +15,49 @@
#include "NewTabMenuEntry.g.cpp"
using namespace Microsoft::Terminal::Settings::Model;
using namespace winrt::Microsoft::Terminal::Settings::Model::implementation;
using NewTabMenuEntryType = winrt::Microsoft::Terminal::Settings::Model::NewTabMenuEntryType;
static constexpr std::string_view TypeKey{ "type" };
NewTabMenuEntry::NewTabMenuEntry(const NewTabMenuEntryType type) noexcept :
_Type{ type }
namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{
}
// This method will be overridden by the subclasses, which will then call this
// parent implementation for a "base" json object.
Json::Value NewTabMenuEntry::ToJson() const
{
Json::Value json{ Json::ValueType::objectValue };
JsonUtils::SetValueForKey(json, TypeKey, _Type);
return json;
}
// Deserialize the JSON object based on the given type. We use the map from above for that.
winrt::com_ptr<NewTabMenuEntry> NewTabMenuEntry::FromJson(const Json::Value& json)
{
const auto type = JsonUtils::GetValueForKey<NewTabMenuEntryType>(json, TypeKey);
switch (type)
NewTabMenuEntry::NewTabMenuEntry(const NewTabMenuEntryType type) noexcept :
_Type{ type }
{
case NewTabMenuEntryType::Separator:
return SeparatorEntry::FromJson(json);
case NewTabMenuEntryType::Folder:
return FolderEntry::FromJson(json);
case NewTabMenuEntryType::Profile:
return ProfileEntry::FromJson(json);
case NewTabMenuEntryType::RemainingProfiles:
return RemainingProfilesEntry::FromJson(json);
case NewTabMenuEntryType::MatchProfiles:
return MatchProfilesEntry::FromJson(json);
case NewTabMenuEntryType::Action:
return ActionEntry::FromJson(json);
default:
return nullptr;
}
// This method will be overridden by the subclasses, which will then call this
// parent implementation for a "base" json object.
Json::Value NewTabMenuEntry::ToJson() const
{
Json::Value json{ Json::ValueType::objectValue };
JsonUtils::SetValueForKey(json, TypeKey, _Type);
return json;
}
// Deserialize the JSON object based on the given type. We use the map from above for that.
winrt::com_ptr<NewTabMenuEntry> NewTabMenuEntry::FromJson(const Json::Value& json)
{
const auto type = JsonUtils::GetValueForKey<NewTabMenuEntryType>(json, TypeKey);
switch (type)
{
case NewTabMenuEntryType::Separator:
return SeparatorEntry::FromJson(json);
case NewTabMenuEntryType::Folder:
return FolderEntry::FromJson(json);
case NewTabMenuEntryType::Profile:
return ProfileEntry::FromJson(json);
case NewTabMenuEntryType::RemainingProfiles:
return RemainingProfilesEntry::FromJson(json);
case NewTabMenuEntryType::MatchProfiles:
return MatchProfilesEntry::FromJson(json);
case NewTabMenuEntryType::Action:
return ActionEntry::FromJson(json);
default:
return nullptr;
}
}
}

View File

@ -25,6 +25,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
public:
static com_ptr<NewTabMenuEntry> FromJson(const Json::Value& json);
virtual Json::Value ToJson() const;
virtual Model::NewTabMenuEntry Copy() const = 0;
WINRT_PROPERTY(NewTabMenuEntryType, Type, NewTabMenuEntryType::Invalid);

View File

@ -61,6 +61,7 @@ namespace Microsoft.Terminal.Settings.Model
Boolean AllowEmpty;
IVector<NewTabMenuEntry> Entries();
IVector<NewTabMenuEntry> RawEntries;
}
[default_interface] unsealed runtimeclass ProfileCollectionEntry : NewTabMenuEntry

View File

@ -8,56 +8,67 @@
#include "ProfileEntry.g.cpp"
using namespace Microsoft::Terminal::Settings::Model;
using namespace winrt::Microsoft::Terminal::Settings::Model::implementation;
static constexpr std::string_view ProfileKey{ "profile" };
static constexpr std::string_view IconKey{ "icon" };
ProfileEntry::ProfileEntry() noexcept :
ProfileEntry{ winrt::hstring{} }
namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{
}
ProfileEntry::ProfileEntry(const winrt::hstring& profile) noexcept :
ProfileEntryT<ProfileEntry, NewTabMenuEntry>(NewTabMenuEntryType::Profile),
_ProfileName{ profile }
{
}
Json::Value ProfileEntry::ToJson() const
{
auto json = NewTabMenuEntry::ToJson();
// We will now return a profile reference to the JSON representation. Logic is
// as follows:
// - When Profile is null, this is typically because an existing profile menu entry
// in the JSON config is invalid (nonexistent or hidden profile). Then, we store
// the original profile string value as read from JSON, to improve portability
// of the settings file and limit modifications to the JSON.
// - Otherwise, we always store the GUID of the profile. This will effectively convert
// all name-matched profiles from the settings file to GUIDs. This might be unexpected
// to some users, but is less error-prone and will continue to work when profile
// names are changed.
if (_Profile == nullptr)
ProfileEntry::ProfileEntry() noexcept :
ProfileEntry{ winrt::hstring{} }
{
JsonUtils::SetValueForKey(json, ProfileKey, _ProfileName);
}
else
{
JsonUtils::SetValueForKey(json, ProfileKey, _Profile.Guid());
}
JsonUtils::SetValueForKey(json, IconKey, _Icon);
ProfileEntry::ProfileEntry(const winrt::hstring& profile) noexcept :
ProfileEntryT<ProfileEntry, NewTabMenuEntry>(NewTabMenuEntryType::Profile),
_ProfileName{ profile }
{
}
return json;
}
winrt::com_ptr<NewTabMenuEntry> ProfileEntry::FromJson(const Json::Value& json)
{
auto entry = winrt::make_self<ProfileEntry>();
JsonUtils::GetValueForKey(json, ProfileKey, entry->_ProfileName);
JsonUtils::GetValueForKey(json, IconKey, entry->_Icon);
return entry;
Json::Value ProfileEntry::ToJson() const
{
auto json = NewTabMenuEntry::ToJson();
// We will now return a profile reference to the JSON representation. Logic is
// as follows:
// - When Profile is null, this is typically because an existing profile menu entry
// in the JSON config is invalid (nonexistent or hidden profile). Then, we store
// the original profile string value as read from JSON, to improve portability
// of the settings file and limit modifications to the JSON.
// - Otherwise, we always store the GUID of the profile. This will effectively convert
// all name-matched profiles from the settings file to GUIDs. This might be unexpected
// to some users, but is less error-prone and will continue to work when profile
// names are changed.
if (_Profile == nullptr)
{
JsonUtils::SetValueForKey(json, ProfileKey, _ProfileName);
}
else
{
JsonUtils::SetValueForKey(json, ProfileKey, _Profile.Guid());
}
JsonUtils::SetValueForKey(json, IconKey, _Icon);
return json;
}
winrt::com_ptr<NewTabMenuEntry> ProfileEntry::FromJson(const Json::Value& json)
{
auto entry = winrt::make_self<ProfileEntry>();
JsonUtils::GetValueForKey(json, ProfileKey, entry->_ProfileName);
JsonUtils::GetValueForKey(json, IconKey, entry->_Icon);
return entry;
}
Model::NewTabMenuEntry ProfileEntry::Copy() const
{
auto entry{ winrt::make_self<ProfileEntry>() };
entry->_Profile = _Profile;
entry->_ProfileIndex = _ProfileIndex;
entry->_ProfileName = _ProfileName;
entry->_Icon = _Icon;
return *entry;
}
}

View File

@ -28,6 +28,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
ProfileEntry() noexcept;
explicit ProfileEntry(const winrt::hstring& profile) noexcept;
Model::NewTabMenuEntry Copy() const override;
Json::Value ToJson() const override;
static com_ptr<NewTabMenuEntry> FromJson(const Json::Value& json);

View File

@ -9,14 +9,21 @@
#include "RemainingProfilesEntry.g.cpp"
using namespace Microsoft::Terminal::Settings::Model;
using namespace winrt::Microsoft::Terminal::Settings::Model::implementation;
RemainingProfilesEntry::RemainingProfilesEntry() noexcept :
RemainingProfilesEntryT<RemainingProfilesEntry, ProfileCollectionEntry>(NewTabMenuEntryType::RemainingProfiles)
namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{
}
RemainingProfilesEntry::RemainingProfilesEntry() noexcept :
RemainingProfilesEntryT<RemainingProfilesEntry, ProfileCollectionEntry>(NewTabMenuEntryType::RemainingProfiles)
{
}
winrt::com_ptr<NewTabMenuEntry> RemainingProfilesEntry::FromJson(const Json::Value&)
{
return winrt::make_self<RemainingProfilesEntry>();
winrt::com_ptr<NewTabMenuEntry> RemainingProfilesEntry::FromJson(const Json::Value&)
{
return winrt::make_self<RemainingProfilesEntry>();
}
Model::NewTabMenuEntry RemainingProfilesEntry::Copy() const
{
return winrt::make<RemainingProfilesEntry>();
}
}

View File

@ -25,6 +25,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
public:
RemainingProfilesEntry() noexcept;
Model::NewTabMenuEntry Copy() const override;
static com_ptr<NewTabMenuEntry> FromJson(const Json::Value& json);
};
}

View File

@ -8,14 +8,21 @@
#include "SeparatorEntry.g.cpp"
using namespace Microsoft::Terminal::Settings::Model;
using namespace winrt::Microsoft::Terminal::Settings::Model::implementation;
SeparatorEntry::SeparatorEntry() noexcept :
SeparatorEntryT<SeparatorEntry, NewTabMenuEntry>(NewTabMenuEntryType::Separator)
namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{
}
SeparatorEntry::SeparatorEntry() noexcept :
SeparatorEntryT<SeparatorEntry, NewTabMenuEntry>(NewTabMenuEntryType::Separator)
{
}
winrt::com_ptr<NewTabMenuEntry> SeparatorEntry::FromJson(const Json::Value&)
{
return winrt::make_self<SeparatorEntry>();
winrt::com_ptr<NewTabMenuEntry> SeparatorEntry::FromJson(const Json::Value&)
{
return winrt::make_self<SeparatorEntry>();
}
Model::NewTabMenuEntry SeparatorEntry::Copy() const
{
return winrt::make<SeparatorEntry>();
}
}

View File

@ -24,6 +24,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
public:
SeparatorEntry() noexcept;
Model::NewTabMenuEntry Copy() const override;
static com_ptr<NewTabMenuEntry> FromJson(const Json::Value& json);
};
}