Add IconPicker to New Tab Menu folders in SUI (#19591)

## Summary of the Pull Request
This PR pulls out the icon picker used in Profiles_Base.xaml to be its
own control. Then it's reused as a way to set an icon on folders in the
new tab menu.

As a part of pulling out the icon picker into its own control, some
minor code-health polish was added (i.e. lazy loading icon types and
built in icons).

The new tab menu didn't have a `NavigateToPageArgs`, so I took the
declaration from #19519 and moved it here. I chose to do that instead of
creating a `NavigateToNewTabMenuArgs` since that's more strict and it
would be removed as a part of #19519 anyways.

Aside from that, the PR is pretty straightforward.

## References and Relevant Issues
Part of #18281

## Validation Steps Performed
- Profile icon picker
   - [x] set none
   - [x] set built-in icon
   - [x] set emoji
   - [x] set file
   - [x] loads icons properly (for all of the scenarios above)
- New tab menu folder icon picker
   - [x] set none
   - [x] set built-in icon
   - [x] set emoji
   - [x] set file
   - [x] loads icons properly (for all of the scenarios above)
This commit is contained in:
Carlos Zamora 2026-02-02 11:35:42 -08:00 committed by GitHub
parent cdf4bd9209
commit a449899789
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 738 additions and 416 deletions

View File

@ -0,0 +1,330 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "IconPicker.h"
#include "IconPicker.g.cpp"
#include <LibraryResources.h>
#include "SegoeFluentIconList.h"
#include "../../types/inc/utils.hpp"
#include "../WinRTUtils/inc/Utils.h"
using namespace winrt;
using namespace winrt::Windows::UI;
using namespace winrt::Windows::UI::Xaml;
using namespace winrt::Windows::UI::Xaml::Navigation;
using namespace winrt::Windows::UI::Xaml::Controls;
using namespace winrt::Windows::UI::Xaml::Media;
using namespace winrt::Windows::Foundation;
using namespace winrt::Windows::Foundation::Collections;
using namespace winrt::Microsoft::UI::Xaml::Controls;
namespace winrt
{
namespace MUX = Microsoft::UI::Xaml;
namespace WUX = Windows::UI::Xaml;
}
namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
static constexpr std::wstring_view HideIconValue{ L"none" };
Windows::Foundation::Collections::IObservableVector<Editor::EnumEntry> IconPicker::_BuiltInIcons{ nullptr };
Windows::Foundation::Collections::IObservableVector<Editor::EnumEntry> IconPicker::_IconTypes{ nullptr };
DependencyProperty IconPicker::_CurrentIconPathProperty{ nullptr };
DependencyProperty IconPicker::_WindowRootProperty{ nullptr };
Windows::Foundation::Collections::IObservableVector<Editor::EnumEntry> IconPicker::BuiltInIcons() noexcept
{
if (!_BuiltInIcons)
{
// lazy load the built-in icons
std::vector<Editor::EnumEntry> builtInIcons;
for (auto& [val, name] : s_SegoeFluentIcons)
{
builtInIcons.emplace_back(make<EnumEntry>(hstring{ name }, box_value(val)));
}
_BuiltInIcons = single_threaded_observable_vector<Editor::EnumEntry>(std::move(builtInIcons));
}
return _BuiltInIcons;
}
Windows::Foundation::Collections::IObservableVector<Editor::EnumEntry> IconPicker::IconTypes() noexcept
{
if (!_IconTypes)
{
// lazy load the icon types
std::vector<Editor::EnumEntry> iconTypes;
iconTypes.reserve(4);
iconTypes.emplace_back(make<EnumEntry>(RS_(L"IconPicker_IconTypeNone"), box_value(IconType::None)));
iconTypes.emplace_back(make<EnumEntry>(RS_(L"IconPicker_IconTypeFontIcon"), box_value(IconType::FontIcon)));
iconTypes.emplace_back(make<EnumEntry>(RS_(L"IconPicker_IconTypeEmoji"), box_value(IconType::Emoji)));
iconTypes.emplace_back(make<EnumEntry>(RS_(L"IconPicker_IconTypeImage"), box_value(IconType::Image)));
_IconTypes = winrt::single_threaded_observable_vector<Editor::EnumEntry>(std::move(iconTypes));
}
return _IconTypes;
}
IconPicker::IconPicker()
{
_InitializeProperties();
InitializeComponent();
_DeduceCurrentIconType();
PropertyChanged([this](auto&&, const PropertyChangedEventArgs& args) {
const auto propertyName{ args.PropertyName() };
// "CurrentIconPath" changes are handled by _OnCurrentIconPathChanged()
if (propertyName == L"CurrentIconType")
{
PropertyChanged.raise(*this, Windows::UI::Xaml::Data::PropertyChangedEventArgs{ L"UsingNoIcon" });
PropertyChanged.raise(*this, Windows::UI::Xaml::Data::PropertyChangedEventArgs{ L"UsingBuiltInIcon" });
PropertyChanged.raise(*this, Windows::UI::Xaml::Data::PropertyChangedEventArgs{ L"UsingEmojiIcon" });
PropertyChanged.raise(*this, Windows::UI::Xaml::Data::PropertyChangedEventArgs{ L"UsingImageIcon" });
}
else if (propertyName == L"CurrentBuiltInIcon")
{
CurrentIconPath(unbox_value<hstring>(_CurrentBuiltInIcon.EnumValue()));
}
else if (propertyName == L"CurrentEmojiIcon")
{
CurrentIconPath(CurrentEmojiIcon());
}
});
}
void IconPicker::_InitializeProperties()
{
// Initialize any dependency properties here.
// This performs a lazy load on these properties, instead of
// initializing them when the DLL loads.
if (!_CurrentIconPathProperty)
{
_CurrentIconPathProperty =
DependencyProperty::Register(
L"CurrentIconPath",
xaml_typename<hstring>(),
xaml_typename<Editor::IconPicker>(),
PropertyMetadata{ nullptr, PropertyChangedCallback{ &IconPicker::_OnCurrentIconPathChanged } });
}
if (!_WindowRootProperty)
{
_WindowRootProperty =
DependencyProperty::Register(
L"WindowRoot",
xaml_typename<IHostedInWindow>(),
xaml_typename<Editor::IconPicker>(),
PropertyMetadata{ nullptr });
}
}
void IconPicker::_OnCurrentIconPathChanged(const DependencyObject& d, const DependencyPropertyChangedEventArgs& /*e*/)
{
d.as<IconPicker>()->_DeduceCurrentIconType();
}
safe_void_coroutine IconPicker::Icon_Click(const IInspectable&, const RoutedEventArgs&)
{
auto lifetime = get_strong();
const auto parentHwnd{ reinterpret_cast<HWND>(WindowRoot().GetHostingWindow()) };
auto file = co_await OpenImagePicker(parentHwnd);
if (!file.empty())
{
CurrentIconPath(file);
}
}
void IconPicker::BuiltInIconPicker_GotFocus(const IInspectable& sender, const RoutedEventArgs& /*e*/)
{
_updateIconFilter({});
sender.as<AutoSuggestBox>().IsSuggestionListOpen(true);
}
void IconPicker::BuiltInIconPicker_QuerySubmitted(const AutoSuggestBox& /*sender*/, const AutoSuggestBoxQuerySubmittedEventArgs& e)
{
const auto iconEntry = unbox_value_or<Editor::EnumEntry>(e.ChosenSuggestion(), nullptr);
if (!iconEntry)
{
return;
}
CurrentBuiltInIcon(iconEntry);
}
void IconPicker::BuiltInIconPicker_TextChanged(const AutoSuggestBox& sender, const AutoSuggestBoxTextChangedEventArgs& e)
{
if (e.Reason() != AutoSuggestionBoxTextChangeReason::UserInput)
{
return;
}
std::wstring_view filter{ sender.Text() };
filter = til::trim(filter, L' ');
_updateIconFilter(filter);
}
void IconPicker::_updateIconFilter(std::wstring_view filter)
{
if (_iconFilter != filter)
{
_filteredBuiltInIcons = nullptr;
_iconFilter = filter;
_updateFilteredIconList();
PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"FilteredBuiltInIconList" });
}
}
Windows::Foundation::Collections::IObservableVector<Editor::EnumEntry> IconPicker::FilteredBuiltInIconList()
{
if (!_filteredBuiltInIcons)
{
_updateFilteredIconList();
}
return _filteredBuiltInIcons;
}
void IconPicker::_updateFilteredIconList()
{
_filteredBuiltInIcons = BuiltInIcons();
if (_iconFilter.empty())
{
return;
}
// Find matching icons and populate the filtered list
std::vector<Editor::EnumEntry> filtered;
filtered.reserve(_filteredBuiltInIcons.Size());
for (const auto& icon : _filteredBuiltInIcons)
{
if (til::contains_linguistic_insensitive(icon.EnumName(), _iconFilter))
{
filtered.emplace_back(icon);
}
}
_filteredBuiltInIcons = winrt::single_threaded_observable_vector(std::move(filtered));
}
void IconPicker::CurrentIconType(const Windows::Foundation::IInspectable& value)
{
if (_currentIconType != value)
{
// Switching from...
if (_currentIconType && unbox_value<IconType>(_currentIconType.as<Editor::EnumEntry>().EnumValue()) == IconType::Image)
{
// Stash the current value of Icon. If the user
// switches out of then back to IconType::Image, we want
// the path that we display in the text box to remain unchanged.
_lastIconPath = CurrentIconPath();
}
// Set the member here instead of after setting Icon() below!
// We have an Icon property changed handler defined for when we discard changes.
// Inadvertently, that means that we call this setter again.
// Setting the member here means that we early exit at the beginning of the function
// because _currentIconType == value.
_currentIconType = value;
// Switched to...
switch (unbox_value<IconType>(value.as<Editor::EnumEntry>().EnumValue()))
{
case IconType::None:
{
CurrentIconPath(winrt::hstring{ HideIconValue });
break;
}
case IconType::Image:
{
if (!_lastIconPath.empty())
{
// Conversely, if we switch to Image,
// retrieve that saved value and apply it
CurrentIconPath(_lastIconPath);
}
break;
}
case IconType::FontIcon:
{
if (_CurrentBuiltInIcon)
{
CurrentIconPath(unbox_value<hstring>(_CurrentBuiltInIcon.EnumValue()));
}
break;
}
case IconType::Emoji:
{
// Don't set Icon here!
// Clear out the text box so we direct the user to use the emoji picker.
CurrentEmojiIcon({});
}
}
// We're not using the VM's Icon() setter above,
// so notify HasIcon changed manually
PropertyChanged.raise(*this, Windows::UI::Xaml::Data::PropertyChangedEventArgs{ L"CurrentIconType" });
PropertyChanged.raise(*this, Windows::UI::Xaml::Data::PropertyChangedEventArgs{ L"HasIcon" });
}
}
bool IconPicker::UsingNoIcon() const
{
return _currentIconType == IconTypes().GetAt(0);
}
bool IconPicker::UsingBuiltInIcon() const
{
return _currentIconType == IconTypes().GetAt(1);
}
bool IconPicker::UsingEmojiIcon() const
{
return _currentIconType == IconTypes().GetAt(2);
}
bool IconPicker::UsingImageIcon() const
{
return _currentIconType == IconTypes().GetAt(3);
}
void IconPicker::_DeduceCurrentIconType()
{
const auto icon = CurrentIconPath();
if (icon.empty() || icon == HideIconValue)
{
_currentIconType = IconTypes().GetAt(0);
}
else if (icon.size() == 1 && (L'\uE700' <= til::at(icon, 0) && til::at(icon, 0) <= L'\uF8B3'))
{
_currentIconType = IconTypes().GetAt(1);
_DeduceCurrentBuiltInIcon();
}
else if (::Microsoft::Console::Utils::IsLikelyToBeEmojiOrSymbolIcon(icon))
{
// We already did a range check for MDL2 Assets in the previous one,
// so if we're out of that range but still short, assume we're an emoji
_currentIconType = IconTypes().GetAt(2);
}
else
{
_currentIconType = IconTypes().GetAt(3);
}
PropertyChanged.raise(*this, Windows::UI::Xaml::Data::PropertyChangedEventArgs{ L"CurrentIconType" });
}
void IconPicker::_DeduceCurrentBuiltInIcon()
{
const auto icon = CurrentIconPath();
for (uint32_t i = 0; i < BuiltInIcons().Size(); i++)
{
const auto& builtIn = BuiltInIcons().GetAt(i);
if (icon == unbox_value<hstring>(builtIn.EnumValue()))
{
CurrentBuiltInIcon(builtIn);
return;
}
}
CurrentBuiltInIcon(BuiltInIcons().GetAt(0));
}
WUX::Controls::IconSource IconPicker::BuiltInIconConverter(const IInspectable& iconVal)
{
return Microsoft::Terminal::UI::IconPathConverter::IconSourceWUX(unbox_value<hstring>(iconVal));
}
}

View File

@ -0,0 +1,65 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#include "IconPicker.g.h"
#include "EnumEntry.h"
#include "Utils.h"
#include "cppwinrt_utils.h"
namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
struct IconPicker : public HasScrollViewer<IconPicker>, IconPickerT<IconPicker>
{
public:
IconPicker();
static constexpr std::wstring_view HideIconValue{ L"none" };
static Windows::UI::Xaml::Controls::IconSource BuiltInIconConverter(const Windows::Foundation::IInspectable& iconVal);
static Windows::Foundation::Collections::IObservableVector<Editor::EnumEntry> BuiltInIcons() noexcept;
static Windows::Foundation::Collections::IObservableVector<Editor::EnumEntry> IconTypes() noexcept;
Windows::Foundation::Collections::IObservableVector<Editor::EnumEntry> FilteredBuiltInIconList();
safe_void_coroutine Icon_Click(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& e);
void BuiltInIconPicker_GotFocus(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& e);
void BuiltInIconPicker_TextChanged(const winrt::Windows::UI::Xaml::Controls::AutoSuggestBox& sender, const Windows::UI::Xaml::Controls::AutoSuggestBoxTextChangedEventArgs& e);
void BuiltInIconPicker_QuerySubmitted(const winrt::Windows::UI::Xaml::Controls::AutoSuggestBox& sender, const Windows::UI::Xaml::Controls::AutoSuggestBoxQuerySubmittedEventArgs& e);
Windows::Foundation::IInspectable CurrentIconType() const noexcept { return _currentIconType; }
void CurrentIconType(const Windows::Foundation::IInspectable& value);
bool UsingNoIcon() const;
bool UsingBuiltInIcon() const;
bool UsingEmojiIcon() const;
bool UsingImageIcon() const;
til::property_changed_event PropertyChanged;
WINRT_OBSERVABLE_PROPERTY(hstring, CurrentEmojiIcon, PropertyChanged.raise);
WINRT_OBSERVABLE_PROPERTY(Editor::EnumEntry, CurrentBuiltInIcon, PropertyChanged.raise, nullptr);
DEPENDENCY_PROPERTY(hstring, CurrentIconPath);
DEPENDENCY_PROPERTY(IHostedInWindow, WindowRoot);
private:
static Windows::Foundation::Collections::IObservableVector<Editor::EnumEntry> _BuiltInIcons;
static Windows::Foundation::Collections::IObservableVector<Editor::EnumEntry> _IconTypes;
Windows::Foundation::Collections::IObservableVector<Editor::EnumEntry> _filteredBuiltInIcons;
std::wstring _iconFilter;
Windows::Foundation::IInspectable _currentIconType{};
winrt::hstring _lastIconPath;
static void _InitializeProperties();
static void _OnCurrentIconPathChanged(const Windows::UI::Xaml::DependencyObject& d, const Windows::UI::Xaml::DependencyPropertyChangedEventArgs& e);
void _DeduceCurrentIconType();
void _DeduceCurrentBuiltInIcon();
void _updateIconFilter(std::wstring_view filter);
void _updateFilteredIconList();
};
}
namespace winrt::Microsoft::Terminal::Settings::Editor::factory_implementation
{
BASIC_FACTORY(IconPicker);
}

View File

@ -0,0 +1,42 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import "EnumEntry.idl";
import "MainPage.idl";
namespace Microsoft.Terminal.Settings.Editor
{
enum IconType
{
None = 0,
FontIcon,
Image,
Emoji
};
[default_interface] runtimeclass IconPicker : Windows.UI.Xaml.Controls.UserControl, Windows.UI.Xaml.Data.INotifyPropertyChanged
{
IconPicker();
IInspectable CurrentIconType;
Windows.Foundation.Collections.IObservableVector<EnumEntry> IconTypes { get; };
Boolean UsingBuiltInIcon { get; };
Boolean UsingEmojiIcon { get; };
Boolean UsingImageIcon { get; };
String CurrentEmojiIcon;
EnumEntry CurrentBuiltInIcon;
Windows.Foundation.Collections.IObservableVector<EnumEntry> BuiltInIcons { get; };
Windows.Foundation.Collections.IObservableVector<EnumEntry> FilteredBuiltInIconList { get; };
static Windows.UI.Xaml.Controls.IconSource BuiltInIconConverter(IInspectable iconVal);
String CurrentIconPath;
static Windows.UI.Xaml.DependencyProperty CurrentIconPathProperty { get; };
IHostedInWindow WindowRoot;
static Windows.UI.Xaml.DependencyProperty WindowRootProperty { get; };
}
}

View File

@ -0,0 +1,100 @@
<!--
Copyright (c) Microsoft Corporation. All rights reserved. Licensed under
the MIT License. See LICENSE in the project root for license information.
-->
<UserControl x:Class="Microsoft.Terminal.Settings.Editor.IconPicker"
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">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="CommonResources.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid ColumnSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="auto" />
</Grid.ColumnDefinitions>
<!-- Icon Type -->
<ComboBox x:Uid="IconPicker_IconType"
Grid.Column="0"
ItemsSource="{x:Bind IconTypes}"
SelectedItem="{x:Bind CurrentIconType, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="local:EnumEntry">
<TextBlock Text="{x:Bind EnumName}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<!-- Built-In Icon -->
<AutoSuggestBox x:Uid="IconPicker_BuiltInIcon"
Grid.Column="1"
GotFocus="BuiltInIconPicker_GotFocus"
ItemsSource="{x:Bind FilteredBuiltInIconList, Mode=OneWay}"
QuerySubmitted="BuiltInIconPicker_QuerySubmitted"
Text="{x:Bind CurrentBuiltInIcon.EnumName, Mode=OneWay}"
TextBoxStyle="{StaticResource TextBoxSettingStyle}"
TextChanged="BuiltInIconPicker_TextChanged"
Visibility="{x:Bind UsingBuiltInIcon, Mode=OneWay}">
<AutoSuggestBox.ItemTemplate>
<DataTemplate x:DataType="local:EnumEntry">
<Grid ColumnSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<IconSourceElement Grid.Column="0"
Width="16"
Height="16"
IconSource="{x:Bind local:IconPicker.BuiltInIconConverter(EnumValue), Mode=OneTime}" />
<TextBlock Grid.Column="1"
Text="{x:Bind EnumName}" />
</Grid>
</DataTemplate>
</AutoSuggestBox.ItemTemplate>
</AutoSuggestBox>
<!-- Image (File) Icon -->
<TextBox x:Uid="IconPicker_ImagePathBox"
Grid.Column="1"
MaxWidth="Infinity"
HorizontalAlignment="Stretch"
FontFamily="Segoe UI, Segoe Fluent Icons, Segoe MDL2 Assets"
IsSpellCheckEnabled="False"
Style="{StaticResource TextBoxSettingStyle}"
Text="{x:Bind CurrentIconPath, Mode=TwoWay}"
Visibility="{x:Bind UsingImageIcon, Mode=OneWay}" />
<Button x:Uid="IconPicker_IconBrowse"
Grid.Column="2"
Margin="0"
VerticalAlignment="Top"
Click="Icon_Click"
Style="{StaticResource BrowseButtonStyle}"
Visibility="{x:Bind UsingImageIcon, Mode=OneWay}" />
<!-- Emoji Icon -->
<TextBox x:Uid="IconPicker_EmojiBox"
Grid.Column="1"
MaxWidth="Infinity"
FontFamily="Segoe UI, Segoe Fluent Icons, Segoe MDL2 Assets"
IsSpellCheckEnabled="False"
Style="{StaticResource TextBoxSettingStyle}"
Text="{x:Bind CurrentEmojiIcon, Mode=TwoWay}"
Visibility="{x:Bind UsingEmojiIcon, Mode=OneWay}" />
</Grid>
</UserControl>

View File

@ -83,7 +83,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
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);
contentFrame().Navigate(xaml_typename<Editor::NewTabMenu>(), winrt::make<NavigateToPageArgs>(_newTabMenuPageVM, *this));
}
});
@ -538,7 +538,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
else
{
// Navigate to the NewTabMenu page
contentFrame().Navigate(xaml_typename<Editor::NewTabMenu>(), _newTabMenuPageVM);
contentFrame().Navigate(xaml_typename<Editor::NewTabMenu>(), winrt::make<NavigateToPageArgs>(_newTabMenuPageVM, *this));
const auto crumb = winrt::make<Breadcrumb>(box_value(clickedItemTag), RS_(L"Nav_NewTabMenu/Content"), BreadcrumbSubPage::None);
_breadcrumbs.Append(crumb);
}
@ -659,7 +659,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
_PreNavigateHelper();
contentFrame().Navigate(xaml_typename<Editor::NewTabMenu>(), _newTabMenuPageVM);
contentFrame().Navigate(xaml_typename<Editor::NewTabMenu>(), winrt::make<NavigateToPageArgs>(_newTabMenuPageVM, *this));
const auto crumb = winrt::make<Breadcrumb>(box_value(newTabMenuTag), RS_(L"Nav_NewTabMenu/Content"), BreadcrumbSubPage::None);
_breadcrumbs.Append(crumb);

View File

@ -5,6 +5,7 @@
#include "MainPage.g.h"
#include "Breadcrumb.g.h"
#include "NavigateToPageArgs.g.h"
#include "Utils.h"
namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
@ -23,6 +24,21 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
WINRT_PROPERTY(BreadcrumbSubPage, SubPage);
};
struct NavigateToPageArgs : NavigateToPageArgsT<NavigateToPageArgs>
{
public:
NavigateToPageArgs(Windows::Foundation::IInspectable viewModel, Editor::IHostedInWindow windowRoot) :
_ViewModel(viewModel),
_WindowRoot(windowRoot) {}
Editor::IHostedInWindow WindowRoot() const noexcept { return _WindowRoot; }
Windows::Foundation::IInspectable ViewModel() const noexcept { return _ViewModel; }
private:
Editor::IHostedInWindow _WindowRoot{ nullptr };
Windows::Foundation::IInspectable _ViewModel{ nullptr };
};
struct MainPage : MainPageT<MainPage>
{
MainPage() = delete;

View File

@ -16,6 +16,14 @@ namespace Microsoft.Terminal.Settings.Editor
UInt64 GetHostingWindow();
}
// This is used to pass data between pages during navigation.
// All pages will migrate to using this in GH #19519.
runtimeclass NavigateToPageArgs
{
IHostedInWindow WindowRoot { get; };
IInspectable ViewModel { get; };
}
enum BreadcrumbSubPage
{
None = 0,

View File

@ -69,6 +69,10 @@
<DependentUpon>NullableColorPicker.xaml</DependentUpon>
<SubType>Code</SubType>
</ClInclude>
<ClInclude Include="IconPicker.h">
<DependentUpon>IconPicker.xaml</DependentUpon>
<SubType>Code</SubType>
</ClInclude>
<ClInclude Include="EditColorScheme.h">
<DependentUpon>EditColorScheme.xaml</DependentUpon>
<SubType>Code</SubType>
@ -193,6 +197,9 @@
<Page Include="NullableColorPicker.xaml">
<SubType>Designer</SubType>
</Page>
<Page Include="IconPicker.xaml">
<SubType>Designer</SubType>
</Page>
<Page Include="EditColorScheme.xaml">
<SubType>Designer</SubType>
</Page>
@ -269,6 +276,10 @@
<DependentUpon>NullableColorPicker.xaml</DependentUpon>
<SubType>Code</SubType>
</ClCompile>
<ClCompile Include="IconPicker.cpp">
<DependentUpon>IconPicker.xaml</DependentUpon>
<SubType>Code</SubType>
</ClCompile>
<ClCompile Include="EditColorScheme.cpp">
<DependentUpon>EditColorScheme.xaml</DependentUpon>
<SubType>Code</SubType>
@ -397,6 +408,10 @@
<DependentUpon>NullableColorPicker.xaml</DependentUpon>
<SubType>Code</SubType>
</Midl>
<Midl Include="IconPicker.idl">
<DependentUpon>IconPicker.xaml</DependentUpon>
<SubType>Code</SubType>
</Midl>
<Midl Include="EditColorScheme.idl">
<DependentUpon>EditColorScheme.xaml</DependentUpon>
<SubType>Code</SubType>

View File

@ -55,6 +55,7 @@
<Page Include="AddProfile.xaml" />
<Page Include="KeyChordListener.xaml" />
<Page Include="NullableColorPicker.xaml" />
<Page Include="IconPicker.xaml" />
<Page Include="NewTabMenu.xaml" />
</ItemGroup>
</Project>

View File

@ -4,6 +4,7 @@
#include "pch.h"
#include "NewTabMenu.h"
#include "NewTabMenu.g.cpp"
#include "NavigateToPageArgs.g.h"
#include "NewTabMenuEntryTemplateSelector.g.cpp"
#include "EnumEntry.h"
@ -41,7 +42,9 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
void NewTabMenu::OnNavigatedTo(const NavigationEventArgs& e)
{
_ViewModel = e.Parameter().as<Editor::NewTabMenuViewModel>();
const auto args = e.Parameter().as<Editor::NavigateToPageArgs>();
_ViewModel = args.ViewModel().as<Editor::NewTabMenuViewModel>();
_windowRoot = args.WindowRoot();
TraceLoggingWrite(
g_hTerminalSettingsEditorProvider,

View File

@ -38,11 +38,13 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
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);
Editor::IHostedInWindow WindowRoot() const noexcept { return _windowRoot; }
WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler);
WINRT_OBSERVABLE_PROPERTY(Editor::NewTabMenuViewModel, ViewModel, _PropertyChangedHandlers, nullptr);
private:
Editor::NewTabMenuEntryViewModel _draggedEntry{ nullptr };
Editor::IHostedInWindow _windowRoot;
void _ScrollToEntry(const Editor::NewTabMenuEntryViewModel& entry);
};

View File

@ -9,6 +9,7 @@ namespace Microsoft.Terminal.Settings.Editor
{
NewTabMenu();
NewTabMenuViewModel ViewModel { get; };
IHostedInWindow WindowRoot { get; };
}
[default_interface] runtimeclass NewTabMenuEntryTemplateSelector : Windows.UI.Xaml.Controls.DataTemplateSelector

View File

@ -319,29 +319,48 @@
Visibility="{x:Bind ViewModel.IsFolderView, Mode=OneWay}">
<TextBlock x:Uid="NewTabMenu_CurrentFolderTextBlock"
Style="{StaticResource TextBlockSubHeaderStyle}" />
<!-- TODO GH #18281: 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>
<!-- Icon -->
<local:SettingContainer x:Uid="NewTabMenu_CurrentFolderIcon"
CurrentValueAccessibleName="{x:Bind ViewModel.CurrentFolderLocalizedIcon, Mode=OneWay}"
Style="{StaticResource ExpanderSettingContainerStyleWithComplexPreview}">
<local:SettingContainer.CurrentValue>
<Grid>
<ContentControl Width="16"
Height="16"
Content="{x:Bind ViewModel.CurrentFolderIconPreview, Mode=OneWay}"
IsTabStop="False"
Visibility="{x:Bind mtu:Converters.InvertedBooleanToVisibility(ViewModel.CurrentFolderUsingNoIcon), Mode=OneWay}" />
<TextBlock Margin="0,0,0,0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
FontFamily="Segoe UI, Segoe Fluent Icons, Segoe MDL2 Assets"
Style="{StaticResource SettingsPageItemDescriptionStyle}"
Text="{x:Bind ViewModel.CurrentFolderLocalizedIcon, Mode=OneWay}"
Visibility="{x:Bind ViewModel.CurrentFolderUsingNoIcon, Mode=OneWay}" />
</Grid>
</local:SettingContainer.CurrentValue>
<local:SettingContainer.Content>
<local:IconPicker CurrentIconPath="{x:Bind ViewModel.CurrentFolderIconPath, Mode=TwoWay}"
WindowRoot="{x:Bind WindowRoot, Mode=OneWay}" />
</local:SettingContainer.Content>
</local:SettingContainer>
<!-- Inlining -->
<local:SettingContainer x:Uid="NewTabMenu_CurrentFolderInlining"
Grid.Row="1">
<local:SettingContainer x:Uid="NewTabMenu_CurrentFolderInlining">
<ToggleSwitch IsOn="{x:Bind ViewModel.CurrentFolderInlining, Mode=TwoWay}"
Style="{StaticResource ToggleSwitchInExpanderStyle}" />
</local:SettingContainer>
<!-- Allow Empty -->
<local:SettingContainer x:Uid="NewTabMenu_CurrentFolderAllowEmpty"
Grid.Row="2">
<local:SettingContainer x:Uid="NewTabMenu_CurrentFolderAllowEmpty">
<ToggleSwitch IsOn="{x:Bind ViewModel.CurrentFolderAllowEmpty, Mode=TwoWay}"
Style="{StaticResource ToggleSwitchInExpanderStyle}" />
</local:SettingContainer>

View File

@ -3,6 +3,7 @@
#include "pch.h"
#include "NewTabMenuViewModel.h"
#include "IconPicker.h"
#include "NewTabMenuViewModel.g.cpp"
#include "FolderTreeViewEntry.g.cpp"
@ -160,6 +161,10 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
// FolderTree needs to be updated when a folder is renamed
_folderTreeCache = nullptr;
}
else if (viewModelProperty == L"Icon")
{
_NotifyChanges(L"CurrentFolderIconPreview", L"CurrentFolderLocalizedIcon", L"CurrentFolderIconPath", L"CurrentFolderUsingNoIcon");
}
}
hstring NewTabMenuViewModel::CurrentFolderName() const
@ -216,6 +221,60 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
}
}
Windows::UI::Xaml::Controls::IconElement NewTabMenuViewModel::CurrentFolderIconPreview() const
{
if (!_CurrentFolder)
{
return nullptr;
}
// IconWUX sets the icon width/height to 32 by default
auto icon = Microsoft::Terminal::UI::IconPathConverter::IconWUX(_CurrentFolder.Icon());
icon.Width(16);
icon.Height(16);
return icon;
}
winrt::hstring NewTabMenuViewModel::CurrentFolderLocalizedIcon() const
{
if (!_CurrentFolder)
{
return {};
}
if (CurrentFolderUsingNoIcon())
{
return RS_(L"IconPicker_IconTypeNone");
}
return _CurrentFolder.Icon(); // For display as a string
}
winrt::hstring NewTabMenuViewModel::CurrentFolderIconPath() const
{
if (!_CurrentFolder)
{
return {};
}
return _CurrentFolder.Icon();
}
void NewTabMenuViewModel::CurrentFolderIconPath(const winrt::hstring& path)
{
if (_CurrentFolder && _CurrentFolder.Icon() != path)
{
_CurrentFolder.Icon(path);
_NotifyChanges(L"CurrentFolderIconPreview", L"CurrentFolderLocalizedIcon", L"CurrentFolderIconPath", L"CurrentFolderUsingNoIcon");
}
}
bool NewTabMenuViewModel::CurrentFolderUsingNoIcon() const noexcept
{
if (!_CurrentFolder)
{
return false;
}
const auto icon{ _CurrentFolder.Icon() };
return icon.empty() || icon == IconPicker::HideIconValue;
}
Windows::Foundation::Collections::IObservableVector<Editor::NewTabMenuEntryViewModel> NewTabMenuViewModel::CurrentView() const
{
if (!_CurrentFolder)

View File

@ -47,6 +47,12 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
bool CurrentFolderAllowEmpty() const;
void CurrentFolderAllowEmpty(bool value);
Windows::UI::Xaml::Controls::IconElement CurrentFolderIconPreview() const;
winrt::hstring CurrentFolderLocalizedIcon() const;
winrt::hstring CurrentFolderIconPath() const;
void CurrentFolderIconPath(const winrt::hstring& path);
bool CurrentFolderUsingNoIcon() const noexcept;
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;
@ -134,6 +140,11 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
void Inlining(bool value);
hstring Icon() const { return _FolderEntry.Icon().Path(); }
void Icon(const hstring& value)
{
_FolderEntry.Icon(Model::MediaResourceHelper::FromString(value));
_NotifyChanges(L"Icon");
}
GETSET_OBSERVABLE_PROJECTED_SETTING(_FolderEntry, Name);
GETSET_OBSERVABLE_PROJECTED_SETTING(_FolderEntry, AllowEmpty);

View File

@ -41,6 +41,11 @@ namespace Microsoft.Terminal.Settings.Editor
String ProfileMatcherCommandline;
String AddFolderName;
Windows.UI.Xaml.Controls.IconElement CurrentFolderIconPreview { get; };
String CurrentFolderLocalizedIcon { get; };
String CurrentFolderIconPath;
Boolean CurrentFolderUsingNoIcon { get; };
void RequestReorderEntry(NewTabMenuEntryViewModel vm, Boolean goingUp);
void RequestDeleteEntry(NewTabMenuEntryViewModel vm);
void RequestMoveEntriesToFolder(IVector<NewTabMenuEntryViewModel> entries, FolderEntryViewModel folderEntry);
@ -83,7 +88,7 @@ namespace Microsoft.Terminal.Settings.Editor
FolderEntryViewModel(Microsoft.Terminal.Settings.Model.FolderEntry folderEntry, Microsoft.Terminal.Settings.Model.CascadiaSettings settings);
String Name;
String Icon { get; };
String Icon;
Boolean Inlining;
Boolean AllowEmpty;
IObservableVector<Microsoft.Terminal.Settings.Editor.NewTabMenuEntryViewModel> Entries;

View File

@ -4,14 +4,13 @@
#include "pch.h"
#include "ProfileViewModel.h"
#include "ProfileViewModel.g.cpp"
#include "EnumEntry.h"
#include "Appearances.h"
#include "EnumEntry.h"
#include "IconPicker.h"
#include "../WinRTUtils/inc/Utils.h"
#include "../../renderer/base/FontCache.h"
#include "../TerminalSettingsAppAdapterLib/TerminalSettings.h"
#include "SegoeFluentIconList.h"
#include "../../types/inc/utils.hpp"
using namespace winrt::Windows::UI::Text;
using namespace winrt::Windows::UI::Xaml;
@ -28,9 +27,6 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
Windows::Foundation::Collections::IObservableVector<Editor::Font> ProfileViewModel::_MonospaceFontList{ nullptr };
Windows::Foundation::Collections::IObservableVector<Editor::Font> ProfileViewModel::_FontList{ nullptr };
Windows::Foundation::Collections::IObservableVector<Editor::EnumEntry> ProfileViewModel::_BuiltInIcons{ nullptr };
static constexpr std::wstring_view HideIconValue{ L"none" };
ProfileViewModel::ProfileViewModel(const Model::Profile& profile, const Model::CascadiaSettings& appSettings, const Windows::UI::Core::CoreDispatcher& dispatcher) :
_profile{ profile },
@ -47,17 +43,6 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
_InitializeCurrentBellSounds();
// set up IconTypes
std::vector<IInspectable> iconTypes;
iconTypes.reserve(4);
iconTypes.emplace_back(make<EnumEntry>(RS_(L"Profile_IconTypeNone"), box_value(IconType::None)));
iconTypes.emplace_back(make<EnumEntry>(RS_(L"Profile_IconTypeFontIcon"), box_value(IconType::FontIcon)));
iconTypes.emplace_back(make<EnumEntry>(RS_(L"Profile_IconTypeEmoji"), box_value(IconType::Emoji)));
iconTypes.emplace_back(make<EnumEntry>(RS_(L"Profile_IconTypeImage"), box_value(IconType::Image)));
_IconTypes = winrt::single_threaded_vector<IInspectable>(std::move(iconTypes));
_DeduceCurrentIconType();
_DeduceCurrentBuiltInIcon();
// 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.
@ -92,32 +77,15 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
}
else if (viewModelProperty == L"Icon")
{
// _DeduceCurrentIconType() ends with a "CurrentIconType" notification
// so we don't need to call _UpdateIconPreview() here
_DeduceCurrentIconType();
// The icon changed; let's re-evaluate it with its new context.
_appSettings.ResolveMediaResources();
}
else if (viewModelProperty == L"CurrentIconType")
{
// "Using*" handles the visibility of the IconType-related UI.
// The others propagate the rendered icon into a preview (i.e. nav view, container item)
_NotifyChanges(L"UsingNoIcon",
L"UsingBuiltInIcon",
L"UsingEmojiIcon",
L"UsingImageIcon",
L"LocalizedIcon",
// Propagate the rendered icon into a preview (i.e. nav view, container item)
_NotifyChanges(L"LocalizedIcon",
L"IconPreview",
L"IconPath",
L"EvaluatedIcon");
}
else if (viewModelProperty == L"CurrentBuiltInIcon")
{
IconPath(unbox_value<hstring>(_CurrentBuiltInIcon.EnumValue()));
}
else if (viewModelProperty == L"CurrentEmojiIcon")
{
IconPath(CurrentEmojiIcon());
L"EvaluatedIcon",
L"UsingNoIcon");
}
else if (viewModelProperty == L"CurrentBellSounds")
{
@ -190,61 +158,6 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
_defaultAppearanceViewModel.IsDefault(true);
}
void ProfileViewModel::_UpdateBuiltInIcons()
{
std::vector<Editor::EnumEntry> builtInIcons;
for (auto& [val, name] : s_SegoeFluentIcons)
{
builtInIcons.emplace_back(make<EnumEntry>(hstring{ name }, box_value(val)));
}
_BuiltInIcons = single_threaded_observable_vector<Editor::EnumEntry>(std::move(builtInIcons));
}
void ProfileViewModel::_DeduceCurrentIconType()
{
const auto profileIcon = IconPath();
if (profileIcon == HideIconValue)
{
_currentIconType = _IconTypes.GetAt(0);
}
else if (profileIcon.size() == 1 && (L'\uE700' <= til::at(profileIcon, 0) && til::at(profileIcon, 0) <= L'\uF8B3'))
{
_currentIconType = _IconTypes.GetAt(1);
_DeduceCurrentBuiltInIcon();
}
else if (::Microsoft::Console::Utils::IsLikelyToBeEmojiOrSymbolIcon(profileIcon))
{
// We already did a range check for MDL2 Assets in the previous one,
// so if we're out of that range but still short, assume we're an emoji
_currentIconType = _IconTypes.GetAt(2);
}
else
{
_currentIconType = _IconTypes.GetAt(3);
}
_NotifyChanges(L"CurrentIconType");
}
void ProfileViewModel::_DeduceCurrentBuiltInIcon()
{
if (!_BuiltInIcons)
{
_UpdateBuiltInIcons();
}
const auto profileIcon = IconPath();
for (uint32_t i = 0; i < _BuiltInIcons.Size(); i++)
{
const auto& builtIn = _BuiltInIcons.GetAt(i);
if (profileIcon == unbox_value<hstring>(builtIn.EnumValue()))
{
_CurrentBuiltInIcon = builtIn;
return;
}
}
_CurrentBuiltInIcon = _BuiltInIcons.GetAt(0);
_NotifyChanges(L"CurrentBuiltInIcon");
}
void ProfileViewModel::LeftPadding(double value) noexcept
{
if (std::abs(_parsedPadding.Left - value) >= .0001)
@ -636,9 +549,9 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
winrt::hstring ProfileViewModel::LocalizedIcon() const
{
if (_currentIconType && unbox_value<IconType>(_currentIconType.as<Editor::EnumEntry>().EnumValue()) == IconType::None)
if (UsingNoIcon())
{
return RS_(L"Profile_IconTypeNone");
return RS_(L"IconPicker_IconTypeNone");
}
return IconPath(); // For display as a string
}
@ -652,83 +565,10 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
return icon;
}
void ProfileViewModel::CurrentIconType(const Windows::Foundation::IInspectable& value)
bool ProfileViewModel::UsingNoIcon() const noexcept
{
if (_currentIconType != value)
{
// Switching from...
if (_currentIconType && unbox_value<IconType>(_currentIconType.as<Editor::EnumEntry>().EnumValue()) == IconType::Image)
{
// Stash the current value of Icon. If the user
// switches out of then back to IconType::Image, we want
// the path that we display in the text box to remain unchanged.
_lastIconPath = IconPath();
}
// Set the member here instead of after setting Icon() below!
// We have an Icon property changed handler defined for when we discard changes.
// Inadvertently, that means that we call this setter again.
// Setting the member here means that we early exit at the beginning of the function
// because _currentIconType == value.
_currentIconType = value;
// Switched to...
switch (unbox_value<IconType>(value.as<Editor::EnumEntry>().EnumValue()))
{
case IconType::None:
{
IconPath(winrt::hstring{ HideIconValue });
break;
}
case IconType::Image:
{
if (!_lastIconPath.empty())
{
// Conversely, if we switch to Image,
// retrieve that saved value and apply it
IconPath(_lastIconPath);
}
break;
}
case IconType::FontIcon:
{
if (_CurrentBuiltInIcon)
{
IconPath(unbox_value<hstring>(_CurrentBuiltInIcon.EnumValue()));
}
break;
}
case IconType::Emoji:
{
// Don't set Icon here!
// Clear out the text box so we direct the user to use the emoji picker.
CurrentEmojiIcon({});
}
}
// We're not using the VM's Icon() setter above,
// so notify HasIcon changed manually
_NotifyChanges(L"CurrentIconType", L"HasIcon");
}
}
bool ProfileViewModel::UsingNoIcon() const
{
return _currentIconType == _IconTypes.GetAt(0);
}
bool ProfileViewModel::UsingBuiltInIcon() const
{
return _currentIconType == _IconTypes.GetAt(1);
}
bool ProfileViewModel::UsingEmojiIcon() const
{
return _currentIconType == _IconTypes.GetAt(2);
}
bool ProfileViewModel::UsingImageIcon() const
{
return _currentIconType == _IconTypes.GetAt(3);
const auto iconPath{ IconPath() };
return iconPath.empty() || iconPath == IconPicker::HideIconValue;
}
hstring ProfileViewModel::BellStylePreview() const

View File

@ -49,7 +49,6 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
static void UpdateFontList() noexcept;
static Windows::Foundation::Collections::IObservableVector<Editor::Font> CompleteFontList() noexcept { return _FontList; };
static Windows::Foundation::Collections::IObservableVector<Editor::Font> MonospaceFontList() noexcept { return _MonospaceFontList; };
static Windows::Foundation::Collections::IObservableVector<Editor::EnumEntry> BuiltInIcons() noexcept { return _BuiltInIcons; };
ProfileViewModel(const Model::Profile& profile, const Model::CascadiaSettings& settings, const Windows::UI::Core::CoreDispatcher& dispatcher);
Control::IControlSettings TermSettings() const;
@ -86,23 +85,15 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
return _profile.Icon().Resolved();
}
Windows::Foundation::IInspectable CurrentIconType() const noexcept
{
return _currentIconType;
}
Windows::UI::Xaml::Controls::IconElement IconPreview() const;
winrt::hstring LocalizedIcon() const;
void CurrentIconType(const Windows::Foundation::IInspectable& value);
bool UsingNoIcon() const;
bool UsingBuiltInIcon() const;
bool UsingEmojiIcon() const;
bool UsingImageIcon() const;
winrt::hstring IconPath() const { return _profile.Icon().Path(); }
void IconPath(const winrt::hstring& path)
{
Icon(Model::MediaResourceHelper::FromString(path));
_NotifyChanges(L"Icon", L"IconPath");
}
bool UsingNoIcon() const noexcept;
// starting directory
hstring CurrentStartingDirectoryPreview() const;
@ -134,8 +125,6 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
VIEW_MODEL_OBSERVABLE_PROPERTY(ProfileSubPage, CurrentPage);
VIEW_MODEL_OBSERVABLE_PROPERTY(Windows::Foundation::Collections::IObservableVector<Editor::BellSoundViewModel>, CurrentBellSounds);
VIEW_MODEL_OBSERVABLE_PROPERTY(Editor::EnumEntry, CurrentBuiltInIcon, nullptr);
VIEW_MODEL_OBSERVABLE_PROPERTY(hstring, CurrentEmojiIcon);
PERMANENT_OBSERVABLE_PROJECTED_SETTING(_profile, Guid);
PERMANENT_OBSERVABLE_PROJECTED_SETTING(_profile, ConnectionType);
@ -174,7 +163,6 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
WINRT_PROPERTY(bool, IsBaseLayer, false);
WINRT_PROPERTY(bool, FocusDeleteButton, false);
WINRT_PROPERTY(Windows::Foundation::Collections::IVector<Windows::Foundation::IInspectable>, IconTypes);
GETSET_BINDABLE_ENUM_SETTING(AntiAliasingMode, Microsoft::Terminal::Control::TextAntialiasingMode, AntialiasingMode);
GETSET_BINDABLE_ENUM_SETTING(CloseOnExitMode, Microsoft::Terminal::Settings::Model::CloseOnExitMode, CloseOnExit);
GETSET_BINDABLE_ENUM_SETTING(ScrollState, Microsoft::Terminal::Control::ScrollbarState, ScrollState);
@ -185,8 +173,6 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
winrt::guid _originalProfileGuid{};
winrt::hstring _lastBgImagePath;
winrt::hstring _lastStartingDirectoryPath;
winrt::hstring _lastIconPath;
Windows::Foundation::IInspectable _currentIconType{};
Editor::AppearanceViewModel _defaultAppearanceViewModel;
Windows::UI::Core::CoreDispatcher _dispatcher;
@ -197,13 +183,9 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
void _MarkDuplicateBellSoundDirectories();
static Windows::Foundation::Collections::IObservableVector<Editor::Font> _MonospaceFontList;
static Windows::Foundation::Collections::IObservableVector<Editor::Font> _FontList;
static Windows::Foundation::Collections::IObservableVector<Editor::EnumEntry> _BuiltInIcons;
Model::CascadiaSettings _appSettings;
Editor::AppearanceViewModel _unfocusedAppearanceViewModel;
void _UpdateBuiltInIcons();
void _DeduceCurrentIconType();
void _DeduceCurrentBuiltInIcon();
};
struct DeleteProfileEventArgs :

View File

@ -42,14 +42,6 @@ namespace Microsoft.Terminal.Settings.Editor
Advanced = 3
};
enum IconType
{
None = 0,
FontIcon,
Image,
Emoji
};
runtimeclass ProfileViewModel : Windows.UI.Xaml.Data.INotifyPropertyChanged
{
event Windows.Foundation.TypedEventHandler<ProfileViewModel, DeleteProfileEventArgs> DeleteProfileRequested;
@ -107,17 +99,8 @@ namespace Microsoft.Terminal.Settings.Editor
Windows.UI.Xaml.Controls.IconElement IconPreview { get; };
String EvaluatedIcon { get; };
String LocalizedIcon { get; };
String CurrentEmojiIcon;
IInspectable CurrentIconType;
Windows.Foundation.Collections.IVector<IInspectable> IconTypes { get; };
Boolean UsingNoIcon { get; };
Boolean UsingBuiltInIcon { get; };
Boolean UsingEmojiIcon { get; };
Boolean UsingImageIcon { get; };
String IconPath;
EnumEntry CurrentBuiltInIcon;
Windows.Foundation.Collections.IObservableVector<EnumEntry> BuiltInIcons { get; };
Boolean UsingNoIcon { get; };
String TabTitlePreview { get; };
String AnswerbackMessagePreview { get; };

View File

@ -133,18 +133,6 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
}
}
safe_void_coroutine Profiles_Base::Icon_Click(const IInspectable&, const RoutedEventArgs&)
{
auto lifetime = get_strong();
const auto parentHwnd{ reinterpret_cast<HWND>(_windowRoot.GetHostingWindow()) };
auto file = co_await OpenImagePicker(parentHwnd);
if (!file.empty())
{
_Profile.IconPath(file);
}
}
safe_void_coroutine Profiles_Base::StartingDirectory_Click(const IInspectable&, const RoutedEventArgs&)
{
auto lifetime = get_strong();
@ -169,77 +157,4 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
_Profile.StartingDirectory(folder);
}
}
IconSource Profiles_Base::BuiltInIconConverter(const IInspectable& iconVal)
{
return Microsoft::Terminal::UI::IconPathConverter::IconSourceWUX(unbox_value<hstring>(iconVal));
}
void Profiles_Base::BuiltInIconPicker_GotFocus(const IInspectable& sender, const RoutedEventArgs& /*e*/)
{
_updateIconFilter({});
sender.as<AutoSuggestBox>().IsSuggestionListOpen(true);
}
void Profiles_Base::BuiltInIconPicker_QuerySubmitted(const AutoSuggestBox& /*sender*/, const AutoSuggestBoxQuerySubmittedEventArgs& e)
{
const auto iconEntry = unbox_value_or<EnumEntry>(e.ChosenSuggestion(), nullptr);
if (!iconEntry)
{
return;
}
_Profile.CurrentBuiltInIcon(iconEntry);
}
void Profiles_Base::BuiltInIconPicker_TextChanged(const AutoSuggestBox& sender, const AutoSuggestBoxTextChangedEventArgs& e)
{
if (e.Reason() != AutoSuggestionBoxTextChangeReason::UserInput)
{
return;
}
std::wstring_view filter{ sender.Text() };
filter = til::trim(filter, L' ');
_updateIconFilter(filter);
}
void Profiles_Base::_updateIconFilter(std::wstring_view filter)
{
if (_iconFilter != filter)
{
_filteredBuiltInIcons = nullptr;
_iconFilter = filter;
_updateFilteredIconList();
PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"FilteredBuiltInIconList" });
}
}
Windows::Foundation::Collections::IObservableVector<Editor::EnumEntry> Profiles_Base::FilteredBuiltInIconList()
{
if (!_filteredBuiltInIcons)
{
_updateFilteredIconList();
}
return _filteredBuiltInIcons;
}
void Profiles_Base::_updateFilteredIconList()
{
_filteredBuiltInIcons = ProfileViewModel::BuiltInIcons();
if (_iconFilter.empty())
{
return;
}
// Find matching icons and populate the filtered list
std::vector<Editor::EnumEntry> filtered;
filtered.reserve(_filteredBuiltInIcons.Size());
for (const auto& icon : _filteredBuiltInIcons)
{
if (til::contains_linguistic_insensitive(icon.EnumName(), _iconFilter))
{
filtered.emplace_back(icon);
}
}
_filteredBuiltInIcons = winrt::single_threaded_observable_vector(std::move(filtered));
}
}

View File

@ -18,32 +18,20 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
void OnNavigatedFrom(const Windows::UI::Xaml::Navigation::NavigationEventArgs& e);
safe_void_coroutine StartingDirectory_Click(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& e);
safe_void_coroutine Icon_Click(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& e);
safe_void_coroutine Commandline_Click(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& e);
void Appearance_Click(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& e);
void Terminal_Click(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& e);
void Advanced_Click(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& e);
void DeleteConfirmation_Click(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& e);
Windows::Foundation::Collections::IObservableVector<Editor::EnumEntry> FilteredBuiltInIconList();
void BuiltInIconPicker_GotFocus(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& e);
void BuiltInIconPicker_TextChanged(const winrt::Windows::UI::Xaml::Controls::AutoSuggestBox& sender, const Windows::UI::Xaml::Controls::AutoSuggestBoxTextChangedEventArgs& e);
void BuiltInIconPicker_QuerySubmitted(const winrt::Windows::UI::Xaml::Controls::AutoSuggestBox& sender, const Windows::UI::Xaml::Controls::AutoSuggestBoxQuerySubmittedEventArgs& e);
static Windows::UI::Xaml::Controls::IconSource BuiltInIconConverter(const Windows::Foundation::IInspectable& iconVal);
til::property_changed_event PropertyChanged;
Editor::IHostedInWindow WindowRoot() const noexcept { return _windowRoot; }
WINRT_PROPERTY(Editor::ProfileViewModel, Profile, nullptr);
private:
Windows::UI::Xaml::Data::INotifyPropertyChanged::PropertyChanged_revoker _ViewModelChangedRevoker;
winrt::Windows::UI::Xaml::Controls::SwapChainPanel::LayoutUpdated_revoker _layoutUpdatedRevoker;
Editor::IHostedInWindow _windowRoot;
Windows::Foundation::Collections::IObservableVector<Editor::EnumEntry> _filteredBuiltInIcons;
std::wstring _iconFilter;
void _updateIconFilter(std::wstring_view filter);
void _updateFilteredIconList();
};
};

View File

@ -2,6 +2,7 @@
// Licensed under the MIT license.
import "ProfileViewModel.idl";
import "MainPage.idl";
namespace Microsoft.Terminal.Settings.Editor
{
@ -9,8 +10,6 @@ namespace Microsoft.Terminal.Settings.Editor
{
Profiles_Base();
ProfileViewModel Profile { get; };
Windows.Foundation.Collections.IObservableVector<EnumEntry> FilteredBuiltInIconList { get; };
static Windows.UI.Xaml.Controls.IconSource BuiltInIconConverter(IInspectable iconVal);
IHostedInWindow WindowRoot { get; };
}
}

View File

@ -122,82 +122,8 @@
</Grid>
</local:SettingContainer.CurrentValue>
<local:SettingContainer.Content>
<Grid ColumnSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="auto" />
</Grid.ColumnDefinitions>
<!-- Icon Type -->
<ComboBox x:Uid="Profile_IconType"
Grid.Column="0"
ItemsSource="{x:Bind Profile.IconTypes}"
SelectedItem="{x:Bind Profile.CurrentIconType, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="local:EnumEntry">
<TextBlock Text="{x:Bind EnumName}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<!-- Built-In Icon -->
<AutoSuggestBox x:Uid="Profile_BuiltInIcon"
Grid.Column="1"
GotFocus="BuiltInIconPicker_GotFocus"
ItemsSource="{x:Bind FilteredBuiltInIconList, Mode=OneWay}"
QuerySubmitted="BuiltInIconPicker_QuerySubmitted"
Text="{x:Bind Profile.CurrentBuiltInIcon.EnumName, Mode=OneWay}"
TextBoxStyle="{StaticResource TextBoxSettingStyle}"
TextChanged="BuiltInIconPicker_TextChanged"
Visibility="{x:Bind Profile.UsingBuiltInIcon, Mode=OneWay}">
<AutoSuggestBox.ItemTemplate>
<DataTemplate x:DataType="local:EnumEntry">
<Grid ColumnSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<IconSourceElement Grid.Column="0"
Width="16"
Height="16"
IconSource="{x:Bind local:Profiles_Base.BuiltInIconConverter(EnumValue), Mode=OneTime}" />
<TextBlock Grid.Column="1"
Text="{x:Bind EnumName}" />
</Grid>
</DataTemplate>
</AutoSuggestBox.ItemTemplate>
</AutoSuggestBox>
<!-- Image (File) Icon -->
<TextBox x:Uid="Profile_IconBox"
Grid.Column="1"
MaxWidth="Infinity"
HorizontalAlignment="Stretch"
FontFamily="Segoe UI, Segoe Fluent Icons, Segoe MDL2 Assets"
IsSpellCheckEnabled="False"
Style="{StaticResource TextBoxSettingStyle}"
Text="{x:Bind Profile.IconPath, Mode=TwoWay}"
Visibility="{x:Bind Profile.UsingImageIcon, Mode=OneWay}" />
<Button x:Uid="Profile_IconBrowse"
Grid.Column="2"
Margin="0"
VerticalAlignment="Top"
Click="Icon_Click"
Style="{StaticResource BrowseButtonStyle}"
Visibility="{x:Bind Profile.UsingImageIcon, Mode=OneWay}" />
<!-- Emoji Icon -->
<TextBox x:Uid="Profile_IconEmojiBox"
Grid.Column="1"
MaxWidth="Infinity"
FontFamily="Segoe UI, Segoe Fluent Icons, Segoe MDL2 Assets"
IsSpellCheckEnabled="False"
Style="{StaticResource TextBoxSettingStyle}"
Text="{x:Bind Profile.CurrentEmojiIcon, Mode=TwoWay}"
Visibility="{x:Bind Profile.UsingEmojiIcon, Mode=OneWay}" />
</Grid>
<local:IconPicker CurrentIconPath="{x:Bind Profile.IconPath, Mode=TwoWay}"
WindowRoot="{x:Bind WindowRoot, Mode=OneWay}" />
</local:SettingContainer.Content>
</local:SettingContainer>

View File

@ -1110,11 +1110,11 @@
<value>Icon</value>
<comment>Header for a control to determine what icon can be used to represent this profile. This is not necessarily a file path, but can be one.</comment>
</data>
<data name="Profile_IconBox.[using:Windows.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<data name="IconPicker_ImagePathBox.[using:Windows.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Icon</value>
<comment>Name for a control to determine what icon can be used to represent this profile. This is not necessarily a file path, but can be one.</comment>
<comment>Name for a control to determine what icon can be used. This is not necessarily a file path, but can be one. It's usually used for images.</comment>
</data>
<data name="Profile_IconEmojiBox.[using:Windows.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<data name="IconPicker_EmojiBox.[using:Windows.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Icon</value>
<comment>Name for a control to determine what icon can be used to represent this profile.</comment>
</data>
@ -1122,7 +1122,7 @@
<value>Emoji or image file location of the icon used in the profile.</value>
<comment>A description for what the "icon" setting does. Presented near "Profile_Icon".</comment>
</data>
<data name="Profile_IconBrowse.Content" xml:space="preserve">
<data name="IconPicker_IconBrowse.Content" xml:space="preserve">
<value>Browse...</value>
<comment>Button label that opens a file picker in a new window. The "..." is standard to mean it will open a new window.</comment>
</data>
@ -2318,31 +2318,31 @@
<value>Use theme color</value>
<comment>Label for a button directing the user to use the tab color defined in the terminal's current theme.</comment>
</data>
<data name="Profile_IconTypeNone" xml:space="preserve">
<data name="IconPicker_IconTypeNone" xml:space="preserve">
<value>None</value>
<comment>An option to choose from for the "icon style" dropdown. When selected, there will be no icon for the profile.</comment>
<comment>An option to choose from for the "icon style" dropdown. When selected, there will be no icon set.</comment>
</data>
<data name="Profile_IconTypeImage" xml:space="preserve">
<data name="IconPicker_IconTypeImage" xml:space="preserve">
<value>File</value>
<comment>An option to choose from for the "icon style" dropdown. When selected, a custom image can set for the profile's icon.</comment>
<comment>An option to choose from for the "icon style" dropdown. When selected, a custom image can set as the icon.</comment>
</data>
<data name="Profile_IconTypeEmoji" xml:space="preserve">
<data name="IconPicker_IconTypeEmoji" xml:space="preserve">
<value>Emoji</value>
<comment>An option to choose from for the "icon style" dropdown. When selected, an emoji can be set for the profile's icon.</comment>
<comment>An option to choose from for the "icon style" dropdown. When selected, an emoji can be set as the icon.</comment>
</data>
<data name="Profile_IconTypeFontIcon" xml:space="preserve">
<data name="IconPicker_IconTypeFontIcon" xml:space="preserve">
<value>Built-in Icon</value>
<comment>An option to choose from for the "icon style" dropdown. When selected, the user can choose from several preselected options to set the profile's icon.</comment>
<comment>An option to choose from for the "icon style" dropdown. When selected, the user can choose from several preselected options to set as the icon.</comment>
</data>
<data name="Profile_IconEmojiBox.PlaceholderText" xml:space="preserve">
<data name="IconPicker_EmojiBox.PlaceholderText" xml:space="preserve">
<value>Use "Win + period" to open the emoji picker</value>
<comment>"Win + period" refers to the OS key binding to open the emoji picker.</comment>
</data>
<data name="Profile_IconType.[using:Windows.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<data name="IconPicker_IconType.[using:Windows.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Icon type</value>
<comment>Accessible name for a control allowing the user to select the type of icon they would like to use.</comment>
</data>
<data name="Profile_BuiltInIcon.[using:Windows.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<data name="IconPicker_BuiltInIcon.[using:Windows.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Icon</value>
<comment>Accessible name for a control allowing the user to select the icon from a list of built in icons.</comment>
</data>
@ -2446,6 +2446,14 @@
<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_CurrentFolderIcon.Header" xml:space="preserve">
<value>Folder Icon</value>
<comment>Header for a control that allows the user to modify the icon of the current folder entry.</comment>
</data>
<data name="NewTabMenu_CurrentFolderIcon.[using:Windows.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Folder Icon</value>
<comment>Name for a control to that allows the user to modify the icon 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>
@ -2701,4 +2709,8 @@
<data name="Settings_ResetApplicationStateConfirmationButton.Content" xml:space="preserve">
<value>Yes, clear the cache</value>
</data>
</root>
<data name="IconPicker_BuiltInIcon.PlaceholderText" xml:space="preserve">
<value>Type to filter icons</value>
<comment>Placeholder text for a text box to filter and select an icon.</comment>
</data>
</root>