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

Introduce IconConverter #7830

Merged
merged 6 commits into from
Oct 8, 2020
Merged
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
Expand Up @@ -1399,7 +1399,10 @@ namespace SettingsModelLocalTests
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());

const auto profile{ settings->_profiles.GetAt(0) };
const auto expandedIconPath{ wil::ExpandEnvironmentStringsW<std::wstring>(profile.Icon().c_str()) };
VERIFY_ARE_EQUAL(expectedPath, expandedIconPath);
carlos-zamora marked this conversation as resolved.
Show resolved Hide resolved
}
void DeserializationTests::TestProfileBackgroundImageWithEnvVar()
{
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
208 changes: 208 additions & 0 deletions src/cascadia/TerminalApp/IconPathConverter.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
#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"");
Controls::IconSource iconSource{ _getIconSource<Controls::IconSource>(iconPath) };
return winrt::box_value(iconSource);
carlos-zamora marked this conversation as resolved.
Show resolved Hide resolved
}

// 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);
}
22 changes: 22 additions & 0 deletions src/cascadia/TerminalApp/IconPathConverter.idl
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

namespace TerminalApp
{
// See https://docs.microsoft.com/en-us/windows/uwp/data-binding/data-binding-quickstart

// We use the default attribute to declare IValueConverter as the default
// interface. In the listing, IconPathConverter has only a
// constructor, and no methods, so no default interface is generated for it.
// The default attribute is optimal if you won't be adding instance members
// to IconPathConverter, because no QueryInterface will be
// required to call the IValueConverter methods
runtimeclass IconPathConverter : [default] Windows.UI.Xaml.Data.IValueConverter
{
IconPathConverter();

static Windows.UI.Xaml.Controls.IconSource IconSourceWUX(String path);
static Microsoft.UI.Xaml.Controls.IconSource IconSourceMUX(String path);
};

}
9 changes: 5 additions & 4 deletions src/cascadia/TerminalApp/Jumplist.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,13 @@ static constexpr bool _isProbableFilePath(std::wstring_view path)
// paths to have the "correct" slash direction.
static std::wstring _normalizeIconPath(std::wstring_view path)
{
if (_isProbableFilePath(path))
const auto fullPath{ wil::ExpandEnvironmentStringsW<std::wstring>(path.data()) };
if (_isProbableFilePath(fullPath))
{
std::filesystem::path asPath{ path };
std::filesystem::path asPath{ fullPath };
return asPath.make_preferred().wstring();
}
return std::wstring{ path };
return std::wstring{ fullPath };
}

// Function Description:
Expand Down Expand Up @@ -168,7 +169,7 @@ HRESULT Jumplist::UpdateJumplist(const CascadiaSettings& settings) noexcept

// Create the shell link object for the profile
winrt::com_ptr<IShellLinkW> shLink;
const auto normalizedIconPath{ _normalizeIconPath(profile.ExpandedIconPath()) };
const auto normalizedIconPath{ _normalizeIconPath(profile.Icon()) };
RETURN_IF_FAILED(_createShellLink(profile.Name(), normalizedIconPath, args, shLink.put()));

RETURN_IF_FAILED(jumplistItems->AddObject(shLink.get()));
Expand Down
8 changes: 4 additions & 4 deletions src/cascadia/TerminalApp/Tab.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -229,11 +229,11 @@ namespace winrt::TerminalApp::implementation
if (auto tab{ weakThis.get() })
{
// The TabViewItem Icon needs MUX while the IconSourceElement in the CommandPalette needs WUX...
IconSource(GetColoredIcon<winrt::WUX::Controls::IconSource>(_lastIconPath));
_tabViewItem.IconSource(GetColoredIcon<winrt::MUX::Controls::IconSource>(_lastIconPath));
IconSource(IconPathConverter::IconSourceWUX(_lastIconPath));
_tabViewItem.IconSource(IconPathConverter::IconSourceMUX(_lastIconPath));

// Update SwitchToTab command's icon
SwitchToTabCommand().IconSource(IconSource());
SwitchToTabCommand().Icon(_lastIconPath);
}
}

Expand Down Expand Up @@ -1048,7 +1048,7 @@ namespace winrt::TerminalApp::implementation
Command command;
command.Action(focusTabAction);
command.Name(Title());
command.IconSource(IconSource());
command.Icon(_lastIconPath);

SwitchToTabCommand(command);
}
Expand Down
Loading