diff --git a/src/cascadia/TerminalSettingsEditor/AddProfile.xaml b/src/cascadia/TerminalSettingsEditor/AddProfile.xaml index 633dd20875..0bee1fd2b4 100644 --- a/src/cascadia/TerminalSettingsEditor/AddProfile.xaml +++ b/src/cascadia/TerminalSettingsEditor/AddProfile.xaml @@ -37,8 +37,8 @@ - + - + + + - - - - - + + + ().Tag().as(); + const auto extPkgVM = sender.as().Tag().as(); _ViewModel.CurrentExtensionPackage(extPkgVM); } void Extensions::NavigateToProfile_Click(const IInspectable& sender, const RoutedEventArgs& /*args*/) { - const auto& profileGuid = sender.as().Tag().as(); + const auto element = sender.as(); + const auto profileGuid = winrt::unbox_value(element.Tag()); get_self(_ViewModel)->NavigateToProfile(profileGuid); } void Extensions::NavigateToColorScheme_Click(const IInspectable& sender, const RoutedEventArgs& /*args*/) { - const auto& schemeVM = sender.as().Tag().as(); + const auto element = sender.as(); + const auto schemeVM = element.Tag().as(); get_self(_ViewModel)->NavigateToColorScheme(schemeVM); } diff --git a/src/cascadia/TerminalSettingsEditor/Extensions.xaml b/src/cascadia/TerminalSettingsEditor/Extensions.xaml index 9659f8f467..22d03d8832 100644 --- a/src/cascadia/TerminalSettingsEditor/Extensions.xaml +++ b/src/cascadia/TerminalSettingsEditor/Extensions.xaml @@ -127,122 +127,78 @@ - + + + + + + - + + + + + + - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + @@ -403,41 +359,17 @@ - - - - - - - - - - - - - - - - - - - - - - + + + + + + @@ -500,11 +432,13 @@ - + + + SettingContainer.idl + + SettingsCard.idl + + + SettingsExpander.idl + + + StyleExtensions.idl + + + ControlSizeTrigger.idl + + + CornerRadiusFilterConverters.idl + + + StringDefaultTemplateSelector.idl + @@ -255,6 +273,12 @@ DefaultStyle + + DefaultStyle + + + DefaultStyle + @@ -388,6 +412,24 @@ SettingContainer.idl + + SettingsCard.idl + + + SettingsExpander.idl + + + StyleExtensions.idl + + + ControlSizeTrigger.idl + + + CornerRadiusFilterConverters.idl + + + StringDefaultTemplateSelector.idl + PreviewConnection.h @@ -498,6 +540,24 @@ Code + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + diff --git a/src/cascadia/TerminalSettingsEditor/Microsoft.Terminal.Settings.Editor.vcxproj.filters b/src/cascadia/TerminalSettingsEditor/Microsoft.Terminal.Settings.Editor.vcxproj.filters index dd276d82ec..4110f8d8b0 100644 --- a/src/cascadia/TerminalSettingsEditor/Microsoft.Terminal.Settings.Editor.vcxproj.filters +++ b/src/cascadia/TerminalSettingsEditor/Microsoft.Terminal.Settings.Editor.vcxproj.filters @@ -28,7 +28,12 @@ - + + + + + + @@ -50,6 +55,8 @@ + + diff --git a/src/cascadia/TerminalSettingsEditor/Profiles_Advanced.xaml b/src/cascadia/TerminalSettingsEditor/Profiles_Advanced.xaml index b00c1753bb..9adea2aa01 100644 --- a/src/cascadia/TerminalSettingsEditor/Profiles_Advanced.xaml +++ b/src/cascadia/TerminalSettingsEditor/Profiles_Advanced.xaml @@ -35,254 +35,222 @@ Style="{StaticResource DisclaimerStyle}" Visibility="{x:Bind Profile.IsBaseLayer}" /> - + - + - + - + - + - + - + - + - + - + - - - - - - - - + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + - - - + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + diff --git a/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.cpp b/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.cpp index 8a24895a57..e817b20020 100644 --- a/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.cpp +++ b/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.cpp @@ -70,7 +70,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation TraceLoggingValue(_Profile.IsBaseLayer(), "IsProfileDefaults", "If the modified profile is the profile.defaults object"), TraceLoggingValue(static_cast(_Profile.Guid()), "ProfileGuid", "The guid of the profile that was navigated to"), TraceLoggingValue(_Profile.Source().c_str(), "ProfileSource", "The source of the profile that was navigated to"), - TraceLoggingValue(_Profile.DefaultAppearance().BackgroundImageSettingsVisible(), "HasBackgroundImage", "If the profile has a background image defined"), + TraceLoggingValue(_Profile.DefaultAppearance().BackgroundImageSettingsEnabled(), "HasBackgroundImage", "If the profile has a background image defined"), TraceLoggingValue(_Profile.HasUnfocusedAppearance(), "HasUnfocusedAppearance", "If the profile has an unfocused appearance defined"), TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); diff --git a/src/cascadia/TerminalSettingsEditor/Profiles_Base.cpp b/src/cascadia/TerminalSettingsEditor/Profiles_Base.cpp index edd4bdbcf5..3258d5e9b3 100644 --- a/src/cascadia/TerminalSettingsEditor/Profiles_Base.cpp +++ b/src/cascadia/TerminalSettingsEditor/Profiles_Base.cpp @@ -22,11 +22,11 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation Automation::AutomationProperties::SetFullDescription(StartingDirectoryUseParentCheckbox(), unbox_value(startingDirCheckboxTooltip)); Automation::AutomationProperties::SetName(DeleteButton(), RS_(L"Profile_DeleteButton/Text")); - AppearanceNavigator().Content(box_value(RS_(L"Profile_Appearance/Header"))); - AppearanceNavigator().Tag(box_value(RS_(L"Profile_AppearanceNavigator/HelpText"))); - TerminalNavigator().Content(box_value(RS_(L"Profile_Terminal/Header"))); - TerminalNavigator().Tag(box_value(RS_(L"Profile_TerminalNavigator/HelpText"))); - AdvancedNavigator().Content(box_value(RS_(L"Profile_Advanced/Header"))); + AppearanceNavigator().Header(box_value(RS_(L"Profile_Appearance/Header"))); + AppearanceNavigator().Description(box_value(RS_(L"Profile_AppearanceNavigator/HelpText"))); + TerminalNavigator().Header(box_value(RS_(L"Profile_Terminal/Header"))); + TerminalNavigator().Description(box_value(RS_(L"Profile_TerminalNavigator/HelpText"))); + AdvancedNavigator().Header(box_value(RS_(L"Profile_Advanced/Header"))); } void Profiles_Base::OnNavigatedTo(const NavigationEventArgs& e) diff --git a/src/cascadia/TerminalSettingsEditor/Profiles_Base.h b/src/cascadia/TerminalSettingsEditor/Profiles_Base.h index 2fa4ab7225..8b8594c6b9 100644 --- a/src/cascadia/TerminalSettingsEditor/Profiles_Base.h +++ b/src/cascadia/TerminalSettingsEditor/Profiles_Base.h @@ -22,6 +22,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation void Appearance_Click(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& e); void Terminal_Click(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& e); void Advanced_Click(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& e); + void ResetProfileConfirmation_Click(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& e); void DeleteConfirmation_Click(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& e); til::property_changed_event PropertyChanged; diff --git a/src/cascadia/TerminalSettingsEditor/Profiles_Base.xaml b/src/cascadia/TerminalSettingsEditor/Profiles_Base.xaml index 3e165cedb9..a46dcbff76 100644 --- a/src/cascadia/TerminalSettingsEditor/Profiles_Base.xaml +++ b/src/cascadia/TerminalSettingsEditor/Profiles_Base.xaml @@ -190,15 +190,15 @@ Margin="0,32,0,4" Style="{StaticResource TextBlockSubHeaderStyle}" /> - - - + + - - + + - - + + - - + + - - + + diff --git a/src/cascadia/TerminalSettingsEditor/Profiles_Terminal.xaml b/src/cascadia/TerminalSettingsEditor/Profiles_Terminal.xaml index b8701d7132..9d6d165b36 100644 --- a/src/cascadia/TerminalSettingsEditor/Profiles_Terminal.xaml +++ b/src/cascadia/TerminalSettingsEditor/Profiles_Terminal.xaml @@ -29,69 +29,58 @@ Style="{StaticResource DisclaimerStyle}" Visibility="{x:Bind Profile.IsBaseLayer}" /> - + - + - + - + - + - + - + - + - + - + - - - + + + + + + + + + + diff --git a/src/cascadia/TerminalSettingsEditor/Resources/de-DE/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/de-DE/Resources.resw index 3b20dcb8ee..0e84a3de2f 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/de-DE/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/de-DE/Resources.resw @@ -1,4 +1,4 @@ - + - @@ -64,16 +48,6 @@ - - - - - - @@ -84,8 +58,6 @@ - @@ -97,28 +69,14 @@ - - - - - - - SemiBold - - - 0,4,8,4 16 @@ -198,7 +156,7 @@ BasedOn="{StaticResource SettingsPageItemDescriptionStyle}" TargetType="TextBlock"> - + @@ -209,6 +167,9 @@ Text="{Binding}" /> + + - - - - - - + + diff --git a/src/cascadia/TerminalSettingsEditor/SettingsControlsStyle.xaml b/src/cascadia/TerminalSettingsEditor/SettingsControlsStyle.xaml new file mode 100644 index 0000000000..a57be63b3b --- /dev/null +++ b/src/cascadia/TerminalSettingsEditor/SettingsControlsStyle.xaml @@ -0,0 +1,1100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + 16,16,16,16 + 148 + 68 + 12 + 20 + 0 + 120 + 2,0,20,0 + 14,0,0,0 + 13 + 0.4 + 8 + 476 + 286 + + 16,16,4,16 + 58,8,44,8 + 58,8,16,8 + 0,1,0,0 + 16 + 32 + 32 + + + + Show all settings + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/cascadia/TerminalSettingsEditor/SettingsExpander.cpp b/src/cascadia/TerminalSettingsEditor/SettingsExpander.cpp new file mode 100644 index 0000000000..1048a66b35 --- /dev/null +++ b/src/cascadia/TerminalSettingsEditor/SettingsExpander.cpp @@ -0,0 +1,342 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "SettingsExpander.h" +#include "SettingsExpander.g.cpp" +#include "SettingsExpanderAutomationPeer.g.cpp" +#include "SettingsExpanderItemStyleSelector.g.cpp" +#include "StyleExtensions.h" + +using namespace winrt::Windows::Foundation; +using namespace winrt::Windows::Foundation::Collections; +using namespace winrt::Windows::UI::Xaml; +using namespace winrt::Windows::UI::Xaml::Automation; +using namespace winrt::Windows::UI::Xaml::Automation::Peers; +using namespace winrt::Windows::UI::Xaml::Controls; + +namespace winrt::Microsoft::Terminal::Settings::Editor::implementation +{ + DependencyProperty SettingsExpander::_HeaderProperty{ nullptr }; + DependencyProperty SettingsExpander::_DescriptionProperty{ nullptr }; + DependencyProperty SettingsExpander::_HeaderIconProperty{ nullptr }; + DependencyProperty SettingsExpander::_ContentProperty{ nullptr }; + DependencyProperty SettingsExpander::_IsExpandedProperty{ nullptr }; + DependencyProperty SettingsExpander::_ItemsHeaderProperty{ nullptr }; + DependencyProperty SettingsExpander::_ItemsFooterProperty{ nullptr }; + DependencyProperty SettingsExpander::_ItemsProperty{ nullptr }; + DependencyProperty SettingsExpander::_ItemsSourceProperty{ nullptr }; + DependencyProperty SettingsExpander::_ItemTemplateProperty{ nullptr }; + DependencyProperty SettingsExpander::_ItemContainerStyleSelectorProperty{ nullptr }; + + static constexpr std::wstring_view PART_ItemsHost{ L"PART_ItemsHost" }; + + SettingsExpander::SettingsExpander() + { + _InitializeProperties(); + + // Items is backed by an observable vector so post-construction mutations + // (e.g. user code that adds cards after the expander is on screen) also + // refresh the inner ItemsControl. The XAML parser populates Items via + // Append before OnApplyTemplate runs, so the eager population path also + // works. + Items(single_threaded_observable_vector()); + } + + void SettingsExpander::_InitializeProperties() + { + if (!_HeaderProperty) + { + _HeaderProperty = DependencyProperty::Register( + L"Header", + xaml_typename(), + xaml_typename(), + PropertyMetadata{ nullptr }); + } + if (!_DescriptionProperty) + { + _DescriptionProperty = DependencyProperty::Register( + L"Description", + xaml_typename(), + xaml_typename(), + PropertyMetadata{ nullptr }); + } + if (!_HeaderIconProperty) + { + _HeaderIconProperty = DependencyProperty::Register( + L"HeaderIcon", + xaml_typename(), + xaml_typename(), + PropertyMetadata{ nullptr }); + } + if (!_ContentProperty) + { + _ContentProperty = DependencyProperty::Register( + L"Content", + xaml_typename(), + xaml_typename(), + PropertyMetadata{ nullptr }); + } + if (!_IsExpandedProperty) + { + _IsExpandedProperty = DependencyProperty::Register( + L"IsExpanded", + xaml_typename(), + xaml_typename(), + PropertyMetadata{ box_value(false), PropertyChangedCallback{ &SettingsExpander::_OnIsExpandedChanged } }); + } + if (!_ItemsHeaderProperty) + { + _ItemsHeaderProperty = DependencyProperty::Register( + L"ItemsHeader", + xaml_typename(), + xaml_typename(), + PropertyMetadata{ nullptr }); + } + if (!_ItemsFooterProperty) + { + _ItemsFooterProperty = DependencyProperty::Register( + L"ItemsFooter", + xaml_typename(), + xaml_typename(), + PropertyMetadata{ nullptr }); + } + if (!_ItemsProperty) + { + _ItemsProperty = DependencyProperty::Register( + L"Items", + xaml_typename>(), + xaml_typename(), + PropertyMetadata{ nullptr, PropertyChangedCallback{ &SettingsExpander::_OnItemsConnectedPropertyChanged } }); + } + if (!_ItemsSourceProperty) + { + _ItemsSourceProperty = DependencyProperty::Register( + L"ItemsSource", + xaml_typename(), + xaml_typename(), + PropertyMetadata{ nullptr, PropertyChangedCallback{ &SettingsExpander::_OnItemsConnectedPropertyChanged } }); + } + if (!_ItemTemplateProperty) + { + _ItemTemplateProperty = DependencyProperty::Register( + L"ItemTemplate", + xaml_typename(), + xaml_typename(), + PropertyMetadata{ nullptr }); + } + if (!_ItemContainerStyleSelectorProperty) + { + _ItemContainerStyleSelectorProperty = DependencyProperty::Register( + L"ItemContainerStyleSelector", + xaml_typename(), + xaml_typename(), + PropertyMetadata{ nullptr }); + } + } + + AutomationPeer SettingsExpander::OnCreateAutomationPeer() + { + return winrt::make(*this); + } + + void SettingsExpander::OnApplyTemplate() + { + // Same implicit-styles injection as SettingsCard so a ToggleSwitch / + // Slider / ComboBox / TextBox placed directly as SettingsExpander.Content + // gets the same Windows 11 defaults. + StyleExtensions::EnsureImplicitStylesMergedInto(*this); + + _SetAccessibleName(); + + // Drop the prior template's host before locating the new one. + _itemsHost = nullptr; + + if (const auto child{ GetTemplateChild(hstring{ PART_ItemsHost }) }) + { + _itemsHost = child.try_as(); + } + + if (_itemsHost) + { + // Push our initial ItemsSource through to the host and stamp item-container styles. + _UpdateItemsSource(); + } + } + + void SettingsExpander::_SetAccessibleName() + { + if (!AutomationProperties::GetName(*this).empty()) + { + return; + } + if (const auto headerString{ unbox_value_or(Header(), hstring{}) }; !headerString.empty()) + { + AutomationProperties::SetName(*this, headerString); + } + } + + void SettingsExpander::_UpdateItemsSource() + { + if (!_itemsHost) + { + return; + } + // ItemsSource wins when set; otherwise fall back to the inline Items collection. + if (const auto source{ ItemsSource() }) + { + _itemsHost.ItemsSource(source); + } + else + { + _itemsHost.ItemsSource(Items()); + } + + _ApplyItemContainerStyles(); + _SubscribeToItemsVectorChanged(); + } + + // Watch our inline Items vector for changes so containers added after + // OnApplyTemplate also get the proper SettingsCard item style. (When + // ItemsSource is set, this is a no-op since the parser only touches Items.) + void SettingsExpander::_SubscribeToItemsVectorChanged() + { + _itemsVectorChangedRevoker.revoke(); + + if (ItemsSource()) + { + return; + } + + if (const auto observable{ Items().try_as>() }) + { + _itemsVectorChangedRevoker = observable.VectorChanged(winrt::auto_revoke, [weakThis = get_weak()](auto&&, auto&&) { + if (const auto strongThis{ weakThis.get() }) + { + strongThis->_ApplyItemContainerStyles(); + } + }); + } + } + + // Apply the per-item style produced by ItemContainerStyleSelector. ItemsControl + // only generates ContentPresenter containers for non-UIElement items, so when + // SettingsCards are added directly the cards themselves are the "containers" + // and we have to set Style on them ourselves. Mirrors the ElementPrepared path + // we used when this was an ItemsRepeater. + void SettingsExpander::_ApplyItemContainerStyles() + { + const auto selector{ ItemContainerStyleSelector() }; + if (!selector) + { + return; + } + + const auto items{ Items() }; + if (!items) + { + return; + } + + for (uint32_t i = 0; i < items.Size(); ++i) + { + if (const auto element{ items.GetAt(i).try_as() }) + { + if (element.ReadLocalValue(FrameworkElement::StyleProperty()) == DependencyProperty::UnsetValue()) + { + element.Style(selector.SelectStyle(items.GetAt(i), element)); + } + } + } + } + + void SettingsExpander::_OnItemsConnectedPropertyChanged(const DependencyObject& d, const DependencyPropertyChangedEventArgs& /*e*/) + { + if (const auto obj{ d.try_as() }) + { + get_self(obj)->_UpdateItemsSource(); + } + } + + void SettingsExpander::_OnIsExpandedChanged(const DependencyObject& d, const DependencyPropertyChangedEventArgs& e) + { + const auto obj{ d.try_as() }; + if (!obj) + { + return; + } + const auto self = get_self(obj); + const auto newValue = unbox_value_or(e.NewValue(), false); + + // Notify the automation peer so screen readers see the expand/collapse state change. + if (const auto peer{ FrameworkElementAutomationPeer::FromElement(obj).try_as() }) + { + peer.RaiseExpandedChangedEvent(newValue); + } + + if (newValue) + { + self->Expanded.raise(obj, nullptr); + } + else + { + self->Collapsed.raise(obj, nullptr); + } + } + + SettingsExpanderAutomationPeer::SettingsExpanderAutomationPeer(const Editor::SettingsExpander& owner) : + SettingsExpanderAutomationPeerT(owner) + { + } + + AutomationControlType SettingsExpanderAutomationPeer::GetAutomationControlTypeCore() const + { + return AutomationControlType::Group; + } + + hstring SettingsExpanderAutomationPeer::GetClassNameCore() const + { + return hstring{ L"SettingsExpander" }; + } + + hstring SettingsExpanderAutomationPeer::GetNameCore() const + { + if (const auto expander{ Owner().try_as() }) + { + if (const auto manualName{ AutomationProperties::GetName(expander) }; !manualName.empty()) + { + return manualName; + } + if (const auto headerString{ unbox_value_or(expander.Header(), hstring{}) }; !headerString.empty()) + { + return headerString; + } + } + return {}; + } + + void SettingsExpanderAutomationPeer::RaiseExpandedChangedEvent(bool newValue) + { + // Mirrors the toolkit's SettingsExpanderAutomationPeer.RaiseExpandedChangedEvent. + // Narrator doesn't actually announce this today (microsoft/microsoft-ui-xaml#3469), + // but other AT can subscribe and we keep parity with the toolkit. + const auto newState = newValue ? ExpandCollapseState::Expanded : ExpandCollapseState::Collapsed; + const auto oldState = newValue ? ExpandCollapseState::Collapsed : ExpandCollapseState::Expanded; + RaisePropertyChangedEvent(ExpandCollapsePatternIdentifiers::ExpandCollapseStateProperty(), box_value(oldState), box_value(newState)); + } + + Windows::UI::Xaml::Style SettingsExpanderItemStyleSelector::SelectStyleCore(const winrt::Windows::Foundation::IInspectable& /*item*/, const Windows::UI::Xaml::DependencyObject& container) + { + // When the prepared container is a clickable SettingsCard, give it the + // clickable item style (which adds the right padding for the chevron). + // Otherwise fall back to the default item style. + if (const auto card{ container.try_as() }) + { + if (card.IsClickEnabled()) + { + return _ClickableStyle; + } + } + return _DefaultStyle; + } +} diff --git a/src/cascadia/TerminalSettingsEditor/SettingsExpander.h b/src/cascadia/TerminalSettingsEditor/SettingsExpander.h new file mode 100644 index 0000000000..f77855171a --- /dev/null +++ b/src/cascadia/TerminalSettingsEditor/SettingsExpander.h @@ -0,0 +1,97 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- SettingsExpander + +Abstract: +- A collapsable container for grouping multiple SettingsCards. Based + on the Windows Community Toolkit's SettingsExpander. + +Author(s): +- Carlos Zamora - May 2026 (port from CommunityToolkit.WinUI.Controls.SettingsExpander) + +--*/ + +#pragma once + +#include "SettingsExpander.g.h" +#include "SettingsExpanderAutomationPeer.g.h" +#include "SettingsExpanderItemStyleSelector.g.h" +#include "Utils.h" + +namespace winrt::Microsoft::Terminal::Settings::Editor::implementation +{ + struct SettingsExpander : SettingsExpanderT + { + public: + SettingsExpander(); + + void OnApplyTemplate(); + + Windows::UI::Xaml::Automation::Peers::AutomationPeer OnCreateAutomationPeer(); + + til::typed_event Expanded; + til::typed_event Collapsed; + + DEPENDENCY_PROPERTY(Windows::Foundation::IInspectable, Header); + DEPENDENCY_PROPERTY(Windows::Foundation::IInspectable, Description); + DEPENDENCY_PROPERTY(Windows::UI::Xaml::Controls::IconElement, HeaderIcon); + DEPENDENCY_PROPERTY(Windows::Foundation::IInspectable, Content); + DEPENDENCY_PROPERTY(bool, IsExpanded); + DEPENDENCY_PROPERTY(Windows::UI::Xaml::UIElement, ItemsHeader); + DEPENDENCY_PROPERTY(Windows::UI::Xaml::UIElement, ItemsFooter); + DEPENDENCY_PROPERTY(Windows::Foundation::Collections::IVector, Items); + DEPENDENCY_PROPERTY(Windows::Foundation::IInspectable, ItemsSource); + DEPENDENCY_PROPERTY(Windows::Foundation::IInspectable, ItemTemplate); + DEPENDENCY_PROPERTY(Windows::UI::Xaml::Controls::StyleSelector, ItemContainerStyleSelector); + + private: + static void _InitializeProperties(); + static void _OnIsExpandedChanged(const Windows::UI::Xaml::DependencyObject& d, const Windows::UI::Xaml::DependencyPropertyChangedEventArgs& e); + static void _OnItemsConnectedPropertyChanged(const Windows::UI::Xaml::DependencyObject& d, const Windows::UI::Xaml::DependencyPropertyChangedEventArgs& e); + + void _SetAccessibleName(); + void _UpdateItemsSource(); + void _SubscribeToItemsVectorChanged(); + void _ApplyItemContainerStyles(); + + Windows::UI::Xaml::Controls::ItemsControl _itemsHost{ nullptr }; + Windows::Foundation::Collections::IObservableVector::VectorChanged_revoker _itemsVectorChangedRevoker; + }; + + // AutomationPeer for SettingsExpander. Reports class name and falls back to + // Header text for the name when AutomationProperties.Name is unset. + struct SettingsExpanderAutomationPeer : SettingsExpanderAutomationPeerT + { + public: + SettingsExpanderAutomationPeer(const Editor::SettingsExpander& owner); + + Windows::UI::Xaml::Automation::Peers::AutomationControlType GetAutomationControlTypeCore() const; + hstring GetClassNameCore() const; + hstring GetNameCore() const; + + void RaiseExpandedChangedEvent(bool newValue); + }; + + // StyleSelector used by SettingsExpander to choose between a clickable vs. + // non-clickable SettingsCard container style for items in its ItemsControl. + // Ported from the Windows Community Toolkit's SettingsExpanderItemStyleSelector. + struct SettingsExpanderItemStyleSelector : SettingsExpanderItemStyleSelectorT + { + SettingsExpanderItemStyleSelector() = default; + + Windows::UI::Xaml::Style SelectStyleCore(const Windows::Foundation::IInspectable& item, const Windows::UI::Xaml::DependencyObject& container); + + WINRT_PROPERTY(Windows::UI::Xaml::Style, DefaultStyle, nullptr); + WINRT_PROPERTY(Windows::UI::Xaml::Style, ClickableStyle, nullptr); + }; +} + +namespace winrt::Microsoft::Terminal::Settings::Editor::factory_implementation +{ + BASIC_FACTORY(SettingsExpander); + BASIC_FACTORY(SettingsExpanderAutomationPeer); + BASIC_FACTORY(SettingsExpanderItemStyleSelector); +} diff --git a/src/cascadia/TerminalSettingsEditor/SettingsExpander.idl b/src/cascadia/TerminalSettingsEditor/SettingsExpander.idl new file mode 100644 index 0000000000..86fea0bd72 --- /dev/null +++ b/src/cascadia/TerminalSettingsEditor/SettingsExpander.idl @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace Microsoft.Terminal.Settings.Editor +{ + [contentproperty("Content")] + [default_interface] runtimeclass SettingsExpander : Windows.UI.Xaml.Controls.Control + { + SettingsExpander(); + + Object Header; + static Windows.UI.Xaml.DependencyProperty HeaderProperty { get; }; + + Object Description; + static Windows.UI.Xaml.DependencyProperty DescriptionProperty { get; }; + + Windows.UI.Xaml.Controls.IconElement HeaderIcon; + static Windows.UI.Xaml.DependencyProperty HeaderIconProperty { get; }; + + Object Content; + static Windows.UI.Xaml.DependencyProperty ContentProperty { get; }; + + Boolean IsExpanded; + static Windows.UI.Xaml.DependencyProperty IsExpandedProperty { get; }; + + Windows.UI.Xaml.UIElement ItemsHeader; + static Windows.UI.Xaml.DependencyProperty ItemsHeaderProperty { get; }; + + Windows.UI.Xaml.UIElement ItemsFooter; + static Windows.UI.Xaml.DependencyProperty ItemsFooterProperty { get; }; + + Windows.Foundation.Collections.IVector Items; + static Windows.UI.Xaml.DependencyProperty ItemsProperty { get; }; + + Object ItemsSource; + static Windows.UI.Xaml.DependencyProperty ItemsSourceProperty { get; }; + + Object ItemTemplate; + static Windows.UI.Xaml.DependencyProperty ItemTemplateProperty { get; }; + + Windows.UI.Xaml.Controls.StyleSelector ItemContainerStyleSelector; + static Windows.UI.Xaml.DependencyProperty ItemContainerStyleSelectorProperty { get; }; + + event Windows.Foundation.TypedEventHandler Expanded; + event Windows.Foundation.TypedEventHandler Collapsed; + }; + + [default_interface] runtimeclass SettingsExpanderAutomationPeer : Windows.UI.Xaml.Automation.Peers.FrameworkElementAutomationPeer + { + SettingsExpanderAutomationPeer(SettingsExpander owner); + + void RaiseExpandedChangedEvent(Boolean newValue); + }; + + [default_interface] runtimeclass SettingsExpanderItemStyleSelector : Windows.UI.Xaml.Controls.StyleSelector + { + SettingsExpanderItemStyleSelector(); + + Windows.UI.Xaml.Style DefaultStyle; + Windows.UI.Xaml.Style ClickableStyle; + }; +} diff --git a/src/cascadia/TerminalSettingsEditor/StringDefaultTemplateSelector.cpp b/src/cascadia/TerminalSettingsEditor/StringDefaultTemplateSelector.cpp new file mode 100644 index 0000000000..4c17b18f2f --- /dev/null +++ b/src/cascadia/TerminalSettingsEditor/StringDefaultTemplateSelector.cpp @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "StringDefaultTemplateSelector.h" +#include "StringDefaultTemplateSelector.g.cpp" + +using namespace winrt::Windows::Foundation; +using namespace winrt::Windows::UI::Xaml; + +namespace winrt::Microsoft::Terminal::Settings::Editor::implementation +{ + DataTemplate StringDefaultTemplateSelector::SelectTemplateCore(const IInspectable& item, const DependencyObject& /*container*/) + { + return SelectTemplateCore(item); + } + + DataTemplate StringDefaultTemplateSelector::SelectTemplateCore(const IInspectable& item) + { + if (const auto pv{ item.try_as() }; pv && pv.Type() == PropertyType::String) + { + return _StringTemplate; + } + return nullptr; + } +} diff --git a/src/cascadia/TerminalSettingsEditor/StringDefaultTemplateSelector.h b/src/cascadia/TerminalSettingsEditor/StringDefaultTemplateSelector.h new file mode 100644 index 0000000000..56b7f4a355 --- /dev/null +++ b/src/cascadia/TerminalSettingsEditor/StringDefaultTemplateSelector.h @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation +// Licensed under the MIT license. + +#pragma once + +#include "StringDefaultTemplateSelector.g.h" + +namespace winrt::Microsoft::Terminal::Settings::Editor::implementation +{ + struct StringDefaultTemplateSelector : StringDefaultTemplateSelectorT + { + StringDefaultTemplateSelector() = default; + + Windows::UI::Xaml::DataTemplate SelectTemplateCore(const winrt::Windows::Foundation::IInspectable& item, const winrt::Windows::UI::Xaml::DependencyObject& container); + Windows::UI::Xaml::DataTemplate SelectTemplateCore(const winrt::Windows::Foundation::IInspectable& item); + + WINRT_PROPERTY(winrt::Windows::UI::Xaml::DataTemplate, StringTemplate, nullptr); + }; +} + +namespace winrt::Microsoft::Terminal::Settings::Editor::factory_implementation +{ + BASIC_FACTORY(StringDefaultTemplateSelector); +} diff --git a/src/cascadia/TerminalSettingsEditor/StringDefaultTemplateSelector.idl b/src/cascadia/TerminalSettingsEditor/StringDefaultTemplateSelector.idl new file mode 100644 index 0000000000..db8562d8ac --- /dev/null +++ b/src/cascadia/TerminalSettingsEditor/StringDefaultTemplateSelector.idl @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace Microsoft.Terminal.Settings.Editor +{ + [default_interface] runtimeclass StringDefaultTemplateSelector : Windows.UI.Xaml.Controls.DataTemplateSelector + { + StringDefaultTemplateSelector(); + + Windows.UI.Xaml.DataTemplate StringTemplate; + } +} diff --git a/src/cascadia/TerminalSettingsEditor/StyleExtensions.cpp b/src/cascadia/TerminalSettingsEditor/StyleExtensions.cpp new file mode 100644 index 0000000000..4a9bb2b876 --- /dev/null +++ b/src/cascadia/TerminalSettingsEditor/StyleExtensions.cpp @@ -0,0 +1,179 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "StyleExtensions.h" +#include "StyleExtensions.g.cpp" + +using namespace winrt::Windows::Foundation; +using namespace winrt::Windows::Foundation::Collections; +using namespace winrt::Windows::UI::Xaml; + +namespace winrt::Microsoft::Terminal::Settings::Editor::implementation +{ + DependencyProperty StyleExtensions::_resourcesProperty{ nullptr }; + ResourceDictionary StyleExtensions::_sharedImplicitStylesDictionary{ nullptr }; + + DependencyProperty StyleExtensions::ResourcesProperty() + { + _InitializeProperties(); + return _resourcesProperty; + } + + void StyleExtensions::_InitializeProperties() + { + if (!_resourcesProperty) + { + _resourcesProperty = DependencyProperty::RegisterAttached( + L"Resources", + xaml_typename(), + xaml_typename(), + PropertyMetadata{ nullptr, PropertyChangedCallback{ &StyleExtensions::_OnResourcesChanged } }); + } + } + + ResourceDictionary StyleExtensions::GetResources(const DependencyObject& target) + { + return target.GetValue(ResourcesProperty()).try_as(); + } + + void StyleExtensions::SetResources(const DependencyObject& target, const ResourceDictionary& value) + { + target.SetValue(ResourcesProperty(), value); + } + + void StyleExtensions::_OnResourcesChanged(const DependencyObject& d, const DependencyPropertyChangedEventArgs& e) + { + const auto frameworkElement{ d.try_as() }; + if (!frameworkElement) + { + return; + } + + const auto elementResources{ frameworkElement.Resources() }; + if (!elementResources) + { + return; + } + const auto mergedDictionaries{ elementResources.MergedDictionaries() }; + if (!mergedDictionaries) + { + return; + } + + // Remove the previously-merged dictionary (if any). Resource dictionaries + // are reference types so IndexOf walks by identity, which is exactly what + // we want: the same Setter.Value is shared across every element that uses + // this Style, so the OldValue we see here is the exact instance we + // appended last time the property changed. + if (const auto oldDict{ e.OldValue().try_as() }) + { + uint32_t index{ 0 }; + if (mergedDictionaries.IndexOf(oldDict, index)) + { + mergedDictionaries.RemoveAt(index); + } + } + + // Add the new dictionary directly. We deliberately do NOT clone the way + // the WCT C# port does: ResourceDictionary is sealed (so we can't tag a + // private subclass like the toolkit), and a deep clone would have to + // copy the inline dictionary's Source URI — which XAML may leave as a + // relative string like "CommonResources.xaml" and which the runtime + // then rejects with "is not a valid absolute URI". Sharing the same + // dictionary across elements is fine: each element's MergedDictionaries + // only holds a reference, and implicit styles are designed to be shared. + if (const auto newDict{ e.NewValue().try_as() }) + { + mergedDictionaries.Append(newDict); + + if (frameworkElement.IsLoaded()) + { + _ForceControlToReloadThemeResources(frameworkElement); + } + } + } + + void StyleExtensions::_ForceControlToReloadThemeResources(const FrameworkElement& element) + { + // Toggle RequestedTheme to force the framework to re-resolve all + // {ThemeResource} bindings under this element. Required when the + // style is applied to an already-loaded element. Matches the toolkit. + const auto currentTheme{ element.RequestedTheme() }; + element.RequestedTheme(currentTheme == ElementTheme::Dark ? ElementTheme::Light : ElementTheme::Dark); + element.RequestedTheme(currentTheme); + } + + // Lazy singleton: loads SettingsControlsImplicitStyles.xaml exactly once + // for the process lifetime. We do NOT append this dictionary itself to any + // element's MergedDictionaries — that triggers "Element is already the + // child of another element" once a second element tries to merge it. + // Instead, EnsureImplicitStylesMergedInto copies the dictionary's entries + // (Style references) into the target's own Resources. Style instances are + // reference types but not UIElements, so sharing them across multiple + // elements' Resources collections is safe. + ResourceDictionary StyleExtensions::_SharedImplicitStylesDictionary() + { + if (!_sharedImplicitStylesDictionary) + { + try + { + auto dict{ ResourceDictionary{} }; + dict.Source(winrt::Windows::Foundation::Uri{ L"ms-appx:///Microsoft.Terminal.Settings.Editor/SettingsControlsImplicitStyles.xaml" }); + _sharedImplicitStylesDictionary = dict; + } + CATCH_LOG(); + } + return _sharedImplicitStylesDictionary; + } + + void StyleExtensions::EnsureImplicitStylesMergedInto(const FrameworkElement& target) + { + if (!target) + { + return; + } + + try + { + const auto resources{ target.Resources() }; + if (!resources) + { + return; + } + + // Idempotency marker: if we've already populated this element's + // Resources with the implicit styles, skip. Cheap to check + // (one hash lookup), independent of MergedDictionaries. + const auto markerKey{ box_value(hstring{ L"__SettingsControls_ImplicitStyles" }) }; + if (resources.HasKey(markerKey)) + { + return; + } + + const auto sharedDict{ _SharedImplicitStylesDictionary() }; + if (!sharedDict) + { + return; + } + + // Copy each entry (Style or other resource) from the shared loaded + // dictionary into the target's Resources. Style instances are + // reference types that can safely be shared across multiple + // element Resources collections (unlike UIElements, which have a + // single-parent constraint). We deliberately do NOT + // MergedDictionaries.Append(sharedDict) — that path throws + // "Element is already the child of another element" once a second + // element tries to merge the same shared dict. + for (const auto& kv : sharedDict) + { + if (!resources.HasKey(kv.Key())) + { + resources.Insert(kv.Key(), kv.Value()); + } + } + resources.Insert(markerKey, box_value(true)); + } + CATCH_LOG(); + } +} diff --git a/src/cascadia/TerminalSettingsEditor/StyleExtensions.h b/src/cascadia/TerminalSettingsEditor/StyleExtensions.h new file mode 100644 index 0000000000..764d93d7ce --- /dev/null +++ b/src/cascadia/TerminalSettingsEditor/StyleExtensions.h @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation +// Licensed under the MIT license. + +#pragma once + +#include "StyleExtensions.g.h" + +namespace winrt::Microsoft::Terminal::Settings::Editor::implementation +{ + struct StyleExtensions + { + StyleExtensions() = default; + + static Windows::UI::Xaml::DependencyProperty ResourcesProperty(); + + static Windows::UI::Xaml::ResourceDictionary GetResources(const Windows::UI::Xaml::DependencyObject& target); + static void SetResources(const Windows::UI::Xaml::DependencyObject& target, const Windows::UI::Xaml::ResourceDictionary& value); + + static void EnsureImplicitStylesMergedInto(const Windows::UI::Xaml::FrameworkElement& target); + + private: + static void _InitializeProperties(); + static void _OnResourcesChanged(const Windows::UI::Xaml::DependencyObject& d, const Windows::UI::Xaml::DependencyPropertyChangedEventArgs& e); + static void _ForceControlToReloadThemeResources(const Windows::UI::Xaml::FrameworkElement& element); + static Windows::UI::Xaml::ResourceDictionary _SharedImplicitStylesDictionary(); + + static Windows::UI::Xaml::DependencyProperty _resourcesProperty; + static Windows::UI::Xaml::ResourceDictionary _sharedImplicitStylesDictionary; + }; +} + +namespace winrt::Microsoft::Terminal::Settings::Editor::factory_implementation +{ + BASIC_FACTORY(StyleExtensions); +} diff --git a/src/cascadia/TerminalSettingsEditor/StyleExtensions.idl b/src/cascadia/TerminalSettingsEditor/StyleExtensions.idl new file mode 100644 index 0000000000..716151816e --- /dev/null +++ b/src/cascadia/TerminalSettingsEditor/StyleExtensions.idl @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace Microsoft.Terminal.Settings.Editor +{ + // Attached property that lets a Style merge an extra ResourceDictionary into + // the target FrameworkElement's Resources.MergedDictionaries. Used by + // SettingsCard / SettingsExpander to inject right-aligned ToggleSwitch and + // sized Slider / ComboBox / TextBox defaults so children laid out inside a + // card look right out of the box. Mirrors the Windows Community Toolkit's + // CommunityToolkit.WinUI.Controls.StyleExtensions. + static runtimeclass StyleExtensions + { + static Windows.UI.Xaml.DependencyProperty ResourcesProperty { get; }; + + static Windows.UI.Xaml.ResourceDictionary GetResources(Windows.UI.Xaml.DependencyObject target); + static void SetResources(Windows.UI.Xaml.DependencyObject target, Windows.UI.Xaml.ResourceDictionary value); + + // Idempotently merges the project's SettingsControlsImplicitStyles.xaml + // dictionary into the target's Resources.MergedDictionaries. Called from + // SettingsCard / SettingsExpander OnApplyTemplate to give children + // (ToggleSwitch / Slider / ComboBox / TextBox) sensible Windows 11 + // defaults without each call site setting Style explicitly. Loaded from + // C++ rather than via the attached DP because the WCT Setter.Value + // pattern crashes WinUI 2 in this codebase. + static void EnsureImplicitStylesMergedInto(Windows.UI.Xaml.FrameworkElement target); + } +} diff --git a/src/cascadia/TerminalSettingsEditor/Utils.h b/src/cascadia/TerminalSettingsEditor/Utils.h index 3e86fdc34c..8001cf137d 100644 --- a/src/cascadia/TerminalSettingsEditor/Utils.h +++ b/src/cascadia/TerminalSettingsEditor/Utils.h @@ -3,8 +3,6 @@ #pragma once -#include "SettingContainer.h" - // This macro must be used alongside GETSET_BINDABLE_ENUM_SETTING. // Use this in your class's constructor after Initialize_Component(). // It sorts and initializes the observable list of enum entries with the enum name diff --git a/tools/GenerateSettingsIndex.ps1 b/tools/GenerateSettingsIndex.ps1 index ca05a48e44..caebb12610 100644 --- a/tools/GenerateSettingsIndex.ps1 +++ b/tools/GenerateSettingsIndex.ps1 @@ -2,7 +2,7 @@ Copyright (c) Microsoft Corporation. Licensed under the MIT license. .SYNOPSIS -Scans XAML files for local:SettingContainer entries and generates GeneratedSettingsIndex.g.h / .g.cpp. +Scans XAML files for local:SettingsCard and local:SettingsExpander entries and generates GeneratedSettingsIndex.g.h / .g.cpp. .PARAMETER SourceDir Directory to scan recursively for .xaml files. @@ -23,7 +23,9 @@ $ProhibitedUids = @( "Profile_ProportionalFontFaces", "ColorScheme_InboxSchemeDuplicate", "ColorScheme_ColorsHeader", - "ColorScheme_Rename" + "ColorScheme_Rename", + "Profile_ResetProfile", + "Profile_DeleteProfile" ) # Prohibited XAML files (already limited to Page root elements) @@ -197,13 +199,13 @@ foreach ($xamlFile in Get-ChildItem -Path $SourceDir -Filter *.xaml) } } - # Iterate over all local:SettingContainer nodes - foreach ($settingContainer in $xml.SelectNodes("//local:SettingContainer", $xm)) + # Iterate over all local:SettingsCard and local:SettingsExpander nodes + foreach ($settingContainer in ($xml.SelectNodes("//local:SettingsCard", $xm) + $xml.SelectNodes("//local:SettingsExpander", $xm))) { # Extract Uid if ($null -eq $settingContainer.Uid) { - Write-Warning "No x:Uid found for a SettingContainer in file $filename. Skipping entry." + # SettingsCard/SettingsExpander without x:Uid are not indexable — skip silently continue } elseif ($ProhibitedUids -contains $settingContainer.Uid)