diff --git a/src/cascadia/TerminalSettingsEditor/IconPicker.cpp b/src/cascadia/TerminalSettingsEditor/IconPicker.cpp new file mode 100644 index 00000000000..cc40e636c4d --- /dev/null +++ b/src/cascadia/TerminalSettingsEditor/IconPicker.cpp @@ -0,0 +1,330 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "IconPicker.h" +#include "IconPicker.g.cpp" + +#include +#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 IconPicker::_BuiltInIcons{ nullptr }; + Windows::Foundation::Collections::IObservableVector IconPicker::_IconTypes{ nullptr }; + DependencyProperty IconPicker::_CurrentIconPathProperty{ nullptr }; + DependencyProperty IconPicker::_WindowRootProperty{ nullptr }; + + Windows::Foundation::Collections::IObservableVector IconPicker::BuiltInIcons() noexcept + { + if (!_BuiltInIcons) + { + // lazy load the built-in icons + std::vector builtInIcons; + for (auto& [val, name] : s_SegoeFluentIcons) + { + builtInIcons.emplace_back(make(hstring{ name }, box_value(val))); + } + _BuiltInIcons = single_threaded_observable_vector(std::move(builtInIcons)); + } + return _BuiltInIcons; + } + + Windows::Foundation::Collections::IObservableVector IconPicker::IconTypes() noexcept + { + if (!_IconTypes) + { + // lazy load the icon types + std::vector iconTypes; + iconTypes.reserve(4); + iconTypes.emplace_back(make(RS_(L"IconPicker_IconTypeNone"), box_value(IconType::None))); + iconTypes.emplace_back(make(RS_(L"IconPicker_IconTypeFontIcon"), box_value(IconType::FontIcon))); + iconTypes.emplace_back(make(RS_(L"IconPicker_IconTypeEmoji"), box_value(IconType::Emoji))); + iconTypes.emplace_back(make(RS_(L"IconPicker_IconTypeImage"), box_value(IconType::Image))); + _IconTypes = winrt::single_threaded_observable_vector(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(_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(), + xaml_typename(), + PropertyMetadata{ nullptr, PropertyChangedCallback{ &IconPicker::_OnCurrentIconPathChanged } }); + } + if (!_WindowRootProperty) + { + _WindowRootProperty = + DependencyProperty::Register( + L"WindowRoot", + xaml_typename(), + xaml_typename(), + PropertyMetadata{ nullptr }); + } + } + + void IconPicker::_OnCurrentIconPathChanged(const DependencyObject& d, const DependencyPropertyChangedEventArgs& /*e*/) + { + d.as()->_DeduceCurrentIconType(); + } + + safe_void_coroutine IconPicker::Icon_Click(const IInspectable&, const RoutedEventArgs&) + { + auto lifetime = get_strong(); + + const auto parentHwnd{ reinterpret_cast(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().IsSuggestionListOpen(true); + } + + void IconPicker::BuiltInIconPicker_QuerySubmitted(const AutoSuggestBox& /*sender*/, const AutoSuggestBoxQuerySubmittedEventArgs& e) + { + const auto iconEntry = unbox_value_or(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 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 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(_currentIconType.as().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(value.as().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(_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(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(iconVal)); + } +} diff --git a/src/cascadia/TerminalSettingsEditor/IconPicker.h b/src/cascadia/TerminalSettingsEditor/IconPicker.h new file mode 100644 index 00000000000..353b3c3db8f --- /dev/null +++ b/src/cascadia/TerminalSettingsEditor/IconPicker.h @@ -0,0 +1,64 @@ +// 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, IconPickerT + { + public: + IconPicker(); + + static Windows::UI::Xaml::Controls::IconSource BuiltInIconConverter(const Windows::Foundation::IInspectable& iconVal); + static Windows::Foundation::Collections::IObservableVector BuiltInIcons() noexcept; + static Windows::Foundation::Collections::IObservableVector IconTypes() noexcept; + + Windows::Foundation::Collections::IObservableVector 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 _BuiltInIcons; + static Windows::Foundation::Collections::IObservableVector _IconTypes; + Windows::Foundation::Collections::IObservableVector _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); +} diff --git a/src/cascadia/TerminalSettingsEditor/IconPicker.idl b/src/cascadia/TerminalSettingsEditor/IconPicker.idl new file mode 100644 index 00000000000..35cc126cea2 --- /dev/null +++ b/src/cascadia/TerminalSettingsEditor/IconPicker.idl @@ -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 IconTypes { get; }; + + Boolean UsingBuiltInIcon { get; }; + Boolean UsingEmojiIcon { get; }; + Boolean UsingImageIcon { get; }; + + String CurrentEmojiIcon; + + EnumEntry CurrentBuiltInIcon; + Windows.Foundation.Collections.IObservableVector BuiltInIcons { get; }; + Windows.Foundation.Collections.IObservableVector 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; }; + } +} diff --git a/src/cascadia/TerminalSettingsEditor/IconPicker.xaml b/src/cascadia/TerminalSettingsEditor/IconPicker.xaml new file mode 100644 index 00000000000..1e585cd823d --- /dev/null +++ b/src/cascadia/TerminalSettingsEditor/IconPicker.xaml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +