mirror of
https://github.com/microsoft/terminal.git
synced 2026-02-04 01:04:40 +00:00
## 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
1516 lines
57 KiB
C++
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;
|
|
}
|
|
|
|
}
|