Add New Tab Menu Customization to Settings UI

This commit is contained in:
Carlos Zamora 2024-03-14 15:19:29 -07:00
parent 544452dad4
commit 0e304d8197
31 changed files with 1203 additions and 21 deletions

View File

@ -16,6 +16,7 @@
#include "AddProfile.h"
#include "InteractionViewModel.h"
#include "LaunchViewModel.h"
#include "NewTabMenuViewModel.h"
#include "..\types\inc\utils.hpp"
#include <..\WinRTUtils\inc\Utils.h>
@ -40,6 +41,7 @@ static const std::wstring_view launchTag{ L"Launch_Nav" };
static const std::wstring_view interactionTag{ L"Interaction_Nav" };
static const std::wstring_view renderingTag{ L"Rendering_Nav" };
static const std::wstring_view 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" };
@ -59,6 +61,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
InitializeComponent();
_UpdateBackgroundForMica();
_newTabMenuPageVM = winrt::make<NewTabMenuViewModel>(_settingsClone);
_colorSchemesPageVM = winrt::make<ColorSchemesPageViewModel>(_settingsClone);
_colorSchemesPageViewModelChangedRevoker = _colorSchemesPageVM.PropertyChanged(winrt::auto_revoke, [=](auto&&, const PropertyChangedEventArgs& args) {
const auto settingName{ args.PropertyName() };
@ -134,6 +137,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
_InitializeProfilesList();
// Update the Nav State with the new version of the settings
_colorSchemesPageVM.UpdateSettings(_settingsClone);
get_self<NewTabMenuViewModel>(_newTabMenuPageVM)->UpdateSettings(_settingsClone);
// We'll update the profile in the _profilesNavState whenever we actually navigate to one
@ -378,6 +382,12 @@ 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)
{
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) };

View File

@ -74,6 +74,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
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;

View File

@ -142,6 +142,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

@ -70,6 +70,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>
@ -106,6 +109,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>
@ -160,6 +167,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>
@ -210,6 +220,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>
@ -249,6 +262,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>
@ -307,6 +324,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>
@ -326,6 +347,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,78 @@
// 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>();
// TODO CARLOS: set auto props, if necessary
//Automation::AutomationProperties::SetName(NewTabMenuModeComboBox(), RS_(L"Globals_NewTabMenuModeSetting/Text"));
//Automation::AutomationProperties::SetHelpText(NewTabMenuModeComboBox(), RS_(L"Globals_NewTabMenuModeSetting/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip"));
//Automation::AutomationProperties::SetHelpText(PosXBox(), RS_(L"Globals_InitialPosXBox/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip"));
//Automation::AutomationProperties::SetHelpText(PosYBox(), RS_(L"Globals_InitialPosYBox/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip"));
//Automation::AutomationProperties::SetHelpText(UseDefaultNewTabMenuPositionCheckbox(), RS_(L"Globals_DefaultNewTabMenuPositionCheckbox/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip"));
//Automation::AutomationProperties::SetName(CenterOnNewTabMenuToggle(), RS_(L"Globals_CenterOnNewTabMenu/Text"));
//Automation::AutomationProperties::SetHelpText(CenterOnNewTabMenuToggle(), RS_(L"Globals_CenterOnNewTabMenu/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip"));
}
void NewTabMenu::OnNavigatedTo(const NavigationEventArgs& e)
{
_ViewModel = e.Parameter().as<Editor::NewTabMenuViewModel>();
}
void NewTabMenu::FolderNameTextBox_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::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,47 @@
// 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);
void FolderNameTextBox_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 };
};
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, 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,24 @@
// 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 SeparatorEntryTemplate;
Windows.UI.Xaml.DataTemplate FolderEntryTemplate;
Windows.UI.Xaml.DataTemplate MatchProfilesEntryTemplate;
Windows.UI.Xaml.DataTemplate RemainingProfilesEntryTemplate;
}
}

View File

@ -0,0 +1,258 @@
<!--
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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="using:Microsoft.Terminal.Settings.Editor"
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>
<!--<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<StaticResource x:Key="ControlExampleDisplayBrush" ResourceKey="SolidBackgroundFillColorBaseBrush" />
<Thickness x:Key="ControlExampleDisplayBorderThickness">0</Thickness>
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<StaticResource x:Key="ControlExampleDisplayBrush" ResourceKey="SolidBackgroundFillColorBaseBrush" />
<Thickness x:Key="ControlExampleDisplayBorderThickness">0</Thickness>
</ResourceDictionary>
<ResourceDictionary x:Key="HighContrast">
<SolidColorBrush x:Key="ControlExampleDisplayBrush" Color="{ThemeResource SystemColorWindowColor}" />
<Thickness x:Key="ControlExampleDisplayBorderThickness">1</Thickness>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>-->
<DataTemplate x:Key="ProfileEntryTemplate"
x:DataType="local:ProfileEntryViewModel">
<muxc:TreeViewItem AutomationProperties.Name="{x:Bind ProfileEntry.Profile.Name, Mode=OneWay}">
<muxc:TreeViewItem.Content>
<StackPanel Orientation="Horizontal"
Spacing="10">
<Image Source="{x:Bind ProfileEntry.Profile.Icon, Mode=OneWay}"
Width="16"
Height="16" />
<TextBlock Text="{x:Bind ProfileEntry.Profile.Name, Mode=OneWay}"/>
</StackPanel>
</muxc:TreeViewItem.Content>
</muxc:TreeViewItem>
</DataTemplate>
<DataTemplate x:Key="SeparatorEntryTemplate"
x:DataType="local:SeparatorEntryViewModel">
<muxc:TreeViewItem x:Uid="NewTabMenuEntry_Separator"
FontStyle="Italic"/>
</DataTemplate>
<!-- TODO CARLOS: I'm using TreeViewItem.ItemsSource to recursively define
folder entries. This is the method shown in the WinUI 2 Gallery.
It's not working. Investigate.
Investigation Notes:
If you comment out the images, this works. Otherwise, sometimes you
get a crash or the nested folders don't load.-->
<DataTemplate x:Key="FolderEntryTemplate"
x:DataType="local:FolderEntryViewModel">
<muxc:TreeViewItem AutomationProperties.Name="{x:Bind Name, Mode=OneWay}"
ItemsSource="{x:Bind Entries, Mode=TwoWay}"
IsExpanded="True">
<muxc:TreeViewItem.Content>
<StackPanel Orientation="Horizontal" Spacing="10">
<!--<Image Source="{x:Bind Icon}"
Width="16"
Height="16" />-->
<TextBlock Text="{x:Bind Name, Mode=OneWay}"/>
</StackPanel>
</muxc:TreeViewItem.Content>
</muxc:TreeViewItem>
</DataTemplate>
<DataTemplate x:Key="MatchProfilesEntryTemplate"
x:DataType="local:MatchProfilesEntryViewModel">
<muxc:TreeViewItem FontStyle="Italic"
Content="{x:Bind DisplayText, Mode=OneWay}"/>
</DataTemplate>
<DataTemplate x:Key="RemainingProfilesEntryTemplate"
x:DataType="local:RemainingProfilesEntryViewModel">
<muxc:TreeViewItem x:Uid="NewTabMenuEntry_RemainingProfiles"
FontStyle="Italic"/>
</DataTemplate>
<local:NewTabMenuEntryTemplateSelector x:Key="NewTabMenuEntryTemplateSelector"
ProfileEntryTemplate="{StaticResource ProfileEntryTemplate}"
SeparatorEntryTemplate="{StaticResource SeparatorEntryTemplate}"
FolderEntryTemplate="{StaticResource FolderEntryTemplate}"
MatchProfilesEntryTemplate="{StaticResource MatchProfilesEntryTemplate}"
RemainingProfilesEntryTemplate="{StaticResource RemainingProfilesEntryTemplate}"/>
</ResourceDictionary>
</Page.Resources>
<StackPanel>
<!-- Margin stolen from SettingsStackStyle -->
<Grid Margin="13,0,13,48">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!--New Tab Menu Content-->
<Border Grid.Row="0"
Margin="0,12,0,12"
BorderBrush="{ThemeResource SystemControlForegroundBaseMediumLowBrush}"
BorderThickness="1"
MaxWidth="{StaticResource StandardControlMaxWidth}"
CornerRadius="{ThemeResource ControlCornerRadius}"
Height="200">
<muxc:TreeView x:Name="NewTabMenuTreeView"
AllowDrop="False"
CanDrag="False"
CanDragItems="True"
CanReorderItems="True"
SelectionMode="None"
ItemsSource="{x:Bind ViewModel.Entries, Mode=OneWay}"
ItemTemplateSelector="{StaticResource NewTabMenuEntryTemplateSelector}"/>
</Border>
<StackPanel Grid.Row="1"
HorizontalAlignment="Stretch">
<!-- Add Profile -->
<local:SettingContainer x:Uid="NewTabMenu_AddProfile"
FontIconGlyph="&#xE756;">
<StackPanel Orientation="Horizontal"
Spacing="5">
<!--Select profile to add-->
<!--TODO CARLOS: sort the list alphabetically?-->
<ComboBox x:Name="AddProfileComboBox"
ItemsSource="{x:Bind ViewModel.AvailableProfiles, Mode=OneWay}"
SelectedItem="{x:Bind ViewModel.SelectedProfile, Mode=TwoWay}"
MinWidth="{StaticResource StandardBoxMinWidth}">
<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(Icon), Mode=OneTime}" />
<TextBlock Grid.Column="1"
Text="{x:Bind Name}" />
</Grid>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<Button x:Name="AddProfileButton"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
Click="{x:Bind ViewModel.RequestAddSelectedProfileEntry}">
<Button.Content>
<FontIcon FontSize="{StaticResource StandardIconSize}"
Glyph="&#xE710;" />
</Button.Content>
</Button>
</StackPanel>
</local:SettingContainer>
<!-- Add Separator -->
<!--TODO CARLOS: the button height is different from the one before.-->
<local:SettingContainer x:Uid="NewTabMenu_AddSeparator"
FontIconGlyph="&#xE76f;">
<Button x:Name="AddSeparatorButton"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
Click="{x:Bind ViewModel.RequestAddSeparatorEntry}">
<Button.Content>
<FontIcon FontSize="{StaticResource StandardIconSize}"
Glyph="&#xE710;" />
</Button.Content>
</Button>
</local:SettingContainer>
<!-- Add Folder -->
<!--TODO CARLOS: adding an icon would be cool, but I don't think we're equipped to do that yet-->
<local:SettingContainer x:Uid="NewTabMenu_AddFolder"
FontIconGlyph="&#xF12B;">
<StackPanel Orientation="Horizontal"
Spacing="5">
<TextBox x:Uid="NewTabMenu_AddFolder_FolderName"
x:Name="FolderNameTextBox"
MinWidth="{StaticResource StandardBoxMinWidth}"
Text="{x:Bind ViewModel.FolderName, Mode=TwoWay}"
TextChanged="FolderNameTextBox_TextChanged"/>
<Button x:Name="AddFolderButton"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
Click="{x:Bind ViewModel.RequestAddFolderEntry}"
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="{x:Bind ViewModel.RequestAddProfileMatcherEntry}">
<Button.Content>
<StackPanel Orientation="Horizontal">
<FontIcon FontSize="{StaticResource StandardIconSize}"
Glyph="&#xE710;" />
<TextBlock x:Uid="NewTabMenu_AddMatchProfiles_Confirmation"
Style="{StaticResource IconButtonTextBlockStyle}" />
</StackPanel>
</Button.Content>
</Button>
</StackPanel>
</local:SettingContainer>
<!-- Add Remaining Profiles -->
<!--TODO CARLOS: it would be cool if when disabled, we should -->
<local:SettingContainer x:Uid="NewTabMenu_AddRemainingProfiles"
FontIconGlyph="&#xE902;">
<Button VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
Click="{x:Bind ViewModel.RequestAddRemainingProfilesEntry}"
IsEnabled="{x:Bind ViewModel.IsRemainingProfilesEntryMissing(ViewModel.Entries), Mode=OneWay}">
<Button.Content>
<FontIcon FontSize="{StaticResource StandardIconSize}"
Glyph="&#xE710;" />
</Button.Content>
</Button>
</local:SettingContainer>
</StackPanel>
</Grid>
</StackPanel>
</Page>

View File

@ -0,0 +1,321 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "NewTabMenuViewModel.h"
#include <LibraryResources.h>
#include "NewTabMenuViewModel.g.cpp"
#include "NewTabMenuEntryViewModel.g.cpp"
#include "ProfileEntryViewModel.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(IVector<Model::NewTabMenuEntry> settingsModelEntries)
{
auto result = single_threaded_observable_vector<Editor::NewTabMenuEntryViewModel>();
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())
{
const auto profileEntryVM = make<ProfileEntryViewModel>(profileEntry);
result.Append(profileEntryVM);
}
break;
}
case NewTabMenuEntryType::Separator:
{
if (const auto& separatorEntry = entry.as<Model::SeparatorEntry>())
{
const auto separatorEntryVM = make<SeparatorEntryViewModel>(separatorEntry);
result.Append(separatorEntryVM);
}
break;
}
case NewTabMenuEntryType::Folder:
{
if (const auto& folderEntry = entry.as<Model::FolderEntry>())
{
const auto folderEntryVM = make<FolderEntryViewModel>(folderEntry);
result.Append(folderEntryVM);
}
break;
}
case NewTabMenuEntryType::MatchProfiles:
{
if (const auto& matchProfilesEntry = entry.as<Model::MatchProfilesEntry>())
{
const auto matchProfilesEntryVM = make<MatchProfilesEntryViewModel>(matchProfilesEntry);
result.Append(matchProfilesEntryVM);
}
break;
}
case NewTabMenuEntryType::RemainingProfiles:
{
if (const auto& remainingProfilesEntry = entry.as<Model::RemainingProfilesEntry>())
{
const auto remainingProfilesEntryVM = make<RemainingProfilesEntryViewModel>(remainingProfilesEntry);
result.Append(remainingProfilesEntryVM);
}
break;
}
case NewTabMenuEntryType::Invalid:
default:
break;
}
}
return result;
}
bool NewTabMenuViewModel::IsRemainingProfilesEntryMissing(const IObservableVector<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;
}
}
default:
break;
}
}
return true;
};
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");
}
});
}
void NewTabMenuViewModel::UpdateSettings(Model::CascadiaSettings settings)
{
_Settings = settings;
_NotifyChanges(L"AvailableProfiles");
const auto& newTabMenuEntries = _Settings.GlobalSettings().NewTabMenu();
Entries(_ConvertToViewModelEntries(newTabMenuEntries));
SelectedProfile(AvailableProfiles().GetAt(0));
_Entries.VectorChanged([this](auto&&, const IVectorChangedEventArgs& args) {
switch (args.CollectionChange())
{
case CollectionChange::Reset:
{
// fully replace settings model with _Entries
for (const auto& entry : _Entries)
{
auto modelEntries = single_threaded_vector<Model::NewTabMenuEntry>();
modelEntries.Append(NewTabMenuEntryViewModel::GetModel(entry));
_Settings.GlobalSettings().NewTabMenu(modelEntries);
}
return;
}
case CollectionChange::ItemInserted:
{
const auto& insertedEntry = _Entries.GetAt(args.Index());
auto newTabMenu = _Settings.GlobalSettings().NewTabMenu();
newTabMenu.InsertAt(args.Index(), NewTabMenuEntryViewModel::GetModel(insertedEntry));
return;
}
case CollectionChange::ItemRemoved:
{
auto newTabMenu = _Settings.GlobalSettings().NewTabMenu();
newTabMenu.RemoveAt(args.Index());
return;
}
case CollectionChange::ItemChanged:
{
auto newTabMenu = _Settings.GlobalSettings().NewTabMenu();
const auto modifiedEntry = _Entries.GetAt(args.Index());
newTabMenu.SetAt(args.Index(), NewTabMenuEntryViewModel::GetModel(modifiedEntry));
return;
}
}
});
}
void NewTabMenuViewModel::RequestAddSelectedProfileEntry()
{
if (_SelectedProfile)
{
Model::ProfileEntry profileEntry;
profileEntry.Profile(_SelectedProfile);
_Settings.GlobalSettings().NewTabMenu().Append(profileEntry);
_Entries.Append(make<ProfileEntryViewModel>(profileEntry));
}
}
void NewTabMenuViewModel::RequestAddSeparatorEntry()
{
Model::SeparatorEntry separatorEntry;
_Settings.GlobalSettings().NewTabMenu().Append(separatorEntry);
_Entries.Append(make<SeparatorEntryViewModel>(separatorEntry));
}
void NewTabMenuViewModel::RequestAddFolderEntry()
{
Model::FolderEntry folderEntry;
folderEntry.Name(_FolderName);
_Settings.GlobalSettings().NewTabMenu().Append(folderEntry);
_Entries.Append(make<FolderEntryViewModel>(folderEntry));
// Clear the field after adding the entry
FolderName({});
}
void NewTabMenuViewModel::RequestAddProfileMatcherEntry()
{
Model::MatchProfilesEntry matchProfilesEntry;
matchProfilesEntry.Name(_ProfileMatcherName);
matchProfilesEntry.Source(_ProfileMatcherSource);
matchProfilesEntry.Commandline(_ProfileMatcherCommandline);
_Settings.GlobalSettings().NewTabMenu().Append(matchProfilesEntry);
_Entries.Append(make<MatchProfilesEntryViewModel>(matchProfilesEntry));
// Clear the fields after adding the entry
ProfileMatcherName({});
ProfileMatcherSource({});
ProfileMatcherCommandline({});
}
void NewTabMenuViewModel::RequestAddRemainingProfilesEntry()
{
Model::RemainingProfilesEntry remainingProfilesEntry;
_Settings.GlobalSettings().NewTabMenu().Append(remainingProfilesEntry);
_Entries.Append(make<RemainingProfilesEntryViewModel>(remainingProfilesEntry));
}
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::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 }
{
}
SeparatorEntryViewModel::SeparatorEntryViewModel(Model::SeparatorEntry separatorEntry) :
SeparatorEntryViewModelT<SeparatorEntryViewModel, NewTabMenuEntryViewModel>(Model::NewTabMenuEntryType::Separator),
_SeparatorEntry{ separatorEntry }
{
}
FolderEntryViewModel::FolderEntryViewModel(Model::FolderEntry folderEntry) :
FolderEntryViewModelT<FolderEntryViewModel, NewTabMenuEntryViewModel>(Model::NewTabMenuEntryType::Folder),
_FolderEntry{ folderEntry }
{
_Entries = _ConvertToViewModelEntries(_FolderEntry.Entries());
}
MatchProfilesEntryViewModel::MatchProfilesEntryViewModel(Model::MatchProfilesEntry matchProfilesEntry) :
MatchProfilesEntryViewModelT<MatchProfilesEntryViewModel, NewTabMenuEntryViewModel>(Model::NewTabMenuEntryType::MatchProfiles),
_MatchProfilesEntry{ matchProfilesEntry }
{
}
hstring MatchProfilesEntryViewModel::DisplayText() const
{
std::wstringstream ss;
if (const auto profileName = _MatchProfilesEntry.Name(); !profileName.empty())
{
ss << fmt::format(L"profile: {}, ", profileName);
}
if (const auto commandline = _MatchProfilesEntry.Commandline(); !commandline.empty())
{
ss << fmt::format(L"profile: {}, ", commandline);
}
if (const auto source = _MatchProfilesEntry.Source(); !source.empty())
{
ss << fmt::format(L"profile: {}, ", source);
}
// Chop off the last ", "
auto s = ss.str();
return winrt::hstring{ s.substr(0, s.size() - 2) };
}
RemainingProfilesEntryViewModel::RemainingProfilesEntryViewModel(Model::RemainingProfilesEntry remainingProfilesEntry) :
RemainingProfilesEntryViewModelT<RemainingProfilesEntryViewModel, NewTabMenuEntryViewModel>(Model::NewTabMenuEntryType::RemainingProfiles),
_RemainingProfilesEntry{ remainingProfilesEntry }
{
}
}

View File

@ -0,0 +1,113 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#include "NewTabMenuViewModel.g.h"
#include "NewTabMenuEntryViewModel.g.h"
#include "ProfileEntryViewModel.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);
static bool IsRemainingProfilesEntryMissing(const Windows::Foundation::Collections::IObservableVector<Editor::NewTabMenuEntryViewModel>& entries);
void UpdateSettings(Model::CascadiaSettings settings);
void RequestAddSelectedProfileEntry();
void RequestAddSeparatorEntry();
void RequestAddFolderEntry();
void RequestAddProfileMatcherEntry();
void RequestAddRemainingProfilesEntry();
Windows::Foundation::Collections::IObservableVector<Model::Profile> AvailableProfiles() { return _Settings.AllProfiles(); }
VIEW_MODEL_OBSERVABLE_PROPERTY(Windows::Foundation::Collections::IObservableVector<Editor::NewTabMenuEntryViewModel>, 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, FolderName);
private:
Model::CascadiaSettings _Settings{ nullptr };
};
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 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);
GETSET_OBSERVABLE_PROJECTED_SETTING(_FolderEntry, Name);
GETSET_OBSERVABLE_PROJECTED_SETTING(_FolderEntry, Icon);
VIEW_MODEL_OBSERVABLE_PROPERTY(Windows::Foundation::Collections::IObservableVector<Editor::NewTabMenuEntryViewModel>, Entries);
VIEW_MODEL_OBSERVABLE_PROPERTY(Model::FolderEntry, FolderEntry, nullptr);
};
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 remainingProfielsEntry);
VIEW_MODEL_OBSERVABLE_PROPERTY(Model::RemainingProfilesEntry, RemainingProfilesEntry, nullptr);
};
};
namespace winrt::Microsoft::Terminal::Settings::Editor::factory_implementation
{
BASIC_FACTORY(NewTabMenuViewModel);
BASIC_FACTORY(ProfileEntryViewModel);
BASIC_FACTORY(SeparatorEntryViewModel);
BASIC_FACTORY(FolderEntryViewModel);
BASIC_FACTORY(MatchProfilesEntryViewModel);
BASIC_FACTORY(RemainingProfilesEntryViewModel);
}

View File

@ -0,0 +1,68 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import "ProfileViewModel.idl";
namespace Microsoft.Terminal.Settings.Editor
{
runtimeclass NewTabMenuViewModel : Windows.UI.Xaml.Data.INotifyPropertyChanged
{
NewTabMenuViewModel(Microsoft.Terminal.Settings.Model.CascadiaSettings settings);
IObservableVector<NewTabMenuEntryViewModel> Entries;
IObservableVector<Microsoft.Terminal.Settings.Model.Profile> AvailableProfiles { get; };
Microsoft.Terminal.Settings.Model.Profile SelectedProfile;
String ProfileMatcherName;
String ProfileMatcherSource;
String ProfileMatcherCommandline;
String FolderName;
void RequestAddSelectedProfileEntry();
void RequestAddSeparatorEntry();
void RequestAddFolderEntry();
void RequestAddProfileMatcherEntry();
void RequestAddRemainingProfilesEntry();
static Boolean IsRemainingProfilesEntryMissing(IObservableVector<NewTabMenuEntryViewModel> entries);
}
[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 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);
String Name;
String Icon;
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

@ -1839,4 +1839,72 @@
<value>Non-monospace fonts:</value>
<comment>This is a label that is followed by a list of proportional fonts.</comment>
</data>
<data name="Profile_FontFace_ProportionalFontWarning.Title" xml:space="preserve">
<value>Warning:</value>
<comment>Title for the warning info bar used when a non monospace font face is chosen to indicate that there may be visual artifacts</comment>
</data>
<data name="Profile_FontFace_ProportionalFontWarning.Message" xml:space="preserve">
<value>Choosing a non-monospaced font will likely result in visual artifacts. Use at your own discretion.</value>
<comment>Warning info bar used when a non monospace font face is chosen to indicate that there may be visual artifacts</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.Content" xml:space="preserve">
<value>&lt;Separator&gt;</value>
<comment>{Locked="&lt;"}, {Locked="&gt;"}</comment>
</data>
<data name="NewTabMenuEntry_RemainingProfiles.Content" xml:space="preserve">
<value>&lt;Remaining profiles&gt;</value>
<comment>{Locked="&lt;"}{Locked="&gt;"}</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_AddMatchProfiles_Confirmation.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>
</root>

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

@ -180,10 +180,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"
Glyph="{TemplateBinding FontIconGlyph}"
Margin="0,0,10,0"/>
<StackPanel Grid.Column="1"
Style="{StaticResource StackPanelInExpanderStyle}">
<StackPanel Orientation="Horizontal">
<TextBlock Style="{StaticResource SettingsPageItemHeaderStyle}"
Text="{TemplateBinding Header}" />
@ -197,7 +202,7 @@
Style="{StaticResource SettingsPageItemDescriptionStyle}"
Text="{TemplateBinding HelpText}" />
</StackPanel>
<ContentPresenter Grid.Column="1"
<ContentPresenter Grid.Column="2"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Content="{TemplateBinding Content}" />
@ -224,10 +229,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"
Glyph="{TemplateBinding FontIconGlyph}"
Margin="0,0,10,0"/>
<StackPanel Grid.Column="1"
Style="{StaticResource StackPanelInExpanderStyle}">
<StackPanel Orientation="Horizontal">
<TextBlock Style="{StaticResource SettingsPageItemHeaderStyle}"
Text="{TemplateBinding Header}" />
@ -241,7 +251,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,25 @@ 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) \
{ \
const auto t = target; \
if (t.name() != value) \
{ \
t.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

@ -34,3 +34,10 @@ winrt::com_ptr<NewTabMenuEntry> ActionEntry::FromJson(const Json::Value& json)
return entry;
}
winrt::com_ptr<ActionEntry> ActionEntry::Copy() const
{
auto entry = winrt::make_self<ActionEntry>();
entry->_ActionId = _ActionId;
return entry;
}

View File

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

View File

@ -3,6 +3,11 @@
#include "pch.h"
#include "FolderEntry.h"
#include "ProfileEntry.h"
#include "SeparatorEntry.h"
#include "RemainingProfilesEntry.h"
#include "MatchProfilesEntry.h"
#include "ActionEntry.h"
#include "JsonUtils.h"
#include "TerminalSettingsSerializationHelpers.h"
@ -83,7 +88,7 @@ IVector<NewTabMenuEntryModel> FolderEntry::Entries() const
// 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>();
const auto profileEntry = entry.as<Model::ProfileEntry>();
if (profileEntry.Profile() == nullptr)
{
continue;
@ -95,7 +100,7 @@ IVector<NewTabMenuEntryModel> FolderEntry::Entries() const
case NewTabMenuEntryType::RemainingProfiles:
case NewTabMenuEntryType::MatchProfiles:
{
const auto profileCollectionEntry = entry.as<ProfileCollectionEntry>();
const auto profileCollectionEntry = entry.as<Model::ProfileCollectionEntry>();
if (profileCollectionEntry.Profiles().Size() == 0)
{
continue;
@ -122,3 +127,47 @@ IVector<NewTabMenuEntryModel> FolderEntry::Entries() const
return result;
}
winrt::com_ptr<FolderEntry> FolderEntry::Copy() const
{
auto entry = winrt::make_self<FolderEntry>();
entry->_Name = _Name;
entry->_Icon = _Icon;
entry->_Inlining = _Inlining;
entry->_AllowEmpty = _AllowEmpty;
if (_Entries)
{
entry->_Entries = winrt::single_threaded_vector<Model::NewTabMenuEntry>();
for (const auto& e : _Entries)
{
switch (e.Type())
{
case NewTabMenuEntryType::Profile:
entry->_Entries.Append(*winrt::get_self<implementation::ProfileEntry>(e.as<Model::ProfileEntry>())->Copy());
break;
case NewTabMenuEntryType::Separator:
entry->_Entries.Append(*winrt::get_self<implementation::SeparatorEntry>(e.as<Model::SeparatorEntry>())->Copy());
break;
case NewTabMenuEntryType::Folder:
entry->_Entries.Append(*winrt::get_self<implementation::FolderEntry>(e.as<Model::FolderEntry>())->Copy());
break;
case NewTabMenuEntryType::RemainingProfiles:
entry->_Entries.Append(*winrt::get_self<implementation::RemainingProfilesEntry>(e.as<Model::RemainingProfilesEntry>())->Copy());
break;
case NewTabMenuEntryType::MatchProfiles:
entry->_Entries.Append(*winrt::get_self<implementation::MatchProfilesEntry>(e.as<Model::MatchProfilesEntry>())->Copy());
break;
case NewTabMenuEntryType::Action:
{
entry->_Entries.Append(*winrt::get_self<implementation::ActionEntry>(e.as<Model::ActionEntry>())->Copy());
break;
}
case NewTabMenuEntryType::Invalid:
// ignore invalid
break;
}
}
}
return entry;
}

View File

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

View File

@ -6,6 +6,12 @@
#include "../../types/inc/Utils.hpp"
#include "JsonUtils.h"
#include "KeyChordSerialization.h"
#include "FolderEntry.h"
#include "ProfileEntry.h"
#include "SeparatorEntry.h"
#include "RemainingProfilesEntry.h"
#include "MatchProfilesEntry.h"
#include "ActionEntry.h"
#include "GlobalAppSettings.g.cpp"
@ -79,6 +85,39 @@ 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 (auto entry : *_NewTabMenu)
{
switch (entry.Type())
{
case NewTabMenuEntryType::Profile:
globals->_NewTabMenu->Append(*winrt::get_self<ProfileEntry>(entry.as<Model::ProfileEntry>())->Copy());
break;
case NewTabMenuEntryType::Separator:
globals->_NewTabMenu->Append(*winrt::get_self<SeparatorEntry>(entry.as<Model::SeparatorEntry>())->Copy());
break;
case NewTabMenuEntryType::Folder:
globals->_NewTabMenu->Append(*winrt::get_self<FolderEntry>(entry.as<Model::FolderEntry>())->Copy());
break;
case NewTabMenuEntryType::RemainingProfiles:
globals->_NewTabMenu->Append(*winrt::get_self<RemainingProfilesEntry>(entry.as<Model::RemainingProfilesEntry>())->Copy());
break;
case NewTabMenuEntryType::MatchProfiles:
globals->_NewTabMenu->Append(*winrt::get_self<MatchProfilesEntry>(entry.as<Model::MatchProfilesEntry>())->Copy());
break;
case NewTabMenuEntryType::Action:
{
globals->_NewTabMenu->Append(*winrt::get_self<ActionEntry>(entry.as<Model::ActionEntry>())->Copy());
break;
}
case NewTabMenuEntryType::Invalid:
// ignore invalid
break;
}
}
}
for (const auto& parent : _parents)
{

View File

@ -71,3 +71,12 @@ bool MatchProfilesEntry::MatchesProfile(const Model::Profile& profile)
return isMatching.value_or(false);
}
winrt::com_ptr<MatchProfilesEntry> 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;
winrt::com_ptr<MatchProfilesEntry> Copy() const;
Json::Value ToJson() const override;
static com_ptr<NewTabMenuEntry> FromJson(const Json::Value& json);

View File

@ -57,3 +57,12 @@ winrt::com_ptr<NewTabMenuEntry> ProfileEntry::FromJson(const Json::Value& json)
return entry;
}
winrt::com_ptr<ProfileEntry> ProfileEntry::Copy() const
{
auto entry{ winrt::make_self<ProfileEntry>() };
entry->_Profile = _Profile;
entry->_ProfileIndex = _ProfileIndex;
entry->_ProfileName = _ProfileName;
return entry;
}

View File

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

View File

@ -20,3 +20,9 @@ winrt::com_ptr<NewTabMenuEntry> RemainingProfilesEntry::FromJson(const Json::Val
{
return winrt::make_self<RemainingProfilesEntry>();
}
winrt::com_ptr<RemainingProfilesEntry> RemainingProfilesEntry::Copy() const
{
auto entry = winrt::make_self<RemainingProfilesEntry>();
return entry;
}

View File

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

View File

@ -19,3 +19,8 @@ winrt::com_ptr<NewTabMenuEntry> SeparatorEntry::FromJson(const Json::Value&)
{
return winrt::make_self<SeparatorEntry>();
}
winrt::com_ptr<SeparatorEntry> SeparatorEntry::Copy() const
{
return winrt::make_self<SeparatorEntry>();
}

View File

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