From ba1eab148c741b606b788a68d0d3032f70264fde Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Fri, 21 Aug 2020 12:01:32 -0500 Subject: [PATCH] add support for using profile icons, symbols, emoji, as icons in the cmdpal --- src/cascadia/TerminalApp/Command.cpp | 83 +++++++++++++++++++- src/cascadia/TerminalApp/Command.h | 4 + src/cascadia/TerminalApp/Command.idl | 1 + src/cascadia/TerminalApp/CommandPalette.xaml | 34 ++++---- src/cascadia/TerminalApp/TerminalPage.cpp | 18 ++--- 5 files changed, 109 insertions(+), 31 deletions(-) diff --git a/src/cascadia/TerminalApp/Command.cpp b/src/cascadia/TerminalApp/Command.cpp index b8b8540ef33..1eeadc21494 100644 --- a/src/cascadia/TerminalApp/Command.cpp +++ b/src/cascadia/TerminalApp/Command.cpp @@ -15,14 +15,21 @@ using namespace winrt::TerminalApp; using namespace winrt::Windows::Foundation; using namespace ::TerminalApp; +namespace winrt +{ + namespace MUX = Microsoft::UI::Xaml; + namespace WUX = Windows::UI::Xaml; +} + static constexpr std::string_view NameKey{ "name" }; -static constexpr std::string_view IconPathKey{ "iconPath" }; +static constexpr std::string_view IconKey{ "icon" }; static constexpr std::string_view ActionKey{ "command" }; static constexpr std::string_view ArgsKey{ "args" }; static constexpr std::string_view IterateOnKey{ "iterateOn" }; static constexpr std::string_view CommandsKey{ "commands" }; static constexpr std::string_view ProfileNameToken{ "${profile.name}" }; +static constexpr std::string_view ProfileIconToken{ "${profile.icon}" }; static constexpr std::string_view SchemeNameToken{ "${scheme.name}" }; namespace winrt::TerminalApp::implementation @@ -100,6 +107,70 @@ namespace winrt::TerminalApp::implementation return actionAndArgs->GenerateName(); } + // Method Description: + // - Actually initialize our IconSource for our _lastIconPath. Supports a variety of icons: + // * If the icon is a path to an image, we'll use that. + // * If it isn't, then we'll try and use the text as a FontIcon. If the + // character is in the range of symbols reserved for the Segoe MDL2 + // Asserts, well treat it as such. Otherwise, we'll default to a Sego + // UI icon, so things like emoji will work. + // - MUST BE CALLED ON THE UI THREAD. + // Arguments: + // - + // Return Value: + // - + void Command::RefreshIcon() + { + if (!_lastIconPath.empty()) + { + _setIconSource(GetColoredIcon(_lastIconPath)); + + // If we fail to set the icon source using the "icon" as a path, + // let's try it as a symbol/emoji. + // + // Anything longer that 2 wchar_t's _isn't_ an emoji or symbol, so + // don't do this if it's just an invalid path. + if (IconSource() == nullptr && _lastIconPath.size() <= 2) + { + try + { + WUX::Controls::FontIconSource icon; + const wchar_t ch = _lastIconPath[0]; + + // The range of MDL2 Icons isn't explicitly defined, but + // we're using this based off the table on: + // https://docs.microsoft.com/en-us/windows/uwp/design/style/segoe-ui-symbol-font + const bool isMDL2Icon = ch >= L'\uE700' && ch <= L'\uF8FF'; + if (isMDL2Icon) + { + icon.FontFamily(WUX::Media::FontFamily{ L"Segoe MDL2 Assets" }); + } + else + { + // Note: you _do_ need to manually set the font here. + icon.FontFamily(WUX::Media::FontFamily{ L"Segoe UI" }); + } + icon.FontSize(12); + icon.Glyph(_lastIconPath); + _setIconSource(icon); + } + CATCH_LOG(); + } + } + if (IconSource() == nullptr) + { + // Set the default IconSource to a BitmapIconSource with a null source + // (instead of just nullptr) because there's a really weird crash when swapping + // data bound IconSourceElements in a ListViewTemplate (i.e. CommandPalette). + // Swapping between nullptr IconSources and non-null IconSources causes a crash + // to occur, but swapping between IconSources with a null source and non-null IconSources + // work perfectly fine :shrug:. + winrt::Windows::UI::Xaml::Controls::BitmapIconSource icon; + icon.UriSource(nullptr); + _setIconSource(icon); + } + } + // Method Description: // - Deserialize a Command from the `json` object. The json object should // contain a "name" and "action", and optionally an "icon". @@ -143,9 +214,9 @@ namespace winrt::TerminalApp::implementation return nullptr; } - // TODO GH#6644: iconPath not implemented quite yet. Can't seem to get - // the binding quite right. Additionally, do we want it to be an image, - // or a FontIcon? I've had difficulty binding either/or. + // Only get the icon path right now. The icon needs to be resolved into + // an IconSource on the UI thread, which will be done by RefreshIcon. + JsonUtils::GetValueForKey(json, IconKey, result->_lastIconPath); // If we're a nested command, we can ignore the current action. if (!nested) @@ -396,9 +467,13 @@ namespace winrt::TerminalApp::implementation // - Escape the profile name for JSON appropriately auto escapedProfileName = _escapeForJson(til::u16u8(p.GetName())); + auto escapedProfileIcon = _escapeForJson(til::u16u8(p.GetExpandedIconPath())); auto newJsonString = til::replace_needle_in_haystack(oldJsonString, ProfileNameToken, escapedProfileName); + til::replace_needle_in_haystack_inplace(newJsonString, + ProfileIconToken, + escapedProfileIcon); // If we encounter a re-parsing error, just stop processing the rest of the commands. if (!reParseJson(newJsonString)) diff --git a/src/cascadia/TerminalApp/Command.h b/src/cascadia/TerminalApp/Command.h index 1b14ba8031d..e7aa339a41c 100644 --- a/src/cascadia/TerminalApp/Command.h +++ b/src/cascadia/TerminalApp/Command.h @@ -50,6 +50,8 @@ namespace winrt::TerminalApp::implementation bool HasNestedCommands(); Windows::Foundation::Collections::IMapView NestedCommands(); + void RefreshIcon(); + winrt::Windows::UI::Xaml::Data::INotifyPropertyChanged::PropertyChanged_revoker propertyChangedRevoker; WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler); @@ -64,6 +66,8 @@ namespace winrt::TerminalApp::implementation Json::Value _originalJson; Windows::Foundation::Collections::IMap _subcommands{ nullptr }; + winrt::hstring _lastIconPath{}; + static std::vector _expandCommand(Command* const expandable, gsl::span profiles, gsl::span schemes, diff --git a/src/cascadia/TerminalApp/Command.idl b/src/cascadia/TerminalApp/Command.idl index eea4c9bac73..d60287890b5 100644 --- a/src/cascadia/TerminalApp/Command.idl +++ b/src/cascadia/TerminalApp/Command.idl @@ -14,6 +14,7 @@ namespace TerminalApp String KeyChordText; Windows.UI.Xaml.Controls.IconSource IconSource; + void RefreshIcon(); Boolean HasNestedCommands { get; }; Windows.Foundation.Collections.IMapView NestedCommands { get; }; diff --git a/src/cascadia/TerminalApp/CommandPalette.xaml b/src/cascadia/TerminalApp/CommandPalette.xaml index 015ce7677d6..a7ce2822c6f 100644 --- a/src/cascadia/TerminalApp/CommandPalette.xaml +++ b/src/cascadia/TerminalApp/CommandPalette.xaml @@ -208,7 +208,7 @@ the MIT License. See LICENSE in the project root for license information. --> + to make sure it takes the entire width of the line --> @@ -223,30 +223,32 @@ the MIT License. See LICENSE in the project root for license information. --> + Grid.Column="0" + Width="16" + Height="16" + IconSource="{x:Bind IconSource, Mode=OneWay}"/> + when there's actual text set as the label. See + CommandKeyChordVisibilityConverter for details. --> + Grid.Column="2" + Visibility="{x:Bind KeyChordText, + Mode=OneWay, + Converter={StaticResource CommandKeyChordVisibilityConverter}}" + Style="{ThemeResource KeyChordBorderStyle}" + Padding="2,0,2,0" + HorizontalAlignment="Right" + VerticalAlignment="Center"> + Style="{ThemeResource KeyChordTextBlockStyle}" + FontSize="12" + Text="{x:Bind KeyChordText, Mode=OneWay}" /> diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 02d95dee2b5..24f9504bb66 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -60,7 +60,7 @@ namespace winrt::TerminalApp::implementation // - settings: The settings who's keybindings we should use to look up the key chords from // - commands: The list of commands to label. static void _recursiveUpdateCommandKeybindingLabels(std::shared_ptr<::TerminalApp::CascadiaSettings> settings, - Windows::Foundation::Collections::IMapView commands) + IMapView commands) { for (const auto& nameAndCmd : commands) { @@ -83,20 +83,16 @@ namespace winrt::TerminalApp::implementation } } - static void _recursiveUpdateCommandIcons(Windows::Foundation::Collections::IMapView commands) + static void _recursiveUpdateCommandIcons(IMapView commands) { for (const auto& nameAndCmd : commands) { const auto& command = nameAndCmd.Value(); - // Set the default IconSource to a BitmapIconSource with a null source - // (instead of just nullptr) because there's a really weird crash when swapping - // data bound IconSourceElements in a ListViewTemplate (i.e. CommandPalette). - // Swapping between nullptr IconSources and non-null IconSources causes a crash - // to occur, but swapping between IconSources with a null source and non-null IconSources - // work perfectly fine :shrug:. - winrt::Windows::UI::Xaml::Controls::BitmapIconSource icon; - icon.UriSource(nullptr); - command.IconSource(icon); + + // !!! LOAD-BEARING !!! If this is never called, then Commands will + // have a nullptr icon. If they do, a really weird crash can occur. + // MAKE SURE this is called once after a settings load. + command.RefreshIcon(); if (command.HasNestedCommands()) {