d57fb84557
This pull request brings back the "Base Layer" page, now renamed to "Defaults", and the "Reset to inherited value" buttons. The scope of inheritance for which buttons will display has been widened. The button will be visible in the following cases: The user has set a setting for the current profile, and it overrides... 1. ... something in profiles.defaults. 2. ... something in a Fragment Extension profile. 3. ... something from a Dynamic Profile Generator. 4. ... something from the compiled-in defaults. Compared to the original implementation of reset arrows, cases (1), (3) and (4) are new. Rationale: (1) The user can see a setting on the Defaults page, and they need a way to reset back to it. (3) Dynamic profiles are not meaningfully different from fragments, and users may need a way to reset back to the default value generated for WSL or PowerShell. (4) The user can see a setting on the Defaults page, **BUT** they are not the one who created it. They *still* need a way to get back to it. To support this, I've introduced another origin tag, "User", and renamed "Custom" to "None". Due to the way origin/override detection works¹, we cannot otherwise disambiguate between settings that came from the user and settings that came from the compiled-in defaults. Changes were required in TerminalSettings such that we could construct a settings object with a profile that does not have a GUID. In making this change, I fixed a bit of silliness where we took a profile, extracted its guid, and used that guid to look up the same profile object. Oops. I also fixed the PropertyChanged notifier to include the XxxOverrideSource property. The presence of the page and the reset arrows is restricted to Preview- or Dev-branded builds. Stable builds will retain their current behavior. ¹ `XxxOverrideSource` returns the profile *above* the current profile that holds a value for setting `Xxx`. When the value is the compiled-in value, `XxxOverrideSource` will be `null`. Since it's supposed to be the profile above the current profile, it will also be `null` if the profile contains a setting at this layer. In short, `null` means "user specified" *or* "compiled in". Oops. Fixes #10430 Validation ---------- * [x] Tested Release build to make sure it's mostly arrow-free (apart from fragments)
217 lines
8.9 KiB
C++
217 lines
8.9 KiB
C++
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT license.
|
|
|
|
#include "pch.h"
|
|
#include "SettingContainer.h"
|
|
#include "SettingContainer.g.cpp"
|
|
#include "LibraryResources.h"
|
|
|
|
using namespace winrt::Windows::UI::Xaml;
|
|
|
|
namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
|
|
{
|
|
DependencyProperty SettingContainer::_HeaderProperty{ nullptr };
|
|
DependencyProperty SettingContainer::_HelpTextProperty{ nullptr };
|
|
DependencyProperty SettingContainer::_HasSettingValueProperty{ nullptr };
|
|
DependencyProperty SettingContainer::_SettingOverrideSourceProperty{ nullptr };
|
|
|
|
SettingContainer::SettingContainer()
|
|
{
|
|
_InitializeProperties();
|
|
}
|
|
|
|
void SettingContainer::_InitializeProperties()
|
|
{
|
|
// Initialize any SettingContainer dependency properties here.
|
|
// This performs a lazy load on these properties, instead of
|
|
// initializing them when the DLL loads.
|
|
if (!_HeaderProperty)
|
|
{
|
|
_HeaderProperty =
|
|
DependencyProperty::Register(
|
|
L"Header",
|
|
xaml_typename<IInspectable>(),
|
|
xaml_typename<Editor::SettingContainer>(),
|
|
PropertyMetadata{ nullptr });
|
|
}
|
|
if (!_HelpTextProperty)
|
|
{
|
|
_HelpTextProperty =
|
|
DependencyProperty::Register(
|
|
L"HelpText",
|
|
xaml_typename<hstring>(),
|
|
xaml_typename<Editor::SettingContainer>(),
|
|
PropertyMetadata{ box_value(L"") });
|
|
}
|
|
if (!_HasSettingValueProperty)
|
|
{
|
|
_HasSettingValueProperty =
|
|
DependencyProperty::Register(
|
|
L"HasSettingValue",
|
|
xaml_typename<bool>(),
|
|
xaml_typename<Editor::SettingContainer>(),
|
|
PropertyMetadata{ box_value(false), PropertyChangedCallback{ &SettingContainer::_OnHasSettingValueChanged } });
|
|
}
|
|
if (!_SettingOverrideSourceProperty)
|
|
{
|
|
_SettingOverrideSourceProperty =
|
|
DependencyProperty::Register(
|
|
L"SettingOverrideSource",
|
|
xaml_typename<IInspectable>(),
|
|
xaml_typename<Editor::SettingContainer>(),
|
|
PropertyMetadata{ nullptr, PropertyChangedCallback{ &SettingContainer::_OnHasSettingValueChanged } });
|
|
}
|
|
}
|
|
|
|
void SettingContainer::_OnHasSettingValueChanged(DependencyObject const& d, DependencyPropertyChangedEventArgs const& /*args*/)
|
|
{
|
|
// update visibility for override message and reset button
|
|
const auto& obj{ d.try_as<Editor::SettingContainer>() };
|
|
get_self<SettingContainer>(obj)->_UpdateOverrideSystem();
|
|
}
|
|
|
|
void SettingContainer::OnApplyTemplate()
|
|
{
|
|
if (const auto& child{ GetTemplateChild(L"ResetButton") })
|
|
{
|
|
if (const auto& button{ child.try_as<Controls::Button>() })
|
|
{
|
|
// Apply click handler for the reset button.
|
|
// When clicked, we dispatch the bound ClearSettingValue event,
|
|
// resulting in inheriting the setting value from the parent.
|
|
button.Click([=](auto&&, auto&&) {
|
|
_ClearSettingValueHandlers(*this, nullptr);
|
|
|
|
// move the focus to the child control
|
|
if (const auto& content{ Content() })
|
|
{
|
|
if (const auto& control{ content.try_as<Controls::Control>() })
|
|
{
|
|
control.Focus(FocusState::Programmatic);
|
|
return;
|
|
}
|
|
else if (const auto& panel{ content.try_as<Controls::Panel>() })
|
|
{
|
|
for (const auto& panelChild : panel.Children())
|
|
{
|
|
if (const auto& panelControl{ panelChild.try_as<Controls::Control>() })
|
|
{
|
|
panelControl.Focus(FocusState::Programmatic);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
// if we get here, we didn't find something to reasonably focus to.
|
|
}
|
|
});
|
|
|
|
// apply name (automation property)
|
|
Automation::AutomationProperties::SetName(child, RS_(L"SettingContainer_OverrideMessageBaseLayer"));
|
|
}
|
|
}
|
|
|
|
_UpdateOverrideSystem();
|
|
|
|
if (const auto& content{ Content() })
|
|
{
|
|
if (const auto& obj{ content.try_as<DependencyObject>() })
|
|
{
|
|
// apply header text as name (automation property)
|
|
if (const auto& header{ Header() })
|
|
{
|
|
const auto headerText{ header.try_as<hstring>() };
|
|
if (headerText && !headerText->empty())
|
|
{
|
|
Automation::AutomationProperties::SetName(obj, *headerText);
|
|
}
|
|
}
|
|
|
|
// apply help text as tooltip and full description (automation property)
|
|
const auto& helpText{ HelpText() };
|
|
if (!helpText.empty())
|
|
{
|
|
Controls::ToolTipService::SetToolTip(obj, box_value(helpText));
|
|
Automation::AutomationProperties::SetFullDescription(obj, helpText);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Updates the override system visibility and text
|
|
// Arguments:
|
|
// - <none>
|
|
void SettingContainer::_UpdateOverrideSystem()
|
|
{
|
|
if (const auto& child{ GetTemplateChild(L"ResetButton") })
|
|
{
|
|
if (const auto& button{ child.try_as<Controls::Button>() })
|
|
{
|
|
if (HasSettingValue())
|
|
{
|
|
// We want to be smart about showing the override system.
|
|
// Don't just show it if the user explicitly set the setting.
|
|
// If the tooltip is empty, we'll hide the entire override system.
|
|
|
|
const auto& settingSrc{ SettingOverrideSource() };
|
|
const auto tooltip{ _GenerateOverrideMessage(settingSrc) };
|
|
|
|
Controls::ToolTipService::SetToolTip(button, box_value(tooltip));
|
|
button.Visibility(tooltip.empty() ? Visibility::Collapsed : Visibility::Visible);
|
|
}
|
|
else
|
|
{
|
|
// a value is not being overridden; hide the override system
|
|
button.Visibility(Visibility::Collapsed);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Helper function for generating the override message
|
|
// Arguments:
|
|
// - profile: the profile that defines the setting (aka SettingOverrideSource)
|
|
// Return Value:
|
|
// - text specifying where the setting was defined. If empty, we don't want to show the system.
|
|
hstring SettingContainer::_GenerateOverrideMessage(const IInspectable& settingOrigin)
|
|
{
|
|
// We only get here if the user had an override in place.
|
|
Model::OriginTag originTag{ Model::OriginTag::None };
|
|
winrt::hstring source;
|
|
|
|
if (const auto& profile{ settingOrigin.try_as<Model::Profile>() })
|
|
{
|
|
source = profile.Source();
|
|
originTag = profile.Origin();
|
|
}
|
|
else if (const auto& appearanceConfig{ settingOrigin.try_as<Model::AppearanceConfig>() })
|
|
{
|
|
const auto profile = appearanceConfig.SourceProfile();
|
|
source = profile.Source();
|
|
originTag = profile.Origin();
|
|
}
|
|
|
|
if constexpr (Feature_ShowProfileDefaultsInSettings::IsEnabled())
|
|
{
|
|
// EXPERIMENTAL FEATURE
|
|
// We will display arrows for all origins, and informative tooltips for Fragments and Generated
|
|
if (originTag == Model::OriginTag::Fragment || originTag == Model::OriginTag::Generated)
|
|
{
|
|
// from a fragment extension or generated profile
|
|
return hstring{ fmt::format(std::wstring_view{ RS_(L"SettingContainer_OverrideMessageFragmentExtension") }, source) };
|
|
}
|
|
return RS_(L"SettingContainer_OverrideMessageBaseLayer");
|
|
}
|
|
|
|
// STABLE FEATURE
|
|
// We will only display arrows and informative tooltips for Fragments
|
|
if (originTag == Model::OriginTag::Fragment)
|
|
{
|
|
// from a fragment extension
|
|
return hstring{ fmt::format(std::wstring_view{ RS_(L"SettingContainer_OverrideMessageFragmentExtension") }, source) };
|
|
}
|
|
return {}; // no tooltip
|
|
}
|
|
}
|