mirror of
https://github.com/microsoft/terminal.git
synced 2026-07-08 18:16:28 +00:00
## Summary Fixes the font face `AutoSuggestBox` resetting to its previous value, or stale filter text, after a keyboard-selected font is committed and focus later leaves the control. WinUI restores its cached user query when the suggestion list closes. The existing `LostFocus` path then treated that programmatic restoration as fresh user input and committed the stale value. This change distinguishes genuine user edits from programmatic text restoration, preserving the committed font value while retaining the queued selected-suggestion commit introduced by #20255. Fixes #20353. ## Validation Manually validated with a local `CascadiaPackage` Debug x64 build: - Reproduced the reset before the fix. - Selected `Lucida Console` from `Consolas` using keyboard navigation. - Pressed Enter and confirmed `Lucida Console` was applied. - Returned focus to the Font face control and tabbed out again. - Verified `Lucida Console` remained selected. - Verified stale filter text did not return. - Verified the #20245 crash did not regress. - Verified Save and Discard changes both work.
1561 lines
58 KiB
C++
1561 lines
58 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::Core;
|
|
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 to which the view refers.)
|
|
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");
|
|
}
|
|
|
|
// Appearances doesn't implement HasScrollViewer<T> which normally adds this function.
|
|
void Appearances::BringIntoViewWhenLoaded(hstring elementToFocus)
|
|
{
|
|
if (elementToFocus.empty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
_loadedRevoker = this->Loaded(winrt::auto_revoke, [weakThis{ get_weak() }, elementToFocus](auto&&, auto&&) {
|
|
if (const auto strongThis = weakThis.get())
|
|
{
|
|
if (const auto& controlToFocus{ strongThis->FindName(elementToFocus).try_as<Controls::Control>() })
|
|
{
|
|
controlToFocus.as<FrameworkElement>().StartBringIntoView();
|
|
controlToFocus.Focus(FocusState::Programmatic);
|
|
}
|
|
strongThis->_loadedRevoker.revoke();
|
|
}
|
|
});
|
|
}
|
|
|
|
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&)
|
|
{
|
|
const auto box = sender.as<AutoSuggestBox>();
|
|
_updateFontNameFilter({});
|
|
box.IsSuggestionListOpen(true);
|
|
_fontFaceBoxHasUserInput = false;
|
|
}
|
|
|
|
void Appearances::FontFaceBox_LostFocus(const IInspectable& sender, const RoutedEventArgs&)
|
|
{
|
|
const auto box = sender.as<AutoSuggestBox>();
|
|
if (_fontFaceBoxHasUserInput)
|
|
{
|
|
_updateFontName(box.Text());
|
|
}
|
|
else
|
|
{
|
|
// AutoSuggestBox restores its cached user query when Tab closes the suggestion list.
|
|
// Programmatic Text updates don't synchronize that cache, so restore the committed value.
|
|
box.Text(Appearance().FontFace());
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// 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.
|
|
//
|
|
// Queue the selected-suggestion commit so AutoSuggestBox can finish processing Enter before we change its text/model.
|
|
// Do not manually unfocus the AutoSuggestBox here. Its Focus(FocusState::Unfocused) path crashes during keyboard commits.
|
|
Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [weakThis{ get_weak() }, weakSender{ winrt::make_weak(sender) }, fontSpec{ std::move(fontSpec) }]() {
|
|
if (const auto self{ weakThis.get() })
|
|
{
|
|
self->_fontFaceBoxHasUserInput = false;
|
|
|
|
if (const auto box{ weakSender.get() })
|
|
{
|
|
box.Text(fontSpec);
|
|
}
|
|
|
|
self->_updateFontName(fontSpec);
|
|
self->FontFaceContainer().Focus(FocusState::Programmatic);
|
|
}
|
|
});
|
|
}
|
|
|
|
void Appearances::FontFaceBox_TextChanged(const AutoSuggestBox& sender, const AutoSuggestBoxTextChangedEventArgs& args)
|
|
{
|
|
if (args.Reason() != AutoSuggestionBoxTextChangeReason::UserInput)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_fontFaceBoxHasUserInput = true;
|
|
|
|
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 so that if an old appearance was deleted and then a new one created,
|
|
// the old settings are 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;
|
|
}
|
|
|
|
}
|