Skip to content

Commit

Permalink
Introduce IconConverter (microsoft#7830)
Browse files Browse the repository at this point in the history
## Summary of the Pull Request
Introduce the `IconPathConverter` to `TerminalApp`. `Command` and `Profile` now both return the unexpanded icon path. `IconPathConverter` is responsible for expanding the icon path and retrieving the appropriate icon source.

This also removes `Profile`'s expanded icon path and uses the `IconPathConverter` when necessary. This allows users to set profile icons to emoji as well. However, emoji do not appear in the jumplist.

## References
Based on microsoft#7667 

## PR Checklist
* [X] Closes microsoft#7784 
* [x] Documentation updated. If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/terminal) and link it here: #xxx
* [x] Schema updated.

## Validation Steps Performed
Deploy succeeded.
  • Loading branch information
carlos-zamora authored and skyline75489 committed Oct 10, 2020
1 parent 828723f commit 84f0fba
Show file tree
Hide file tree
Showing 23 changed files with 334 additions and 255 deletions.
20 changes: 16 additions & 4 deletions doc/cascadia/profiles.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@
"pattern": "^\\{[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}\\}$",
"type": "string"
},
"Icon": {
"description": "Image file location or an emoji to be used as an icon. Displays within the tab, the dropdown menu, and jumplist.",
"type": [
"string",
"null"
]
},
"ShortcutActionName": {
"enum": [
"adjustFontSize",
Expand Down Expand Up @@ -465,6 +472,14 @@
"type": "array"
}
]
},
"icon": { "$ref": "#/definitions/Icon" },
"name": {
"description": "The name that will appear in the command palette. If one isn't provided, the terminal will attempt to automatically generate a name.",
"type": [
"string",
"null"
]
}
},
"required": [
Expand Down Expand Up @@ -810,10 +825,7 @@
"minimum": -1,
"type": "integer"
},
"icon": {
"description": "Image file location of the icon used in the profile. Displays within the tab and the dropdown menu.",
"type": ["string", "null"]
},
"icon":{ "$ref": "#/definitions/Icon" },
"name": {
"description": "Name of the profile. Displays in the dropdown menu.",
"minLength": 1,
Expand Down
23 changes: 0 additions & 23 deletions src/cascadia/LocalTests_SettingsModel/DeserializationTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ namespace SettingsModelLocalTests
TEST_METHOD(TestInvalidColorSchemeName);
TEST_METHOD(TestHelperFunctions);

TEST_METHOD(TestProfileIconWithEnvVar);
TEST_METHOD(TestProfileBackgroundImageWithEnvVar);

TEST_METHOD(TestCloseOnExitParsing);
Expand Down Expand Up @@ -1379,28 +1378,6 @@ namespace SettingsModelLocalTests
VERIFY_ARE_EQUAL(name2, prof2.Name());
}

void DeserializationTests::TestProfileIconWithEnvVar()
{
const auto expectedPath = wil::ExpandEnvironmentStringsW<std::wstring>(L"%WINDIR%\\System32\\x_80.png");

const std::string settingsJson{ R"(
{
"profiles": [
{
"name": "profile0",
"icon": "%WINDIR%\\System32\\x_80.png"
}
]
})" };

VerifyParseSucceeded(settingsJson);

auto settings = winrt::make_self<implementation::CascadiaSettings>();
settings->_ParseJsonString(settingsJson, false);
settings->LayerJson(settings->_userSettings);
VERIFY_ARE_NOT_EQUAL(0u, settings->_profiles.Size());
VERIFY_ARE_EQUAL(expectedPath, settings->_profiles.GetAt(0).ExpandedIconPath());
}
void DeserializationTests::TestProfileBackgroundImageWithEnvVar()
{
const auto expectedPath = wil::ExpandEnvironmentStringsW<std::wstring>(L"%WINDIR%\\System32\\x_80.png");
Expand Down
22 changes: 11 additions & 11 deletions src/cascadia/LocalTests_SettingsModel/ProfileTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -195,32 +195,32 @@ namespace SettingsModelLocalTests
const auto profile3Json = VerifyParseSucceeded(profile3String);

auto profile0 = implementation::Profile::FromJson(profile0Json);
VERIFY_IS_FALSE(profile0->IconPath().empty());
VERIFY_ARE_EQUAL(L"not-null.png", profile0->IconPath());
VERIFY_IS_FALSE(profile0->Icon().empty());
VERIFY_ARE_EQUAL(L"not-null.png", profile0->Icon());

Log::Comment(NoThrowString().Format(
L"Verify that layering an object the key set to null will clear the key"));
profile0->LayerJson(profile1Json);
VERIFY_IS_TRUE(profile0->IconPath().empty());
VERIFY_IS_TRUE(profile0->Icon().empty());

profile0->LayerJson(profile2Json);
VERIFY_IS_TRUE(profile0->IconPath().empty());
VERIFY_IS_TRUE(profile0->Icon().empty());

profile0->LayerJson(profile3Json);
VERIFY_IS_FALSE(profile0->IconPath().empty());
VERIFY_ARE_EQUAL(L"another-real.png", profile0->IconPath());
VERIFY_IS_FALSE(profile0->Icon().empty());
VERIFY_ARE_EQUAL(L"another-real.png", profile0->Icon());

Log::Comment(NoThrowString().Format(
L"Verify that layering an object _without_ the key will not clear the key"));
profile0->LayerJson(profile2Json);
VERIFY_IS_FALSE(profile0->IconPath().empty());
VERIFY_ARE_EQUAL(L"another-real.png", profile0->IconPath());
VERIFY_IS_FALSE(profile0->Icon().empty());
VERIFY_ARE_EQUAL(L"another-real.png", profile0->Icon());

auto profile1 = implementation::Profile::FromJson(profile1Json);
VERIFY_IS_TRUE(profile1->IconPath().empty());
VERIFY_IS_TRUE(profile1->Icon().empty());
profile1->LayerJson(profile3Json);
VERIFY_IS_FALSE(profile1->IconPath().empty());
VERIFY_ARE_EQUAL(L"another-real.png", profile1->IconPath());
VERIFY_IS_FALSE(profile1->Icon().empty());
VERIFY_ARE_EQUAL(L"another-real.png", profile1->Icon());
}

void ProfileTests::LayerProfilesOnArray()
Expand Down
5 changes: 4 additions & 1 deletion src/cascadia/TerminalApp/CommandPalette.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ the MIT License. See LICENSE in the project root for license information. -->
<local:EmptyStringVisibilityConverter x:Key="CommandKeyChordVisibilityConverter"/>
<local:EmptyStringVisibilityConverter x:Key="ParentCommandVisibilityConverter"/>
<local:HasNestedCommandsVisibilityConverter x:Key="HasNestedCommandsVisibilityConverter"/>
<local:IconPathConverter x:Key="IconSourceConverter"/>

<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Dark">
Expand Down Expand Up @@ -227,7 +228,9 @@ 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}"/>
IconSource="{x:Bind Icon,
Mode=OneWay,
Converter={StaticResource IconSourceConverter}}"/>

<TextBlock Grid.Column="1"
HorizontalAlignment="Left"
Expand Down
207 changes: 207 additions & 0 deletions src/cascadia/TerminalApp/IconPathConverter.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
#include "pch.h"
#include "IconPathConverter.h"
#include "IconPathConverter.g.cpp"

#include "Utils.h"

using namespace winrt::Windows;
using namespace winrt::Windows::UI::Xaml;

namespace winrt::TerminalApp::implementation
{
// These are templates that help us figure out which BitmapIconSource/FontIconSource to use for a given IconSource.
// We have to do this because some of our code still wants to use WUX/MUX IconSources.
#pragma region BitmapIconSource
template<typename TIconSource>
struct BitmapIconSource
{
};

template<>
struct BitmapIconSource<winrt::Microsoft::UI::Xaml::Controls::IconSource>
{
using type = winrt::Microsoft::UI::Xaml::Controls::BitmapIconSource;
};

template<>
struct BitmapIconSource<winrt::Windows::UI::Xaml::Controls::IconSource>
{
using type = winrt::Windows::UI::Xaml::Controls::BitmapIconSource;
};
#pragma endregion

#pragma region FontIconSource
template<typename TIconSource>
struct FontIconSource
{
};

template<>
struct FontIconSource<winrt::Microsoft::UI::Xaml::Controls::IconSource>
{
using type = winrt::Microsoft::UI::Xaml::Controls::FontIconSource;
};

template<>
struct FontIconSource<winrt::Windows::UI::Xaml::Controls::IconSource>
{
using type = winrt::Windows::UI::Xaml::Controls::FontIconSource;
};
#pragma endregion

// Method Description:
// - Creates an IconSource for the given path. The icon returned is a colored
// icon. If we couldn't create the icon for any reason, we return an empty
// IconElement.
// Template Types:
// - <TIconSource>: The type of IconSource (MUX, WUX) to generate.
// Arguments:
// - path: the full, expanded path to the icon.
// Return Value:
// - An IconElement with its IconSource set, if possible.
template<typename TIconSource>
TIconSource _getColoredBitmapIcon(const winrt::hstring& path)
{
if (!path.empty())
{
try
{
winrt::Windows::Foundation::Uri iconUri{ path };
BitmapIconSource<TIconSource>::type iconSource;
// Make sure to set this to false, so we keep the RGB data of the
// image. Otherwise, the icon will be white for all the
// non-transparent pixels in the image.
iconSource.ShowAsMonochrome(false);
iconSource.UriSource(iconUri);
return iconSource;
}
CATCH_LOG();
}

return nullptr;
}

// Method Description:
// - Creates an IconSource for the given path.
// * 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.
// * If we couldn't create the icon for any reason, we return an empty
// IconElement.
// Template Types:
// - <TIconSource>: The type of IconSource (MUX, WUX) to generate.
// Arguments:
// - path: the unprocessed path to the icon.
// Return Value:
// - An IconElement with its IconSource set, if possible.
template<typename TIconSource>
TIconSource _getIconSource(const winrt::hstring& iconPath)
{
TIconSource iconSource{ nullptr };

if (iconPath.size() != 0)
{
const auto expandedIconPath{ _expandIconPath(iconPath) };
iconSource = _getColoredBitmapIcon<TIconSource>(expandedIconPath);

// If we fail to set the icon source using the "icon" as a path,
// let's try it as a symbol/emoji.
//
// Anything longer than 2 wchar_t's _isn't_ an emoji or symbol, so
// don't do this if it's just an invalid path.
if (!iconSource && iconPath.size() <= 2)
{
try
{
FontIconSource<TIconSource>::type icon;
const wchar_t ch = iconPath[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(winrt::Windows::UI::Xaml::Media::FontFamily{ L"Segoe MDL2 Assets" });
}
else
{
// Note: you _do_ need to manually set the font here.
icon.FontFamily(winrt::Windows::UI::Xaml::Media::FontFamily{ L"Segoe UI" });
}
icon.FontSize(12);
icon.Glyph(iconPath);
iconSource = icon;
}
CATCH_LOG();
}
}
if (!iconSource)
{
// 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:.
BitmapIconSource<TIconSource>::type icon;
icon.UriSource(nullptr);
iconSource = icon;
}

return iconSource;
}

static winrt::hstring _expandIconPath(hstring iconPath)
{
if (iconPath.empty())
{
return iconPath;
}
winrt::hstring envExpandedPath{ wil::ExpandEnvironmentStringsW<std::wstring>(iconPath.c_str()) };
return envExpandedPath;
}

// Method Description:
// - Attempt to convert something into another type. For the
// IconPathConverter, we support 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:
// - value: the input object to attempt to convert into an IconSource.
// Return Value:
// - Visible if the object was a string and wasn't the empty string.
Foundation::IInspectable IconPathConverter::Convert(Foundation::IInspectable const& value,
Windows::UI::Xaml::Interop::TypeName const& /* targetType */,
Foundation::IInspectable const& /* parameter */,
hstring const& /* language */)
{
const auto& iconPath = winrt::unbox_value_or<winrt::hstring>(value, L"");
return _getIconSource<Controls::IconSource>(iconPath);
}

// unused for one-way bindings
Foundation::IInspectable IconPathConverter::ConvertBack(Foundation::IInspectable const& /* value */,
Windows::UI::Xaml::Interop::TypeName const& /* targetType */,
Foundation::IInspectable const& /* parameter */,
hstring const& /* language */)
{
throw hresult_not_implemented();
}

Windows::UI::Xaml::Controls::IconSource IconPathConverter::IconSourceWUX(hstring path)
{
return _getIconSource<Windows::UI::Xaml::Controls::IconSource>(path);
}

Microsoft::UI::Xaml::Controls::IconSource IconPathConverter::IconSourceMUX(hstring path)
{
return _getIconSource<Microsoft::UI::Xaml::Controls::IconSource>(path);
}
}
30 changes: 30 additions & 0 deletions src/cascadia/TerminalApp/IconPathConverter.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#pragma once

#include "IconPathConverter.g.h"
#include "..\inc\cppwinrt_utils.h"

namespace winrt::TerminalApp::implementation
{
struct IconPathConverter : IconPathConverterT<IconPathConverter>
{
IconPathConverter() = default;

Windows::Foundation::IInspectable Convert(Windows::Foundation::IInspectable const& value,
Windows::UI::Xaml::Interop::TypeName const& targetType,
Windows::Foundation::IInspectable const& parameter,
hstring const& language);

Windows::Foundation::IInspectable ConvertBack(Windows::Foundation::IInspectable const& value,
Windows::UI::Xaml::Interop::TypeName const& targetType,
Windows::Foundation::IInspectable const& parameter,
hstring const& language);

static Windows::UI::Xaml::Controls::IconSource IconSourceWUX(hstring path);
static Microsoft::UI::Xaml::Controls::IconSource IconSourceMUX(hstring path);
};
}

namespace winrt::TerminalApp::factory_implementation
{
BASIC_FACTORY(IconPathConverter);
}
Loading

0 comments on commit 84f0fba

Please sign in to comment.