Dustin L. Howett a46fac25d3
Remove startOnUserLogin from the settings; use OS APIs only (#18530)
Before we had a Settings UI, we added support for a setting called
`startOnUserLogin`. It was a boolean, and on startup we would try to
yeet the value of that setting into the Windows API responsible for
registering us as a startup task.

Unfortunately, we failed to take into account a few things.

- Startup tasks can be independently controlled by the user in Windows
Settings or by an enterprise using enterprise policy
- This control is not limited to *disabling* the task; it also supports
enabling it!

Users could enable our startup task outside the settings file and we
would never know it. We would load up, see that `startOnUserLogin` was
`false`, and go disable the task again. 🤦

Conversely, if the user disables our task outside the app _we can never
enable it from inside the app._ If an enterprise has configured it
either direction, we can't change it either.

The best way forward is to remove it from our settings model and only
ever interact with the Windows API.

This pull request replaces `startOnUserLogin` with a rich settings
experience that will reflect the current and final state of the task as
configured through Windows. Terminal will enable it if it can and
display a message if it can't.

My first attempt at this PR (which you can read in the commit history)
made us try harder to sync the state between the settings model and the
OS; we would propagate the disabled state back to the user setting when
the task was disabled in the OS or if we failed to enable it when the
user asked for it. That was fragile and didn't support reporting the
state in the settings UI, and it seems like it would be confusing for a
setting to silently turn itself back off anyway...

Closes #12564
2025-02-20 16:53:33 -06:00

348 lines
14 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::_FontIconGlyphProperty{ nullptr };
DependencyProperty SettingContainer::_CurrentValueProperty{ nullptr };
DependencyProperty SettingContainer::_CurrentValueTemplateProperty{ nullptr };
DependencyProperty SettingContainer::_CurrentValueAccessibleNameProperty{ nullptr };
DependencyProperty SettingContainer::_HasSettingValueProperty{ nullptr };
DependencyProperty SettingContainer::_SettingOverrideSourceProperty{ nullptr };
DependencyProperty SettingContainer::_StartExpandedProperty{ 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""), PropertyChangedCallback{ &SettingContainer::_OnHelpTextChanged } });
}
if (!_FontIconGlyphProperty)
{
_FontIconGlyphProperty =
DependencyProperty::Register(
L"FontIconGlyph",
xaml_typename<hstring>(),
xaml_typename<Editor::SettingContainer>(),
PropertyMetadata{ box_value(L"") });
}
if (!_CurrentValueProperty)
{
_CurrentValueProperty =
DependencyProperty::Register(
L"CurrentValue",
xaml_typename<IInspectable>(),
xaml_typename<Editor::SettingContainer>(),
PropertyMetadata{ nullptr, PropertyChangedCallback{ &SettingContainer::_OnCurrentValueChanged } });
}
if (!_CurrentValueTemplateProperty)
{
_CurrentValueTemplateProperty =
DependencyProperty::Register(
L"CurrentValueTemplate",
xaml_typename<Windows::UI::Xaml::DataTemplate>(),
xaml_typename<Editor::SettingContainer>(),
PropertyMetadata{ nullptr });
}
if (!_CurrentValueAccessibleNameProperty)
{
_CurrentValueAccessibleNameProperty =
DependencyProperty::Register(
L"CurrentValueAccessibleName",
xaml_typename<Windows::UI::Xaml::DataTemplate>(),
xaml_typename<Editor::SettingContainer>(),
PropertyMetadata{ box_value(L""), PropertyChangedCallback{ &SettingContainer::_OnCurrentValueChanged } });
}
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 } });
}
if (!_StartExpandedProperty)
{
_StartExpandedProperty =
DependencyProperty::Register(
L"StartExpanded",
xaml_typename<bool>(),
xaml_typename<Editor::SettingContainer>(),
PropertyMetadata{ box_value(false) });
}
}
void SettingContainer::_OnCurrentValueChanged(const Windows::UI::Xaml::DependencyObject& d, const Windows::UI::Xaml::DependencyPropertyChangedEventArgs& /*e*/)
{
const auto& obj{ d.try_as<Editor::SettingContainer>() };
get_self<SettingContainer>(obj)->_UpdateCurrentValueAutoProp();
}
void SettingContainer::_OnHasSettingValueChanged(const DependencyObject& d, const DependencyPropertyChangedEventArgs& /*args*/)
{
// update visibility for override message and reset button
const auto& obj{ d.try_as<Editor::SettingContainer>() };
get_self<SettingContainer>(obj)->_UpdateOverrideSystem();
}
void SettingContainer::_OnHelpTextChanged(const DependencyObject& d, const DependencyPropertyChangedEventArgs& /*args*/)
{
// update visibility for override message and reset button
const auto& obj{ d.try_as<Editor::SettingContainer>() };
get_self<SettingContainer>(obj)->_UpdateHelpText();
}
void SettingContainer::_UpdateHelpText()
{
// Get the correct base to apply automation properties to
std::vector<DependencyObject> base;
base.reserve(2);
if (const auto& child{ GetTemplateChild(L"Expander") })
{
if (const auto& expander{ child.try_as<Microsoft::UI::Xaml::Controls::Expander>() })
{
base.push_back(child);
}
}
if (const auto& content{ Content() })
{
const auto& panel{ content.try_as<Controls::Panel>() };
const auto& obj{ content.try_as<DependencyObject>() };
if (!panel && obj)
{
base.push_back(obj);
}
}
for (const auto& obj : base)
{
// apply header and current value as name (automation property)
Automation::AutomationProperties::SetName(obj, _GenerateAccessibleName());
// apply help text as tooltip and full description (automation property)
if (const auto& helpText{ HelpText() }; !helpText.empty())
{
Automation::AutomationProperties::SetFullDescription(obj, helpText);
}
else
{
Controls::ToolTipService::SetToolTip(obj, nullptr);
Automation::AutomationProperties::SetFullDescription(obj, L"");
}
}
const auto textBlockHidden = HelpText().empty();
if (const auto& child{ GetTemplateChild(L"HelpTextBlock") })
{
if (const auto& textBlock{ child.try_as<Controls::TextBlock>() })
{
textBlock.Visibility(textBlockHidden ? Visibility::Collapsed : Visibility::Visible);
}
}
}
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&&) {
ClearSettingValue.raise(*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();
_UpdateHelpText();
}
void SettingContainer::SetExpanded(bool expanded)
{
if (const auto& child{ GetTemplateChild(L"Expander") })
{
if (const auto& expander{ child.try_as<Microsoft::UI::Xaml::Controls::Expander>() })
{
expander.IsExpanded(expanded);
}
}
}
// 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);
}
}
}
}
void SettingContainer::_UpdateCurrentValueAutoProp()
{
if (const auto& child{ GetTemplateChild(L"Expander") })
{
if (const auto& expander{ child.try_as<Microsoft::UI::Xaml::Controls::Expander>() })
{
Automation::AutomationProperties::SetName(expander, _GenerateAccessibleName());
}
}
}
// 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.
auto 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();
}
// 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{ RS_fmt(L"SettingContainer_OverrideMessageFragmentExtension", source) };
}
return RS_(L"SettingContainer_OverrideMessageBaseLayer");
}
// Method Description:
// - Helper function for generating the accessible name
// Return Value:
// - text specifying the accessible name. Includes header and current value, if available.
hstring SettingContainer::_GenerateAccessibleName()
{
hstring name{};
if (const auto& header{ Header() })
{
if (const auto headerText{ header.try_as<hstring>() })
{
name = *headerText;
}
// append current value to the name, if it exists
if (const auto currentValAccessibleName{ CurrentValueAccessibleName() }; !currentValAccessibleName.empty())
{
// prefer CurrentValueAccessibleName, if it exists
name = name + L": " + currentValAccessibleName;
}
else if (const auto& currentVal{ CurrentValue() })
{
// the accessible name was not defined, so try to
// extract the value directly from the CurrentValue property
if (const auto currentValText{ currentVal.try_as<hstring>() })
{
name = name + L": " + *currentValText;
}
}
}
return name;
}
}