Skip to content

Commit

Permalink
Represent font face as a combo box in SUI (#9275)
Browse files Browse the repository at this point in the history
## Summary of the Pull Request

This replaces the Profiles > Font Face text box with a combo box.

## References
#6800 - Settings UI Epic

## Detailed Description of the Pull Request / Additional comments

- Enumerating the fonts
  - [This doc](https://docs.microsoft.com/en-us/windows/win32/directwrite/font-enumeration) was the main reference used to enumerate the fonts. It was mildly adapted to use WinRT instead of WRL.
- Updating the UI
  - Similar to other combo box settings, `Profiles` keeps a reference to the current value. We use that as a way to update the settings model. If an invalid value is used, we fallback to `Cascadia Mono`.
  - A checkbox was added to let the user select from all of the installed fonts, or just the monospace ones.

## Demo
![Font Face Combo Box Demo](https://user-images.githubusercontent.com/11050425/109342917-6cd3b600-7821-11eb-8df9-fb988b037e02.gif)
  • Loading branch information
carlos-zamora committed Mar 8, 2021
1 parent c6a3171 commit 87fa526
Show file tree
Hide file tree
Showing 6 changed files with 362 additions and 43 deletions.
257 changes: 247 additions & 10 deletions src/cascadia/TerminalSettingsEditor/Profiles.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,33 +27,48 @@ static const std::array<winrt::guid, 2> InBoxProfileGuids{

namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
Windows::Foundation::Collections::IObservableVector<Editor::Font> ProfileViewModel::_MonospaceFontList{ nullptr };
Windows::Foundation::Collections::IObservableVector<Editor::Font> ProfileViewModel::_FontList{ nullptr };

ProfileViewModel::ProfileViewModel(const Model::Profile& profile) :
_profile{ profile }
_profile{ profile },
_ShowAllFonts{ false }
{
// Add a property changed handler to our own property changed event.
// When the BackgroundImagePath changes, we _also_ need to change the
// value of UseDesktopBGImage.
//
// We need to do this so if someone manually types "desktopWallpaper"
// into the path TextBox, we properly update the checkbox and stored
// _lastBgImagePath. Without this, then we'll permanently hide the text
// box, prevent it from ever being changed again.
//
// We do the same for the starting directory path
// This propagates changes from the settings model to anybody listening to our
// unique view model members.
PropertyChanged([this](auto&&, const PropertyChangedEventArgs& args) {
const auto viewModelProperty{ args.PropertyName() };
if (viewModelProperty == L"BackgroundImagePath")
{
// notify listener that all background image related values might have changed
//
// We need to do this so if someone manually types "desktopWallpaper"
// into the path TextBox, we properly update the checkbox and stored
// _lastBgImagePath. Without this, then we'll permanently hide the text
// box, prevent it from ever being changed again.
_NotifyChanges(L"UseDesktopBGImage", L"BackgroundImageSettingsVisible");
}
else if (viewModelProperty == L"IsBaseLayer")
{
// we _always_ want to show the background image settings in base layer
_NotifyChanges(L"BackgroundImageSettingsVisible");
}
else if (viewModelProperty == L"StartingDirectory")
{
// notify listener that all starting directory related values might have changed
// NOTE: this is similar to what is done with BackgroundImagePath above
_NotifyChanges(L"UseParentProcessDirectory", L"UseCustomStartingDirectory");
}
else if (viewModelProperty == L"FontFace")
{
// notify listener that all font face related values might have changed
if (!UsingMonospaceFont())
{
_ShowAllFonts = true;
}
_NotifyChanges(L"ShowAllFonts", L"UsingMonospaceFont");
}
});

// Cache the original BG image path. If the user clicks "Use desktop
Expand All @@ -69,6 +84,183 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
_lastStartingDirectoryPath = StartingDirectory();
}

// generate the font list, if we don't have one
if (!_FontList || !_MonospaceFontList)
{
UpdateFontList();
}
}

// Method Description:
// - Updates the lists of fonts and sorts them alphabetically
void ProfileViewModel::UpdateFontList() noexcept
try
{
// initialize font list
std::vector<Editor::Font> fontList;
std::vector<Editor::Font> monospaceFontList;

// get a DWriteFactory
com_ptr<IDWriteFactory> factory;
THROW_IF_FAILED(DWriteCreateFactory(
DWRITE_FACTORY_TYPE_SHARED,
__uuidof(IDWriteFactory),
reinterpret_cast<::IUnknown**>(factory.put())));

// get the font collection; subscribe to updates
com_ptr<IDWriteFontCollection> fontCollection;
THROW_IF_FAILED(factory->GetSystemFontCollection(fontCollection.put(), TRUE));

for (UINT32 i = 0; i < fontCollection->GetFontFamilyCount(); ++i)
{
try
{
// get the font family
com_ptr<IDWriteFontFamily> fontFamily;
THROW_IF_FAILED(fontCollection->GetFontFamily(i, fontFamily.put()));

// get the font's localized names
com_ptr<IDWriteLocalizedStrings> localizedFamilyNames;
THROW_IF_FAILED(fontFamily->GetFamilyNames(localizedFamilyNames.put()));

// construct a font entry for tracking
if (const auto fontEntry{ _GetFont(localizedFamilyNames) })
{
// check if the font is monospaced
try
{
com_ptr<IDWriteFont> font;
THROW_IF_FAILED(fontFamily->GetFirstMatchingFont(DWRITE_FONT_WEIGHT::DWRITE_FONT_WEIGHT_NORMAL,
DWRITE_FONT_STRETCH::DWRITE_FONT_STRETCH_NORMAL,
DWRITE_FONT_STYLE::DWRITE_FONT_STYLE_NORMAL,
font.put()));

// add the font name to our list of monospace fonts
const auto castedFont{ font.try_as<IDWriteFont1>() };
if (castedFont && castedFont->IsMonospacedFont())
{
monospaceFontList.emplace_back(fontEntry);
}
}
CATCH_LOG();

// add the font name to our list of all fonts
fontList.emplace_back(std::move(fontEntry));
}
}
CATCH_LOG();
}

// sort and save the lists
std::sort(begin(fontList), end(fontList));
_FontList = single_threaded_observable_vector<Editor::Font>(std::move(fontList));

std::sort(begin(monospaceFontList), end(monospaceFontList));
_MonospaceFontList = single_threaded_observable_vector<Editor::Font>(std::move(monospaceFontList));
}
CATCH_LOG();

Editor::Font ProfileViewModel::_GetFont(com_ptr<IDWriteLocalizedStrings> localizedFamilyNames)
{
// used for the font's name as an identifier (i.e. text block's font family property)
std::wstring nameID;
UINT32 nameIDIndex;

// used for the font's localized name
std::wstring localizedName;
UINT32 localizedNameIndex;

// use our current locale to find the localized name
BOOL exists{ FALSE };
HRESULT hr;
wchar_t localeName[LOCALE_NAME_MAX_LENGTH];
if (GetUserDefaultLocaleName(localeName, LOCALE_NAME_MAX_LENGTH))
{
hr = localizedFamilyNames->FindLocaleName(localeName, &localizedNameIndex, &exists);
}
if (SUCCEEDED(hr) && !exists)
{
// if we can't find the font for our locale, fallback to the en-us one
// Source: https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nf-dwrite-idwritelocalizedstrings-findlocalename
hr = localizedFamilyNames->FindLocaleName(L"en-us", &localizedNameIndex, &exists);
}
if (!exists)
{
// failed to find the correct locale, using the first one
localizedNameIndex = 0;
}

// get the localized name
UINT32 nameLength;
THROW_IF_FAILED(localizedFamilyNames->GetStringLength(localizedNameIndex, &nameLength));

localizedName.resize(nameLength);
THROW_IF_FAILED(localizedFamilyNames->GetString(localizedNameIndex, localizedName.data(), nameLength + 1));

// now get the nameID
hr = localizedFamilyNames->FindLocaleName(L"en-us", &nameIDIndex, &exists);
if (FAILED(hr) || !exists)
{
// failed to find it, using the first one
nameIDIndex = 0;
}

// get the nameID
THROW_IF_FAILED(localizedFamilyNames->GetStringLength(nameIDIndex, &nameLength));
nameID.resize(nameLength);
THROW_IF_FAILED(localizedFamilyNames->GetString(nameIDIndex, nameID.data(), nameLength + 1));

if (!nameID.empty() && !localizedName.empty())
{
return make<Font>(nameID, localizedName);
}
return nullptr;
}

IObservableVector<Editor::Font> ProfileViewModel::CompleteFontList() const noexcept
{
return _FontList;
}

IObservableVector<Editor::Font> ProfileViewModel::MonospaceFontList() const noexcept
{
return _MonospaceFontList;
}

// Method Description:
// - Searches through our list of monospace fonts to determine if the settings model's current font face is a monospace font
// - NOTE: This is information stored from DWrite in _UpdateFontList()
bool ProfileViewModel::UsingMonospaceFont() const noexcept
{
bool result{ false };
const auto currentFont{ FontFace() };
for (const auto& font : _MonospaceFontList)
{
if (font.LocalizedName() == currentFont)
{
result = true;
}
}
return result;
}

// Method Description:
// - Determines whether we should show the list of all the fonts, or we should just show monospace fonts
bool ProfileViewModel::ShowAllFonts() const noexcept
{
// - _ShowAllFonts is directly bound to the checkbox. So this is the user set value.
// - If we are not using a monospace font, show all of the fonts so that the ComboBox is still properly bound
return _ShowAllFonts || !UsingMonospaceFont();
}

void ProfileViewModel::ShowAllFonts(const bool& value)
{
if (_ShowAllFonts != value)
{
_ShowAllFonts = value;
_NotifyChanges(L"ShowAllFonts");
}
}

bool ProfileViewModel::CanDeleteProfile() const
Expand Down Expand Up @@ -221,13 +413,54 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
const auto backgroundImgCheckboxTooltip{ ToolTipService::GetToolTip(UseDesktopImageCheckBox()) };
Automation::AutomationProperties::SetFullDescription(UseDesktopImageCheckBox(), unbox_value<hstring>(backgroundImgCheckboxTooltip));

const auto showAllFontsCheckboxTooltip{ ToolTipService::GetToolTip(ShowAllFontsCheckbox()) };
Automation::AutomationProperties::SetFullDescription(ShowAllFontsCheckbox(), unbox_value<hstring>(showAllFontsCheckboxTooltip));

Automation::AutomationProperties::SetName(DeleteButton(), RS_(L"Profile_DeleteButton/Text"));
}

IInspectable Profiles::CurrentFontFace() const
{
// look for the current font in our shown list of fonts
const auto& profileVM{ State().Profile() };
const auto profileFontFace{ profileVM.FontFace() };
const auto& currentFontList{ profileVM.ShowAllFonts() ? profileVM.CompleteFontList() : profileVM.MonospaceFontList() };
IInspectable fallbackFont;
for (const auto& font : currentFontList)
{
if (font.LocalizedName() == profileFontFace)
{
return box_value(font);
}
else if (font.LocalizedName() == L"Cascadia Mono")
{
fallbackFont = box_value(font);
}
}

// we couldn't find the desired font, set to "Cascadia Mono" since that ships by default
return fallbackFont;
}

void Profiles::FontFace_SelectionChanged(IInspectable const& /*sender*/, SelectionChangedEventArgs const& e)
{
// NOTE: We need to hook up a selection changed event handler here instead of directly binding to the profile view model.
// A two way binding to the view model causes an infinite loop because both combo boxes keep fighting over which one's right.
const auto selectedItem{ e.AddedItems().GetAt(0) };
const auto newFontFace{ unbox_value<Editor::Font>(selectedItem) };
State().Profile().FontFace(newFontFace.LocalizedName());
}

void Profiles::OnNavigatedTo(const NavigationEventArgs& e)
{
_State = e.Parameter().as<Editor::ProfilePageNavigationState>();

// generate the font list, if we don't have one
if (!_State.Profile().CompleteFontList() || !_State.Profile().MonospaceFontList())
{
ProfileViewModel::UpdateFontList();
}

const auto& colorSchemeMap{ _State.Schemes() };
for (const auto& pair : colorSchemeMap)
{
Expand Down Expand Up @@ -300,6 +533,10 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
_PropertyChangedHandlers(*this, PropertyChangedEventArgs{ L"CurrentColorScheme" });
}
else if (settingName == L"FontFace" || settingName == L"CurrentFontList")
{
_PropertyChangedHandlers(*this, PropertyChangedEventArgs{ L"CurrentFontFace" });
}
else if (settingName == L"BackgroundImageAlignment")
{
_UpdateBIAlignmentControl(static_cast<int32_t>(_State.Profile().BackgroundImageAlignment()));
Expand Down
41 changes: 38 additions & 3 deletions src/cascadia/TerminalSettingsEditor/Profiles.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#pragma once

#include "Font.g.h"
#include "Profiles.g.h"
#include "ProfilePageNavigationState.g.h"
#include "DeleteProfileEventArgs.g.h"
Expand All @@ -12,20 +13,44 @@

namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
struct Font : FontT<Font>
{
public:
Font(std::wstring name, std::wstring localizedName) :
_Name{ name },
_LocalizedName{ localizedName } {};

hstring ToString() { return _LocalizedName; }

WINRT_PROPERTY(hstring, Name);
WINRT_PROPERTY(hstring, LocalizedName);
};

struct ProfileViewModel : ProfileViewModelT<ProfileViewModel>, ViewModelHelper<ProfileViewModel>
{
public:
ProfileViewModel(const Model::Profile& profile);

bool CanDeleteProfile() const;

// background image
bool UseDesktopBGImage();
void UseDesktopBGImage(const bool useDesktop);
bool BackgroundImageSettingsVisible();

// starting directory
bool UseParentProcessDirectory();
void UseParentProcessDirectory(const bool useParent);
bool UseCustomStartingDirectory();
bool BackgroundImageSettingsVisible();

// font face
static void UpdateFontList() noexcept;
Windows::Foundation::Collections::IObservableVector<Editor::Font> CompleteFontList() const noexcept;
Windows::Foundation::Collections::IObservableVector<Editor::Font> MonospaceFontList() const noexcept;
bool UsingMonospaceFont() const noexcept;
bool ShowAllFonts() const noexcept;
void ShowAllFonts(const bool& value);

// general profile knowledge
bool CanDeleteProfile() const;
WINRT_PROPERTY(bool, IsBaseLayer, false);

PERMANENT_OBSERVABLE_PROJECTED_SETTING(_profile, Guid);
Expand Down Expand Up @@ -71,6 +96,12 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
Model::Profile _profile;
winrt::hstring _lastBgImagePath;
winrt::hstring _lastStartingDirectoryPath;
bool _ShowAllFonts;

static Windows::Foundation::Collections::IObservableVector<Editor::Font> _MonospaceFontList;
static Windows::Foundation::Collections::IObservableVector<Editor::Font> _FontList;

static Editor::Font _GetFont(com_ptr<IDWriteLocalizedStrings> localizedFamilyNames);
};

struct DeleteProfileEventArgs :
Expand Down Expand Up @@ -129,6 +160,9 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
public:
Profiles();

// font face
Windows::Foundation::IInspectable CurrentFontFace() const;

void OnNavigatedTo(const Windows::UI::Xaml::Navigation::NavigationEventArgs& e);
void OnNavigatedFrom(const Windows::UI::Xaml::Navigation::NavigationEventArgs& e);

Expand All @@ -142,6 +176,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
void BIAlignment_Click(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::RoutedEventArgs const& e);
void DeleteConfirmation_Click(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::RoutedEventArgs const& e);
void Pivot_SelectionChanged(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::RoutedEventArgs const& e);
void FontFace_SelectionChanged(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Controls::SelectionChangedEventArgs const& e);

// CursorShape visibility logic
bool IsVintageCursor() const;
Expand Down

0 comments on commit 87fa526

Please sign in to comment.