Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add icons to commands in the Command Palette #7368

Merged
1 commit merged into from
Aug 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 79 additions & 4 deletions src/cascadia/TerminalApp/Command.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
// - <none>
// Return Value:
// - <none>
void Command::RefreshIcon()
{
if (!_lastIconPath.empty())
{
_setIconSource(GetColoredIcon<winrt::WUX::Controls::IconSource>(_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".
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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))
Expand Down
4 changes: 4 additions & 0 deletions src/cascadia/TerminalApp/Command.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ namespace winrt::TerminalApp::implementation
bool HasNestedCommands();
Windows::Foundation::Collections::IMapView<winrt::hstring, TerminalApp::Command> NestedCommands();

void RefreshIcon();

winrt::Windows::UI::Xaml::Data::INotifyPropertyChanged::PropertyChanged_revoker propertyChangedRevoker;

WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler);
Expand All @@ -64,6 +66,8 @@ namespace winrt::TerminalApp::implementation
Json::Value _originalJson;
Windows::Foundation::Collections::IMap<winrt::hstring, winrt::TerminalApp::Command> _subcommands{ nullptr };

winrt::hstring _lastIconPath{};

static std::vector<winrt::TerminalApp::Command> _expandCommand(Command* const expandable,
gsl::span<const ::TerminalApp::Profile> profiles,
gsl::span<winrt::TerminalApp::ColorScheme> schemes,
Expand Down
1 change: 1 addition & 0 deletions src/cascadia/TerminalApp/Command.idl
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ namespace TerminalApp
String KeyChordText;

Windows.UI.Xaml.Controls.IconSource IconSource;
void RefreshIcon();

Boolean HasNestedCommands { get; };
Windows.Foundation.Collections.IMapView<String, Command> NestedCommands { get; };
Expand Down
34 changes: 18 additions & 16 deletions src/cascadia/TerminalApp/CommandPalette.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ the MIT License. See LICENSE in the project root for license information. -->
<DataTemplate x:DataType="local:Command">

<!-- This HorizontalContentAlignment="Stretch" is important
to make sure it takes the entire width of the line -->
to make sure it takes the entire width of the line -->
<ListViewItem HorizontalContentAlignment="Stretch"
AutomationProperties.Name="{x:Bind Name, Mode=OneWay}"
AutomationProperties.AcceleratorKey="{x:Bind KeyChordText, Mode=OneWay}">
Expand All @@ -223,30 +223,32 @@ the MIT License. See LICENSE in the project root for license information. -->
</Grid.ColumnDefinitions>

<IconSourceElement
Grid.Column="0"
IconSource="{x:Bind IconSource, Mode=OneWay}"/>
Grid.Column="0"
Width="16"
Height="16"
IconSource="{x:Bind IconSource, Mode=OneWay}"/>

<TextBlock Grid.Column="1"
HorizontalAlignment="Left"
Text="{x:Bind Name, Mode=OneWay}" />

<!-- The block for the key chord is only visible
when there's actual text set as the label. See
CommandKeyChordVisibilityConverter for details. -->
when there's actual text set as the label. See
CommandKeyChordVisibilityConverter for details. -->
<Border
Grid.Column="2"
Visibility="{x:Bind KeyChordText,
Mode=OneWay,
Converter={StaticResource CommandKeyChordVisibilityConverter}}"
Style="{ThemeResource KeyChordBorderStyle}"
Padding="2,0,2,0"
HorizontalAlignment="Right"
VerticalAlignment="Center">
Grid.Column="2"
Visibility="{x:Bind KeyChordText,
Mode=OneWay,
Converter={StaticResource CommandKeyChordVisibilityConverter}}"
Style="{ThemeResource KeyChordBorderStyle}"
Padding="2,0,2,0"
HorizontalAlignment="Right"
VerticalAlignment="Center">

<TextBlock
Style="{ThemeResource KeyChordTextBlockStyle}"
FontSize="12"
Text="{x:Bind KeyChordText, Mode=OneWay}" />
Style="{ThemeResource KeyChordTextBlockStyle}"
FontSize="12"
Text="{x:Bind KeyChordText, Mode=OneWay}" />
</Border>

<!-- xE70E is ChevronUp. Rotated 90 degrees, it's _ChevronRight_ -->
Expand Down
18 changes: 7 additions & 11 deletions src/cascadia/TerminalApp/TerminalPage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<winrt::hstring, winrt::TerminalApp::Command> commands)
IMapView<winrt::hstring, winrt::TerminalApp::Command> commands)
{
for (const auto& nameAndCmd : commands)
{
Expand All @@ -83,20 +83,16 @@ namespace winrt::TerminalApp::implementation
}
}

static void _recursiveUpdateCommandIcons(Windows::Foundation::Collections::IMapView<winrt::hstring, winrt::TerminalApp::Command> commands)
static void _recursiveUpdateCommandIcons(IMapView<winrt::hstring, winrt::TerminalApp::Command> 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())
{
Expand Down