Files
terminal-microsoft-1/src/cascadia/TerminalSettingsEditor/Appearances.cpp
Carlos Zamora 27aae1f245 Fix WindowRoot memory leak in SUI (#19826)
## Summary of the Pull Request
Fixes a memory leak for `IHostedInWindow` in the TerminalSettingsEditor.

The memory leak occurs from `MainPage` creating an object that stores
reference to back to `MainPage` as `IHostedInWindow`. With `IconPicker`
specifically, the cycle would look like `MainPage` --> `NewTabMenu` -->
`IconPicker` --> `MainPage`.

I've audited uses of `IHostedInWindow` in the TerminalSettingsEditor and
replaced them as weak references. I also checked areas that
`WindowRoot()` was called and added a null-check for safety.

## Validation Steps Performed
Verified `MainPage` and `NewTabMenu` dtors are called when the settings
UI closes from...
 Launch page (base case - it doesn't have an `IHostedInWindow`)
 New Tab Menu page
2026-02-03 14:25:25 -08:00

1516 lines
57 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "Appearances.h"
#include "../WinRTUtils/inc/Utils.h"
#include "EnumEntry.h"
#include "ProfileViewModel.h"
#include "Appearances.g.cpp"
using namespace winrt::Windows::UI::Text;
using namespace winrt::Windows::UI::Xaml;
using namespace winrt::Windows::UI::Xaml::Controls;
using namespace winrt::Windows::UI::Xaml::Data;
using namespace winrt::Windows::UI::Xaml::Navigation;
using namespace winrt::Windows::Foundation;
using namespace winrt::Windows::Foundation::Collections;
using namespace winrt::Microsoft::Terminal::Settings::Model;
// These features are enabled by default by DWrite, so if a user adds them,
// we initialize the setting to a value of 1 instead of 0.
static constexpr std::array s_defaultFeatures{
DWRITE_MAKE_FONT_FEATURE_TAG('c', 'a', 'l', 't'),
DWRITE_MAKE_FONT_FEATURE_TAG('c', 'c', 'm', 'p'),
DWRITE_MAKE_FONT_FEATURE_TAG('c', 'l', 'i', 'g'),
DWRITE_MAKE_FONT_FEATURE_TAG('d', 'i', 's', 't'),
DWRITE_MAKE_FONT_FEATURE_TAG('k', 'e', 'r', 'n'),
DWRITE_MAKE_FONT_FEATURE_TAG('l', 'i', 'g', 'a'),
DWRITE_MAKE_FONT_FEATURE_TAG('l', 'o', 'c', 'l'),
DWRITE_MAKE_FONT_FEATURE_TAG('m', 'a', 'r', 'k'),
DWRITE_MAKE_FONT_FEATURE_TAG('m', 'k', 'm', 'k'),
DWRITE_MAKE_FONT_FEATURE_TAG('r', 'l', 'i', 'g'),
DWRITE_MAKE_FONT_FEATURE_TAG('r', 'n', 'r', 'n'),
};
namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
struct TagToStringImpl
{
explicit TagToStringImpl(uint32_t tag) noexcept
{
_buffer[0] = static_cast<wchar_t>((tag >> 0) & 0xFF);
_buffer[1] = static_cast<wchar_t>((tag >> 8) & 0xFF);
_buffer[2] = static_cast<wchar_t>((tag >> 16) & 0xFF);
_buffer[3] = static_cast<wchar_t>((tag >> 24) & 0xFF);
_buffer[4] = 0;
}
operator std::wstring_view() const noexcept
{
return { &_buffer[0], 4 };
}
private:
wchar_t _buffer[5];
};
// Turns a DWRITE_MAKE_OPENTYPE_TAG into a string_view...
// (...buffer holder because someone needs to hold onto the data the view refers to.)
static TagToStringImpl tagToString(uint32_t tag) noexcept
{
return TagToStringImpl{ tag };
}
// Turns a string to a DWRITE_MAKE_OPENTYPE_TAG. Returns 0 on failure.
static uint32_t tagFromString(std::wstring_view str) noexcept
{
if (str.size() != 4)
{
return 0;
}
// Check if all 4 characters are printable ASCII.
for (int i = 0; i < 4; ++i)
{
const auto ch = str[i];
if (ch < 0x20 || ch > 0x7E)
{
return 0;
}
}
return DWRITE_MAKE_OPENTYPE_TAG(str[0], str[1], str[2], str[3]);
}
static winrt::hstring getLocalizedStringByIndex(IDWriteLocalizedStrings* strings, UINT32 index)
{
UINT32 length = 0;
THROW_IF_FAILED(strings->GetStringLength(index, &length));
winrt::impl::hstring_builder builder{ length };
THROW_IF_FAILED(strings->GetString(index, builder.data(), length + 1));
return builder.to_hstring();
}
static UINT32 getLocalizedStringIndex(IDWriteLocalizedStrings* strings, const wchar_t* locale, UINT32 fallback)
{
UINT32 index;
BOOL exists;
if (FAILED(strings->FindLocaleName(locale, &index, &exists)) || !exists)
{
index = fallback;
}
return index;
}
Font::Font(winrt::hstring name, winrt::hstring localizedName) :
_Name{ std::move(name) },
_LocalizedName{ std::move(localizedName) }
{
}
bool FontKeyValuePair::SortAscending(const Editor::FontKeyValuePair& lhs, const Editor::FontKeyValuePair& rhs)
{
const auto& a = winrt::get_self<FontKeyValuePair>(lhs)->KeyDisplayStringRef();
const auto& b = winrt::get_self<FontKeyValuePair>(rhs)->KeyDisplayStringRef();
return til::compare_linguistic_insensitive(a, b) < 0;
}
FontKeyValuePair::FontKeyValuePair(winrt::weak_ref<AppearanceViewModel> vm, winrt::hstring keyDisplayString, uint32_t key, float value, bool isFontFeature) :
_vm{ std::move(vm) },
_keyDisplayString{ std::move(keyDisplayString) },
_key{ key },
_value{ value },
_isFontFeature{ isFontFeature }
{
}
uint32_t FontKeyValuePair::Key() const noexcept
{
return _key;
}
winrt::hstring FontKeyValuePair::KeyDisplayString()
{
return KeyDisplayStringRef();
}
// You can't return a const-ref from a WinRT function, because the cppwinrt generated wrapper chokes on it.
// So, now we got two KeyDisplayString() functions, because I refuse to AddRef/Release this for no reason.
// I mean, really it makes no perf. difference, but I'm not kneeling down for an incompetent code generator.
const winrt::hstring& FontKeyValuePair::KeyDisplayStringRef()
{
if (!_keyDisplayString.empty())
{
return _keyDisplayString;
}
const auto tagString = tagToString(_key);
hstring displayString;
if (_isFontFeature)
{
const auto key = fmt::format(FMT_COMPILE(L"Profile_FontFeature_{}"), std::wstring_view{ tagString });
if (HasLibraryResourceWithName(key))
{
displayString = GetLibraryResourceString(key);
displayString = til::hstring_format(FMT_COMPILE(L"{} ({})"), displayString, std::wstring_view{ tagString });
}
}
if (displayString.empty())
{
displayString = hstring{ tagString };
}
_keyDisplayString = displayString;
return _keyDisplayString;
}
float FontKeyValuePair::Value() const noexcept
{
return _value;
}
void FontKeyValuePair::Value(float v)
{
if (_value == v)
{
return;
}
_value = v;
if (const auto vm = _vm.get())
{
vm->UpdateFontSetting(this);
}
}
void FontKeyValuePair::SetValueDirect(float v)
{
_value = v;
}
bool FontKeyValuePair::IsFontFeature() const noexcept
{
return _isFontFeature;
}
winrt::hstring FontKeyValuePair::AutomationName()
{
return til::hstring_format(FMT_COMPILE(L"{}: {}"), KeyDisplayStringRef(), _value);
}
AppearanceViewModel::AppearanceViewModel(const Model::AppearanceConfig& appearance) :
_appearance{ appearance }
{
// Add a property changed handler to our own property changed event.
// 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", L"CurrentBackgroundImagePath");
}
else if (viewModelProperty == L"BackgroundImageAlignment")
{
_NotifyChanges(L"BackgroundImageAlignmentCurrentValue");
}
else if (viewModelProperty == L"Foreground")
{
_NotifyChanges(L"ForegroundPreview");
}
else if (viewModelProperty == L"Background")
{
_NotifyChanges(L"BackgroundPreview");
}
else if (viewModelProperty == L"SelectionBackground")
{
_NotifyChanges(L"SelectionBackgroundPreview");
}
else if (viewModelProperty == L"CursorColor")
{
_NotifyChanges(L"CursorColorPreview");
}
else if (viewModelProperty == L"DarkColorSchemeName" || viewModelProperty == L"LightColorSchemeName")
{
_NotifyChanges(L"CurrentColorScheme");
}
else if (viewModelProperty == L"CurrentColorScheme")
{
_NotifyChanges(L"ForegroundPreview", L"BackgroundPreview", L"SelectionBackgroundPreview", L"CursorColorPreview");
}
});
// Cache the original BG image path. If the user clicks "Use desktop
// wallpaper", then un-checks it, this is the string we'll restore to
// them.
if (BackgroundImagePath().Path() != L"desktopWallpaper")
{
_lastBgImagePath = BackgroundImagePath().Path();
}
}
winrt::hstring AppearanceViewModel::FontFace() const
{
return _appearance.SourceProfile().FontInfo().FontFace();
}
void AppearanceViewModel::FontFace(const winrt::hstring& value)
{
const auto fontInfo = _appearance.SourceProfile().FontInfo();
if (fontInfo.FontFace() == value)
{
return;
}
fontInfo.FontFace(value);
_invalidateFontFaceDependents();
_NotifyChanges(L"HasFontFace", L"FontFace");
}
bool AppearanceViewModel::HasFontFace() const
{
return _appearance.SourceProfile().FontInfo().HasFontFace();
}
void AppearanceViewModel::ClearFontFace()
{
const auto fontInfo = _appearance.SourceProfile().FontInfo();
fontInfo.ClearFontFace();
_invalidateFontFaceDependents();
_NotifyChanges(L"HasFontFace", L"FontFace");
}
Model::FontConfig AppearanceViewModel::FontFaceOverrideSource() const
{
return _appearance.SourceProfile().FontInfo().FontFaceOverrideSource();
}
void AppearanceViewModel::_refreshFontFaceDependents()
{
wil::com_ptr<IDWriteFactory> factory;
THROW_IF_FAILED(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(factory), reinterpret_cast<::IUnknown**>(factory.addressof())));
wil::com_ptr<IDWriteFontCollection> fontCollection;
THROW_IF_FAILED(factory->GetSystemFontCollection(fontCollection.addressof(), FALSE));
const auto fontFaceSpec = FontFace();
std::wstring missingFonts;
std::wstring proportionalFonts;
std::array<std::vector<Editor::FontKeyValuePair>, 2> fontSettingsRemaining;
BOOL hasPowerlineCharacters = FALSE;
wchar_t localeNameBuffer[LOCALE_NAME_MAX_LENGTH];
const auto localeName = GetUserDefaultLocaleName(localeNameBuffer, LOCALE_NAME_MAX_LENGTH) ? localeNameBuffer : L"en-US";
til::iterate_font_families(fontFaceSpec, [&](wil::zwstring_view name) {
std::wstring* accumulator = nullptr;
try
{
UINT32 index = 0;
BOOL exists = FALSE;
THROW_IF_FAILED(fontCollection->FindFamilyName(name.c_str(), &index, &exists));
// Look ma, no goto!
do
{
if (!exists)
{
accumulator = &missingFonts;
break;
}
wil::com_ptr<IDWriteFontFamily> fontFamily;
THROW_IF_FAILED(fontCollection->GetFontFamily(index, fontFamily.addressof()));
wil::com_ptr<IDWriteFont> font;
THROW_IF_FAILED(fontFamily->GetFirstMatchingFont(DWRITE_FONT_WEIGHT_NORMAL, DWRITE_FONT_STRETCH_NORMAL, DWRITE_FONT_STYLE_NORMAL, font.addressof()));
if (!font.query<IDWriteFont1>()->IsMonospacedFont())
{
accumulator = &proportionalFonts;
}
// We're actually checking for the "Extended" PowerLine glyph set.
// They're more fun.
BOOL hasE0B6 = FALSE;
std::ignore = font->HasCharacter(0xE0B6, &hasE0B6);
hasPowerlineCharacters |= hasE0B6;
wil::com_ptr<IDWriteFontFace> fontFace;
THROW_IF_FAILED(font->CreateFontFace(fontFace.addressof()));
_generateFontAxes(fontFace.get(), localeName, fontSettingsRemaining[FontAxesIndex]);
_generateFontFeatures(fontFace.get(), fontSettingsRemaining[FontFeaturesIndex]);
} while (false);
}
catch (...)
{
accumulator = &missingFonts;
LOG_CAUGHT_EXCEPTION();
}
if (accumulator)
{
if (!accumulator->empty())
{
accumulator->append(L", ");
}
accumulator->append(name);
}
});
// Up to this point, our two vectors are sorted by tag value. We want to sort them by display string now,
// because this will result in sorted fontSettingsUsed/Unused lists below.
for (auto& v : fontSettingsRemaining)
{
std::sort(v.begin(), v.end(), FontKeyValuePair::SortAscending);
}
std::array<std::vector<Editor::FontKeyValuePair>, 2> fontSettingsUsed;
const std::array fontSettingsUser{
_appearance.SourceProfile().FontInfo().FontAxes(),
_appearance.SourceProfile().FontInfo().FontFeatures(),
};
// Find all axes and features that are in the user settings, and move them to the used list.
// They'll be displayed as a list in the UI.
for (int i = FontAxesIndex; i <= FontFeaturesIndex; i++)
{
const auto& map = fontSettingsUser[i];
if (!map)
{
continue;
}
for (const auto& [tagString, value] : fontSettingsUser[i])
{
const auto tag = tagFromString(tagString);
if (!tag)
{
continue;
}
auto& remaining = fontSettingsRemaining[i];
const auto it = std::ranges::find_if(remaining, [&](const Editor::FontKeyValuePair& kv) {
return winrt::get_self<FontKeyValuePair>(kv)->Key() == tag;
});
Editor::FontKeyValuePair kv{ nullptr };
if (it != remaining.end())
{
kv = std::move(*it);
remaining.erase(it);
const auto kvImpl = winrt::get_self<FontKeyValuePair>(kv);
kvImpl->SetValueDirect(value);
}
else
{
kv = winrt::make<FontKeyValuePair>(get_weak(), hstring{}, tag, value, i == FontFeaturesIndex);
}
fontSettingsUsed[i].emplace_back(std::move(kv));
}
}
std::array<std::vector<MenuFlyoutItemBase>, 2> fontSettingsUnused;
// All remaining (= unused) axes and features are turned into menu items.
// They'll be displayed as a flyout when clicking the "add item" button.
for (int i = FontAxesIndex; i <= FontFeaturesIndex; i++)
{
for (const auto& kv : fontSettingsRemaining[i])
{
fontSettingsUnused[i].emplace_back(_createFontSettingMenuItem(kv));
}
}
auto& d = _fontFaceDependents.emplace();
d.missingFontFaces = winrt::hstring{ missingFonts };
d.proportionalFontFaces = winrt::hstring{ proportionalFonts };
d.hasPowerlineCharacters = hasPowerlineCharacters;
d.fontSettingsUsed[FontAxesIndex] = winrt::single_threaded_observable_vector(std::move(fontSettingsUsed[FontAxesIndex]));
d.fontSettingsUsed[FontFeaturesIndex] = winrt::single_threaded_observable_vector(std::move(fontSettingsUsed[FontFeaturesIndex]));
d.fontSettingsUnused = std::move(fontSettingsUnused);
_notifyChangesForFontSettings();
}
std::pair<std::vector<Editor::FontKeyValuePair>::const_iterator, bool> AppearanceViewModel::_fontSettingSortedByKeyInsertPosition(const std::vector<Editor::FontKeyValuePair>& vec, uint32_t key)
{
const auto it = std::lower_bound(vec.begin(), vec.end(), key, [](const Editor::FontKeyValuePair& lhs, uint32_t rhs) {
return winrt::get_self<FontKeyValuePair>(lhs)->Key() < rhs;
});
const auto exists = it != vec.end() && winrt::get_self<FontKeyValuePair>(*it)->Key() == key;
return { it, exists };
}
void AppearanceViewModel::_generateFontAxes(IDWriteFontFace* fontFace, const wchar_t* localeName, std::vector<Editor::FontKeyValuePair>& list)
{
const auto fontFace5 = wil::try_com_query<IDWriteFontFace5>(fontFace);
if (!fontFace5)
{
return;
}
const auto axesCount = fontFace5->GetFontAxisValueCount();
if (axesCount == 0)
{
return;
}
std::vector<DWRITE_FONT_AXIS_VALUE> axesVector(axesCount);
THROW_IF_FAILED(fontFace5->GetFontAxisValues(axesVector.data(), axesCount));
wil::com_ptr<IDWriteFontResource> fontResource;
THROW_IF_FAILED(fontFace5->GetFontResource(fontResource.addressof()));
for (UINT32 i = 0; i < axesCount; ++i)
{
wil::com_ptr<IDWriteLocalizedStrings> names;
THROW_IF_FAILED(fontResource->GetAxisNames(i, names.addressof()));
// As per MSDN:
// > The font author may not have supplied names for some font axes.
// > The localized strings will be empty in that case.
if (names->GetCount() == 0)
{
continue;
}
const auto tag = axesVector[i].axisTag;
const auto [it, tagExists] = _fontSettingSortedByKeyInsertPosition(list, tag);
if (tagExists)
{
continue;
}
UINT32 index;
BOOL exists;
if (FAILED(names->FindLocaleName(localeName, &index, &exists)) || !exists)
{
index = 0;
}
const auto idx = getLocalizedStringIndex(names.get(), localeName, 0);
const auto localizedName = getLocalizedStringByIndex(names.get(), idx);
const auto tagString = tagToString(tag);
const auto displayString = til::hstring_format(FMT_COMPILE(L"{} ({})"), localizedName, std::wstring_view{ tagString });
const auto value = axesVector[i].value;
list.emplace(it, winrt::make<FontKeyValuePair>(get_weak(), std::move(displayString), tag, value, false));
}
}
void AppearanceViewModel::_generateFontFeatures(IDWriteFontFace* fontFace, std::vector<Editor::FontKeyValuePair>& list)
{
wil::com_ptr<IDWriteFactory> factory;
THROW_IF_FAILED(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(factory), reinterpret_cast<::IUnknown**>(factory.addressof())));
wil::com_ptr<IDWriteTextAnalyzer> textAnalyzer;
THROW_IF_FAILED(factory->CreateTextAnalyzer(textAnalyzer.addressof()));
const auto textAnalyzer2 = textAnalyzer.query<IDWriteTextAnalyzer2>();
static constexpr DWRITE_SCRIPT_ANALYSIS scriptAnalysis{};
UINT32 tagCount;
if (textAnalyzer2->GetTypographicFeatures(fontFace, scriptAnalysis, L"en-US", 0, &tagCount, nullptr) != E_NOT_SUFFICIENT_BUFFER)
{
return;
}
std::vector<DWRITE_FONT_FEATURE_TAG> tags{ tagCount };
if (FAILED(textAnalyzer2->GetTypographicFeatures(fontFace, scriptAnalysis, L"en-US", tagCount, &tagCount, tags.data())))
{
return;
}
for (const auto& tag : tags)
{
const auto [it, tagExists] = _fontSettingSortedByKeyInsertPosition(list, tag);
if (tagExists)
{
continue;
}
const auto dfBeg = s_defaultFeatures.begin();
const auto dfEnd = s_defaultFeatures.end();
const auto isDefaultFeature = std::find(dfBeg, dfEnd, tag) != dfEnd;
const auto value = isDefaultFeature ? 1.0f : 0.0f;
list.emplace(it, winrt::make<FontKeyValuePair>(get_weak(), hstring{}, tag, value, true));
}
}
MenuFlyoutItemBase AppearanceViewModel::_createFontSettingMenuItem(const Editor::FontKeyValuePair& kv)
{
const auto kvImpl = winrt::get_self<FontKeyValuePair>(kv);
MenuFlyoutItem item;
item.Text(kvImpl->KeyDisplayStringRef());
item.Click([weakSelf = get_weak(), kv](const IInspectable& sender, const RoutedEventArgs&) {
if (const auto self = weakSelf.get())
{
self->AddFontKeyValuePair(sender, kv);
}
});
return item;
}
// Call this when all the _fontFaceDependents members have changed.
void AppearanceViewModel::_notifyChangesForFontSettings()
{
_NotifyChanges(L"FontFaceDependents");
_NotifyChanges(L"FontAxes");
_NotifyChanges(L"FontFeatures");
_NotifyChanges(L"HasFontAxes");
_NotifyChanges(L"HasFontFeatures");
}
// Call this when used items moved into unused and vice versa.
// Because this doesn't recreate the IObservableVector instances,
// we don't need to notify the UI about changes to the "FontAxes" property.
void AppearanceViewModel::_notifyChangesForFontSettingsReactive(FontSettingIndex fontSettingsIndex)
{
_NotifyChanges(L"FontFaceDependents");
switch (fontSettingsIndex)
{
case FontAxesIndex:
_NotifyChanges(L"HasFontAxes");
break;
case FontFeaturesIndex:
_NotifyChanges(L"HasFontFeatures");
break;
default:
break;
}
}
double AppearanceViewModel::_parseCellSizeValue(const hstring& val) const
{
const auto str = val.c_str();
auto& errnoRef = errno; // Nonzero cost, pay it once.
errnoRef = 0;
wchar_t* end;
const auto value = std::wcstod(str, &end);
return str == end || errnoRef == ERANGE ? NAN : value;
}
double AppearanceViewModel::LineHeight() const
{
const auto cellHeight = _appearance.SourceProfile().FontInfo().CellHeight();
return _parseCellSizeValue(cellHeight);
}
double AppearanceViewModel::CellWidth() const
{
const auto cellWidth = _appearance.SourceProfile().FontInfo().CellWidth();
return _parseCellSizeValue(cellWidth);
}
#define CELL_SIZE_SETTER(modelName, viewModelName) \
std::wstring str; \
\
if (value >= 0.1 && value <= 10.0) \
{ \
str = fmt::format(FMT_COMPILE(L"{:.6g}"), value); \
} \
\
const auto fontInfo = _appearance.SourceProfile().FontInfo(); \
\
if (fontInfo.modelName() != str) \
{ \
if (str.empty()) \
{ \
fontInfo.Clear##modelName(); \
} \
else \
{ \
fontInfo.modelName(str); \
} \
_NotifyChanges(L"Has" #viewModelName, L## #viewModelName); \
}
void AppearanceViewModel::LineHeight(const double value)
{
CELL_SIZE_SETTER(CellHeight, LineHeight);
}
void AppearanceViewModel::CellWidth(const double value)
{
CELL_SIZE_SETTER(CellWidth, CellWidth);
}
bool AppearanceViewModel::HasLineHeight() const
{
const auto fontInfo = _appearance.SourceProfile().FontInfo();
return fontInfo.HasCellHeight();
}
bool AppearanceViewModel::HasCellWidth() const
{
const auto fontInfo = _appearance.SourceProfile().FontInfo();
return fontInfo.HasCellWidth();
}
void AppearanceViewModel::ClearLineHeight()
{
LineHeight(NAN);
}
void AppearanceViewModel::ClearCellWidth()
{
CellWidth(NAN);
}
Model::FontConfig AppearanceViewModel::LineHeightOverrideSource() const
{
const auto fontInfo = _appearance.SourceProfile().FontInfo();
return fontInfo.CellHeightOverrideSource();
}
Model::FontConfig AppearanceViewModel::CellWidthOverrideSource() const
{
const auto fontInfo = _appearance.SourceProfile().FontInfo();
return fontInfo.CellWidthOverrideSource();
}
void AppearanceViewModel::SetFontWeightFromDouble(double fontWeight)
{
FontWeight(winrt::Microsoft::Terminal::UI::Converters::DoubleToFontWeight(fontWeight));
}
const AppearanceViewModel::FontFaceDependentsData& AppearanceViewModel::FontFaceDependents()
{
if (!_fontFaceDependents)
{
_refreshFontFaceDependents();
}
return *_fontFaceDependents;
}
winrt::hstring AppearanceViewModel::MissingFontFaces()
{
return FontFaceDependents().missingFontFaces;
}
winrt::hstring AppearanceViewModel::ProportionalFontFaces()
{
return FontFaceDependents().proportionalFontFaces;
}
bool AppearanceViewModel::HasPowerlineCharacters()
{
return FontFaceDependents().hasPowerlineCharacters;
}
IObservableVector<Editor::FontKeyValuePair> AppearanceViewModel::FontAxes()
{
return FontFaceDependents().fontSettingsUsed[FontAxesIndex];
}
bool AppearanceViewModel::HasFontAxes() const
{
return _appearance.SourceProfile().FontInfo().HasFontAxes();
}
void AppearanceViewModel::ClearFontAxes()
{
_deleteAllFontKeyValuePairs(FontAxesIndex);
}
Model::FontConfig AppearanceViewModel::FontAxesOverrideSource() const
{
return _appearance.SourceProfile().FontInfo().FontAxesOverrideSource();
}
IObservableVector<Editor::FontKeyValuePair> AppearanceViewModel::FontFeatures()
{
return FontFaceDependents().fontSettingsUsed[FontFeaturesIndex];
}
bool AppearanceViewModel::HasFontFeatures() const
{
return _appearance.SourceProfile().FontInfo().HasFontFeatures();
}
void AppearanceViewModel::ClearFontFeatures()
{
_deleteAllFontKeyValuePairs(FontFeaturesIndex);
}
Model::FontConfig AppearanceViewModel::FontFeaturesOverrideSource() const
{
return _appearance.SourceProfile().FontInfo().FontFeaturesOverrideSource();
}
void AppearanceViewModel::AddFontKeyValuePair(const IInspectable& sender, const Editor::FontKeyValuePair& kv)
{
if (!_fontFaceDependents)
{
return;
}
const auto kvImpl = winrt::get_self<FontKeyValuePair>(kv);
const auto fontSettingsIndex = kvImpl->IsFontFeature() ? FontFeaturesIndex : FontAxesIndex;
auto& d = *_fontFaceDependents;
auto& used = d.fontSettingsUsed[fontSettingsIndex];
auto& unused = d.fontSettingsUnused[fontSettingsIndex];
const auto it = std::ranges::find(unused, sender);
if (it == unused.end())
{
return;
}
// Sync the added value into the user settings model.
UpdateFontSetting(kvImpl);
// Insert the item into the used list, keeping it sorted by the display text.
{
const auto it = std::lower_bound(used.begin(), used.end(), kv, FontKeyValuePair::SortAscending);
used.InsertAt(gsl::narrow<uint32_t>(it - used.begin()), kv);
}
unused.erase(it);
_notifyChangesForFontSettingsReactive(fontSettingsIndex);
}
void AppearanceViewModel::DeleteFontKeyValuePair(const Editor::FontKeyValuePair& kv)
{
if (!_fontFaceDependents)
{
return;
}
const auto kvImpl = winrt::get_self<FontKeyValuePair>(kv);
const auto tag = kvImpl->Key();
const auto tagString = tagToString(tag);
const auto fontSettingsIndex = kvImpl->IsFontFeature() ? FontFeaturesIndex : FontAxesIndex;
auto& d = *_fontFaceDependents;
auto& used = d.fontSettingsUsed[fontSettingsIndex];
const auto fontInfo = _appearance.SourceProfile().FontInfo();
auto fontSettingsUser = kvImpl->IsFontFeature() ? fontInfo.FontFeatures() : fontInfo.FontAxes();
if (!fontSettingsUser)
{
return;
}
const auto it = std::ranges::find(used, kv);
if (it == used.end())
{
return;
}
fontSettingsUser.Remove(std::wstring_view{ tagString });
_addMenuFlyoutItemToUnused(fontSettingsIndex, _createFontSettingMenuItem(*it));
used.RemoveAt(gsl::narrow<uint32_t>(it - used.begin()));
_notifyChangesForFontSettingsReactive(fontSettingsIndex);
}
void AppearanceViewModel::_deleteAllFontKeyValuePairs(FontSettingIndex fontSettingsIndex)
{
const auto fontInfo = _appearance.SourceProfile().FontInfo();
if (fontSettingsIndex == FontFeaturesIndex)
{
fontInfo.ClearFontFeatures();
}
else
{
fontInfo.ClearFontAxes();
}
if (!_fontFaceDependents)
{
return;
}
auto& d = *_fontFaceDependents;
auto& used = d.fontSettingsUsed[fontSettingsIndex];
for (const auto& kv : used)
{
_addMenuFlyoutItemToUnused(fontSettingsIndex, _createFontSettingMenuItem(kv));
}
used.Clear();
_notifyChangesForFontSettingsReactive(fontSettingsIndex);
}
// Inserts the given menu item into the unused list, while keeping it sorted by the display text.
void AppearanceViewModel::_addMenuFlyoutItemToUnused(FontSettingIndex index, MenuFlyoutItemBase item)
{
if (!_fontFaceDependents)
{
return;
}
auto& d = *_fontFaceDependents;
auto& unused = d.fontSettingsUnused[index];
const auto it = std::lower_bound(unused.begin(), unused.end(), item, [](const MenuFlyoutItemBase& lhs, const MenuFlyoutItemBase& rhs) {
const auto& a = lhs.as<MenuFlyoutItem>().Text();
const auto& b = rhs.as<MenuFlyoutItem>().Text();
return til::compare_linguistic_insensitive(a, b) < 0;
});
unused.insert(it, std::move(item));
}
void AppearanceViewModel::UpdateFontSetting(const FontKeyValuePair* kvImpl)
{
const auto tag = kvImpl->Key();
const auto value = kvImpl->Value();
const auto tagString = tagToString(tag);
const auto fontInfo = _appearance.SourceProfile().FontInfo();
auto fontSettingsUser = kvImpl->IsFontFeature() ? fontInfo.FontFeatures() : fontInfo.FontAxes();
if (!fontSettingsUser)
{
fontSettingsUser = winrt::single_threaded_map<hstring, float>();
if (kvImpl->IsFontFeature())
{
fontInfo.FontFeatures(fontSettingsUser);
}
else
{
fontInfo.FontAxes(fontSettingsUser);
}
}
std::ignore = fontSettingsUser.Insert(std::wstring_view{ tagString }, value);
// Pwease call Profiles_Appearance::_onProfilePropertyChanged to make the pweview connyection wewoad. Thanks!! uwu
// ...I hate this.
_NotifyChanges(L"uwu");
}
void AppearanceViewModel::SetBackgroundImageOpacityFromPercentageValue(double percentageValue)
{
BackgroundImageOpacity(static_cast<float>(percentageValue) / 100.0f);
}
void AppearanceViewModel::SetBackgroundImagePath(winrt::hstring path)
{
_appearance.BackgroundImagePath(Model::MediaResourceHelper::FromString(path));
_NotifyChanges(L"BackgroundImagePath");
}
hstring AppearanceViewModel::BackgroundImageAlignmentCurrentValue() const
{
const auto alignment = BackgroundImageAlignment();
hstring alignmentResourceKey = L"Profile_BackgroundImageAlignment";
if (alignment == (ConvergedAlignment::Vertical_Center | ConvergedAlignment::Horizontal_Center))
{
alignmentResourceKey = alignmentResourceKey + L"Center";
}
else
{
// Append vertical alignment to the resource key
switch (alignment & static_cast<ConvergedAlignment>(0xF0))
{
case ConvergedAlignment::Vertical_Bottom:
alignmentResourceKey = alignmentResourceKey + L"Bottom";
break;
case ConvergedAlignment::Vertical_Top:
alignmentResourceKey = alignmentResourceKey + L"Top";
break;
}
// Append horizontal alignment to the resource key
switch (alignment & static_cast<ConvergedAlignment>(0x0F))
{
case ConvergedAlignment::Horizontal_Left:
alignmentResourceKey = alignmentResourceKey + L"Left";
break;
case ConvergedAlignment::Horizontal_Right:
alignmentResourceKey = alignmentResourceKey + L"Right";
break;
}
}
alignmentResourceKey = alignmentResourceKey + L"/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip";
// We can't use the RS_ macro here because the resource key is dynamic
return GetLibraryResourceString(alignmentResourceKey);
}
hstring AppearanceViewModel::CurrentBackgroundImagePath() const
{
const auto bgImagePath = BackgroundImagePath().Path();
if (bgImagePath.empty())
{
return RS_(L"Appearance_BackgroundImageNone");
}
else if (bgImagePath == L"desktopWallpaper")
{
return RS_(L"Profile_UseDesktopImage/Content");
}
return bgImagePath;
}
bool AppearanceViewModel::UseDesktopBGImage() const
{
return BackgroundImagePath().Path() == L"desktopWallpaper";
}
void AppearanceViewModel::UseDesktopBGImage(const bool useDesktop)
{
if (useDesktop)
{
// Stash the current value of BackgroundImagePath. If the user
// checks and un-checks the "Use desktop wallpaper" button, we want
// the path that we display in the text box to remain unchanged.
//
// Only stash this value if it's not the special "desktopWallpaper"
// value.
if (BackgroundImagePath().Path() != L"desktopWallpaper")
{
_lastBgImagePath = BackgroundImagePath().Path();
}
SetBackgroundImagePath(L"desktopWallpaper");
}
else
{
// Restore the path we had previously cached. This might be the
// empty string.
SetBackgroundImagePath(_lastBgImagePath);
}
}
bool AppearanceViewModel::BackgroundImageSettingsVisible() const
{
return !BackgroundImagePath().Path().empty();
}
void AppearanceViewModel::ClearColorScheme()
{
ClearDarkColorSchemeName();
_NotifyChanges(L"CurrentColorScheme");
}
Editor::ColorSchemeViewModel AppearanceViewModel::CurrentColorScheme() const
{
const auto schemeName{ DarkColorSchemeName() };
const auto allSchemes{ SchemesList() };
for (const auto& scheme : allSchemes)
{
if (scheme.Name() == schemeName)
{
return scheme;
}
}
// This Appearance points to a color scheme that was renamed or deleted.
// Fallback to the first one in the list.
return allSchemes.GetAt(0);
}
void AppearanceViewModel::CurrentColorScheme(const ColorSchemeViewModel& val)
{
DarkColorSchemeName(val.Name());
LightColorSchemeName(val.Name());
}
static inline Windows::UI::Color _getColorPreview(const IReference<Microsoft::Terminal::Core::Color>& modelVal, Windows::UI::Color deducedVal)
{
if (modelVal)
{
// user defined an override value
return Windows::UI::Color{
.A = 255,
.R = modelVal.Value().R,
.G = modelVal.Value().G,
.B = modelVal.Value().B
};
}
// set to null --> deduce value from color scheme
return deducedVal;
}
Windows::UI::Color AppearanceViewModel::ForegroundPreview() const
{
return _getColorPreview(_appearance.Foreground(), CurrentColorScheme().ForegroundColor().Color());
}
Windows::UI::Color AppearanceViewModel::BackgroundPreview() const
{
return _getColorPreview(_appearance.Background(), CurrentColorScheme().BackgroundColor().Color());
}
Windows::UI::Color AppearanceViewModel::SelectionBackgroundPreview() const
{
return _getColorPreview(_appearance.SelectionBackground(), CurrentColorScheme().SelectionBackgroundColor().Color());
}
Windows::UI::Color AppearanceViewModel::CursorColorPreview() const
{
return _getColorPreview(_appearance.CursorColor(), CurrentColorScheme().CursorColor().Color());
}
DependencyProperty Appearances::_AppearanceProperty{ nullptr };
Appearances::Appearances()
{
InitializeComponent();
{
using namespace winrt::Windows::Globalization::NumberFormatting;
// > .NET rounds to 12 significant digits when displaying doubles, so we will [...]
// ...obviously not do that, because this is an UI element for humans. This prevents
// issues when displaying 32-bit floats, because WinUI is unaware about their existence.
IncrementNumberRounder rounder;
rounder.Increment(1e-6);
for (const auto& box : { _fontSizeBox(), _lineHeightBox() })
{
// BODGY: Depends on WinUI internals.
box.NumberFormatter().as<DecimalFormatter>().NumberRounder(rounder);
}
}
INITIALIZE_BINDABLE_ENUM_SETTING(CursorShape, CursorStyle, winrt::Microsoft::Terminal::Core::CursorStyle, L"Profile_CursorShape", L"Content");
INITIALIZE_BINDABLE_ENUM_SETTING(AdjustIndistinguishableColors, AdjustIndistinguishableColors, winrt::Microsoft::Terminal::Core::AdjustTextMode, L"Profile_AdjustIndistinguishableColors", L"Content");
INITIALIZE_BINDABLE_ENUM_SETTING_REVERSE_ORDER(BackgroundImageStretchMode, BackgroundImageStretchMode, winrt::Windows::UI::Xaml::Media::Stretch, L"Profile_BackgroundImageStretchMode", L"Content");
// manually add Custom FontWeight option. Don't add it to the Map
INITIALIZE_BINDABLE_ENUM_SETTING(FontWeight, FontWeight, uint16_t, L"Profile_FontWeight", L"Content");
_CustomFontWeight = winrt::make<EnumEntry>(RS_(L"Profile_FontWeightCustom/Content"), winrt::box_value<uint16_t>(0u));
_FontWeightList.Append(_CustomFontWeight);
if (!_AppearanceProperty)
{
_AppearanceProperty =
DependencyProperty::Register(
L"Appearance",
xaml_typename<Editor::AppearanceViewModel>(),
xaml_typename<Editor::Appearances>(),
PropertyMetadata{ nullptr, PropertyChangedCallback{ &Appearances::_ViewModelChanged } });
}
// manually keep track of all the Background Image Alignment buttons
_BIAlignmentButtons.at(0) = BIAlign_TopLeft();
_BIAlignmentButtons.at(1) = BIAlign_Top();
_BIAlignmentButtons.at(2) = BIAlign_TopRight();
_BIAlignmentButtons.at(3) = BIAlign_Left();
_BIAlignmentButtons.at(4) = BIAlign_Center();
_BIAlignmentButtons.at(5) = BIAlign_Right();
_BIAlignmentButtons.at(6) = BIAlign_BottomLeft();
_BIAlignmentButtons.at(7) = BIAlign_Bottom();
_BIAlignmentButtons.at(8) = BIAlign_BottomRight();
// apply automation properties to more complex setting controls
for (const auto& biButton : _BIAlignmentButtons)
{
const auto tooltip{ ToolTipService::GetToolTip(biButton) };
Automation::AutomationProperties::SetName(biButton, unbox_value<hstring>(tooltip));
}
const auto showAllFontsCheckboxTooltip{ ToolTipService::GetToolTip(ShowAllFontsCheckbox()) };
Automation::AutomationProperties::SetFullDescription(ShowAllFontsCheckbox(), unbox_value<hstring>(showAllFontsCheckboxTooltip));
const auto backgroundImgCheckboxTooltip{ ToolTipService::GetToolTip(UseDesktopImageCheckBox()) };
Automation::AutomationProperties::SetFullDescription(UseDesktopImageCheckBox(), unbox_value<hstring>(backgroundImgCheckboxTooltip));
INITIALIZE_BINDABLE_ENUM_SETTING(IntenseTextStyle, IntenseTextStyle, winrt::Microsoft::Terminal::Settings::Model::IntenseStyle, L"Appearance_IntenseTextStyle", L"Content");
}
IObservableVector<Editor::Font> Appearances::FilteredFontList()
{
if (!_filteredFonts)
{
_updateFilteredFontList();
}
return _filteredFonts;
}
// Method Description:
// - Determines whether we should show the list of all the fonts, or we should just show monospace fonts
bool Appearances::ShowAllFonts() const noexcept
{
return _ShowAllFonts;
}
void Appearances::ShowAllFonts(const bool value)
{
if (_ShowAllFonts != value)
{
_ShowAllFonts = value;
_filteredFonts = nullptr;
PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"ShowAllFonts" });
PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"FilteredFontList" });
}
}
void Appearances::FontFaceBox_GotFocus(const Windows::Foundation::IInspectable& sender, const RoutedEventArgs&)
{
_updateFontNameFilter({});
sender.as<AutoSuggestBox>().IsSuggestionListOpen(true);
}
void Appearances::FontFaceBox_LostFocus(const IInspectable& sender, const RoutedEventArgs&)
{
_updateFontName(sender.as<AutoSuggestBox>().Text());
}
void Appearances::FontFaceBox_QuerySubmitted(const AutoSuggestBox& sender, const AutoSuggestBoxQuerySubmittedEventArgs& args)
{
// When pressing Enter within the input line, this callback will be invoked with no suggestion.
const auto font = unbox_value_or<Editor::Font>(args.ChosenSuggestion(), nullptr);
if (!font)
{
return;
}
const auto fontName = font.Name();
auto fontSpec = sender.Text();
const std::wstring_view fontSpecView{ fontSpec };
if (const auto idx = fontSpecView.rfind(L','); idx != std::wstring_view::npos)
{
const auto prefix = fontSpecView.substr(0, idx);
const auto suffix = std::wstring_view{ fontName };
fontSpec = til::hstring_format(FMT_COMPILE(L"{}, {}"), prefix, suffix);
}
else
{
fontSpec = fontName;
}
sender.Text(fontSpec);
// Normally we'd just update the model property in LostFocus above, but because WinUI is the Ralph Wiggum
// among the UI frameworks, it raises the LostFocus event _before_ the QuerySubmitted event.
// So, when you press Save, the model will have the wrong font face string, because LostFocus was raised too early.
// Also, this causes the first tab in the application to be focused, so when you press Enter it'll switch tabs.
//
// You can't just assign focus back to the AutoSuggestBox, because the FocusState() within the GotFocus event handler
// contains random values. This prevents us from avoiding the IsSuggestionListOpen(true) in our GotFocus event handler.
// You can't just do IsSuggestionListOpen(false) either, because you can show the list with that property but not hide it.
// So, we update the model manually and assign focus to the parent container.
//
// BUT you can't just focus the parent container, because of a weird interaction with AutoSuggestBox where it'll refuse to lose
// focus if you picked a suggestion that matches the current fontSpec. So, we unfocus it first and then focus the parent container.
_updateFontName(fontSpec);
sender.Focus(FocusState::Unfocused);
FontFaceContainer().Focus(FocusState::Programmatic);
}
void Appearances::FontFaceBox_TextChanged(const AutoSuggestBox& sender, const AutoSuggestBoxTextChangedEventArgs& args)
{
if (args.Reason() != AutoSuggestionBoxTextChangeReason::UserInput)
{
return;
}
const auto fontSpec = sender.Text();
std::wstring_view filter{ fontSpec };
// Find the last font name in the font, spec, list.
if (const auto idx = filter.rfind(L','); idx != std::wstring_view::npos)
{
filter = filter.substr(idx + 1);
}
filter = til::trim(filter, L' ');
_updateFontNameFilter(filter);
}
void Appearances::_updateFontName(hstring fontSpec)
{
const auto appearance = Appearance();
if (fontSpec.empty())
{
appearance.ClearFontFace();
}
else
{
appearance.FontFace(std::move(fontSpec));
}
}
void Appearances::_updateFontNameFilter(std::wstring_view filter)
{
if (_fontNameFilter != filter)
{
_filteredFonts = nullptr;
_fontNameFilter = filter;
PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"FilteredFontList" });
}
}
void Appearances::_updateFilteredFontList()
{
_filteredFonts = _ShowAllFonts ? ProfileViewModel::CompleteFontList() : ProfileViewModel::MonospaceFontList();
if (_fontNameFilter.empty())
{
return;
}
std::vector<Editor::Font> filtered;
filtered.reserve(_filteredFonts.Size());
for (const auto& font : _filteredFonts)
{
const auto name = font.Name();
bool match = til::contains_linguistic_insensitive(name, _fontNameFilter);
if (!match)
{
const auto localizedName = font.LocalizedName();
match = localizedName != name && til::contains_linguistic_insensitive(localizedName, _fontNameFilter);
}
if (match)
{
filtered.emplace_back(font);
}
}
_filteredFonts = winrt::single_threaded_observable_vector(std::move(filtered));
}
void Appearances::_ViewModelChanged(const DependencyObject& d, const DependencyPropertyChangedEventArgs& /*args*/)
{
const auto& obj{ d.as<Editor::Appearances>() };
get_self<Appearances>(obj)->_UpdateWithNewViewModel();
}
void Appearances::_UpdateWithNewViewModel()
{
if (const auto appearance = Appearance())
{
const auto appearanceImpl = winrt::get_self<AppearanceViewModel>(appearance);
const auto& biAlignmentVal{ static_cast<int32_t>(appearanceImpl->BackgroundImageAlignment()) };
for (const auto& biButton : _BIAlignmentButtons)
{
biButton.IsChecked(biButton.Tag().as<int32_t>() == biAlignmentVal);
}
{
const auto& d = appearanceImpl->FontFaceDependents();
const std::array buttons{
AddFontAxisButton(),
AddFontFeatureButton(),
};
for (int i = 0; i < 2; ++i)
{
const auto& button = buttons[i];
const auto& data = d.fontSettingsUnused[i];
const auto items = button.Flyout().as<MenuFlyout>().Items();
items.ReplaceAll(data);
button.IsEnabled(!data.empty());
}
}
_ViewModelChangedRevoker = appearance.PropertyChanged(winrt::auto_revoke, [=](auto&&, const PropertyChangedEventArgs& args) {
const auto settingName{ args.PropertyName() };
if (settingName == L"CursorShape")
{
PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"CurrentCursorShape" });
PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"IsVintageCursor" });
}
else if (settingName == L"DarkColorSchemeName" || settingName == L"LightColorSchemeName")
{
PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"CurrentColorScheme" });
}
else if (settingName == L"BackgroundImageStretchMode")
{
PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"CurrentBackgroundImageStretchMode" });
}
else if (settingName == L"BackgroundImageAlignment")
{
_UpdateBIAlignmentControl(static_cast<int32_t>(appearanceImpl->BackgroundImageAlignment()));
}
else if (settingName == L"FontWeight")
{
PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"CurrentFontWeight" });
PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"IsCustomFontWeight" });
}
else if (settingName == L"FontFaceDependents")
{
const auto& d = appearanceImpl->FontFaceDependents();
const std::array buttons{
AddFontAxisButton(),
AddFontFeatureButton(),
};
for (int i = 0; i < 2; ++i)
{
const auto& button = buttons[i];
const auto& data = d.fontSettingsUnused[i];
const auto flyout = button.Flyout().as<MenuFlyout>();
const auto items = flyout.Items();
items.ReplaceAll(data);
button.IsEnabled(!data.empty());
// WinUI doesn't hide the flyout when it's currently open and the items are now empty.
// In fact it doesn't close it, period. You click an item? Flyout stays open!
// This gets called whenever an item is selected, so it's the "perfect" time to close it.
flyout.Hide();
}
}
else if (settingName == L"IntenseTextStyle")
{
PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"CurrentIntenseTextStyle" });
}
else if (settingName == L"AdjustIndistinguishableColors")
{
PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"CurrentAdjustIndistinguishableColors" });
}
else if (settingName == L"ShowProportionalFontWarning")
{
PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"ShowProportionalFontWarning" });
}
// YOU THERE ADDING A NEW APPEARANCE SETTING
// Make sure you add a block like
//
// else if (settingName == L"MyNewSetting")
// {
// PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"CurrentMyNewSetting" });
// }
//
// To make sure that changes to the AppearanceViewModel will
// propagate back up to the actual UI (in Appearances). The
// CurrentMyNewSetting properties are the ones that are bound in
// XAML. If you don't do this right (or only raise a property
// changed for "MyNewSetting"), then things like the reset
// button won't work right.
});
// make sure to send all the property changed events once here
// we do this in the case an old appearance was deleted and then a new one is created,
// the old settings need to be updated in xaml
PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"CurrentCursorShape" });
PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"IsVintageCursor" });
PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"CurrentColorScheme" });
PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"CurrentBackgroundImageStretchMode" });
_UpdateBIAlignmentControl(static_cast<int32_t>(appearance.BackgroundImageAlignment()));
PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"CurrentFontWeight" });
PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"IsCustomFontWeight" });
PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"ShowAllFonts" });
PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"CurrentIntenseTextStyle" });
PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"CurrentAdjustIndistinguishableColors" });
PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"ShowProportionalFontWarning" });
}
}
safe_void_coroutine Appearances::BackgroundImage_Click(const IInspectable&, const RoutedEventArgs&)
{
const auto lifetime = get_strong();
const auto windowRoot = WindowRoot();
if (!windowRoot)
{
co_return;
}
const auto parentHwnd{ reinterpret_cast<HWND>(windowRoot.GetHostingWindow()) };
const auto file = co_await OpenImagePicker(parentHwnd);
if (!file.empty())
{
Appearance().SetBackgroundImagePath(file);
}
}
void Appearances::BIAlignment_Click(const IInspectable& sender, const RoutedEventArgs& /*e*/)
{
if (const auto& button{ sender.try_as<Primitives::ToggleButton>() })
{
if (const auto& tag{ button.Tag().try_as<int32_t>() })
{
// Update the Appearance's value and the control
Appearance().BackgroundImageAlignment(static_cast<ConvergedAlignment>(*tag));
_UpdateBIAlignmentControl(*tag);
}
}
}
// Method Description:
// - Resets all of the buttons to unchecked, and checks the one with the provided tag
// Arguments:
// - val - the background image alignment (ConvergedAlignment) that we want to represent in the control
void Appearances::_UpdateBIAlignmentControl(const int32_t val)
{
for (const auto& biButton : _BIAlignmentButtons)
{
if (const auto& biButtonAlignment{ biButton.Tag().try_as<int32_t>() })
{
biButton.IsChecked(biButtonAlignment == val);
}
}
}
void Appearances::DeleteFontKeyValuePair_Click(const IInspectable& sender, const RoutedEventArgs& /*e*/)
{
const auto element = sender.as<FrameworkElement>();
const auto tag = element.Tag();
const auto kv = tag.as<Editor::FontKeyValuePair>();
winrt::get_self<AppearanceViewModel>(Appearance())->DeleteFontKeyValuePair(kv);
}
bool Appearances::IsVintageCursor() const
{
return Appearance().CursorShape() == Core::CursorStyle::Vintage;
}
IInspectable Appearances::CurrentFontWeight() const
{
// if no value was found, we have a custom value
const auto maybeEnumEntry{ _FontWeightMap.TryLookup(Appearance().FontWeight().Weight) };
return maybeEnumEntry ? maybeEnumEntry : _CustomFontWeight;
}
void Appearances::CurrentFontWeight(const IInspectable& enumEntry)
{
if (auto ee = enumEntry.try_as<Editor::EnumEntry>())
{
if (ee != _CustomFontWeight)
{
const auto weight{ winrt::unbox_value<uint16_t>(ee.EnumValue()) };
const Windows::UI::Text::FontWeight setting{ weight };
Appearance().FontWeight(setting);
// Appearance does not have observable properties
// So the TwoWay binding doesn't update on the State --> Slider direction
FontWeightSlider().Value(weight);
}
PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"IsCustomFontWeight" });
}
}
bool Appearances::IsCustomFontWeight()
{
// Use SelectedItem instead of CurrentFontWeight.
// CurrentFontWeight converts the Appearance's value to the appropriate enum entry,
// whereas SelectedItem identifies which one was selected by the user.
return FontWeightComboBox().SelectedItem() == _CustomFontWeight;
}
}