Compare commits

..

10 Commits

Author SHA1 Message Date
Carlos Zamora
6383fff42b complete port: visual styling confirmed to look like WCT 2026-05-19 19:24:34 -07:00
Carlos Zamora
58e3407de6 fix .Items and StyleExtensions 2026-05-19 17:51:18 -07:00
Carlos Zamora
a175b6291a fix disable handling for AlwaysShowTabs 2026-05-18 18:29:58 -07:00
Carlos Zamora
e60562d99d port chevron animation & click handling; use WCT styling 2026-05-18 17:36:50 -07:00
Carlos Zamora
96d0762a77 port over more WCT SettingsCard/Expander functionality 2026-05-18 15:06:28 -07:00
Carlos Zamora
c64b2433da code format 2026-05-15 13:28:47 -07:00
Carlos Zamora
17538eb499 Convert Navigators to SettingsCards 2026-05-15 13:28:30 -07:00
Carlos Zamora
edfc599d6d Consolidate SettingContainer styles 2026-05-15 12:50:53 -07:00
Carlos Zamora
acc1a59367 Port SettingsCard/Expander from WCT; have SettingContainer use it 2026-05-14 19:31:22 -07:00
aarushi singh
12e3455bb2 Make DECSTR cursor restore behave like xterm in alt screen buffer (#20032)
There is some disagreement among terminal emulators as to whether DECSTR
(and therefore, soft reset) restores the cursor in the active buffer or
in all buffers.

We had previously chosen to restore the cursor in all buffers.

xterm restores the cursor only in the active buffer.

Closes #19918
2026-05-14 17:04:58 +00:00
51 changed files with 3446 additions and 1649 deletions

View File

@@ -604,13 +604,6 @@ namespace winrt::TerminalApp::implementation
args.Handled(true);
}
void TerminalPage::_HandleToggleOverview(const IInspectable& /*sender*/,
const ActionEventArgs& args)
{
ToggleOverview();
args.Handled(true);
}
void TerminalPage::_HandleSetFocusMode(const IInspectable& /*sender*/,
const ActionEventArgs& args)
{

View File

@@ -1,690 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "OverviewPane.h"
#include "OverviewPane.g.cpp"
using namespace winrt;
using namespace winrt::TerminalApp;
using namespace winrt::Windows::Foundation;
using namespace winrt::Windows::UI::Xaml;
using namespace winrt::Windows::UI::Xaml::Controls;
using namespace winrt::Windows::UI::Xaml::Input;
using namespace winrt::Windows::UI::Xaml::Media;
using namespace winrt::Windows::UI::Xaml::Media::Animation;
using namespace winrt::Windows::System;
namespace winrt::TerminalApp::implementation
{
static constexpr double PreviewCellWidth = 320.0;
static constexpr double PreviewCellHeight = 220.0;
static constexpr std::chrono::milliseconds EnterAnimDuration{ 400 };
static constexpr std::chrono::milliseconds ExitAnimDuration{ 100 };
OverviewPane::OverviewPane()
{
InitializeComponent();
// Apply the initial background (opaque by default, since _useMica
// starts false). Without this the XAML-declared brush stays in place
// until someone explicitly calls UseMica().
_UpdateBackgroundForMica();
// Listen for layout passes so we can start the zoom-in animation
// once the WrapGrid has measured and positioned its children.
PreviewGrid().LayoutUpdated([weakThis = get_weak()](auto&&, auto&&) {
if (auto self = weakThis.get())
{
if (!self->_pendingEnterAnimation)
{
return;
}
// Check that at least the first cell is laid out
auto items = self->PreviewGrid().Items();
if (items.Size() == 0)
{
return;
}
auto first = items.GetAt(0).try_as<FrameworkElement>();
if (!first || first.ActualWidth() <= 0)
{
return;
}
self->_pendingEnterAnimation = false;
self->_StartEnterZoomAnimation();
}
});
}
OverviewPane::~OverviewPane()
{
ClearTabContent();
}
void OverviewPane::UpdateTabContent(Windows::Foundation::Collections::IVector<TerminalApp::Tab> tabs, int32_t focusedIndex)
{
// Clear any previous state
ClearTabContent();
if (!tabs || tabs.Size() == 0)
{
return;
}
const auto itemsControl = PreviewGrid();
// Determine a reference size from the currently visible tab's content.
// Inactive tabs have zero ActualWidth/Height since they're not laid out
// in the visual tree, but all tabs share the same content area, so the
// active tab's size is the right reference for all of them.
double referenceWidth = 0;
double referenceHeight = 0;
if (focusedIndex >= 0 && focusedIndex < static_cast<int32_t>(tabs.Size()))
{
auto focusedContent = tabs.GetAt(static_cast<uint32_t>(focusedIndex)).Content();
if (focusedContent)
{
referenceWidth = focusedContent.ActualWidth();
referenceHeight = focusedContent.ActualHeight();
}
}
for (uint32_t i = 0; i < tabs.Size(); i++)
{
const auto& tab = tabs.GetAt(i);
auto cell = _BuildPreviewCell(tab, static_cast<int32_t>(i), referenceWidth, referenceHeight);
itemsControl.Items().Append(cell);
}
_selectedIndex = focusedIndex;
_UpdateSelection();
_PlayEnterAnimation();
// Focus self for keyboard input
Focus(FocusState::Programmatic);
}
void OverviewPane::ClearTabContent()
{
_pendingEnterAnimation = false;
// Stop any running exit animation
if (_exitContentStoryboard)
{
_exitContentStoryboard.Completed(_exitAnimationToken);
_exitAnimationToken = {};
_exitContentStoryboard.Stop();
_exitContentStoryboard = nullptr;
}
// Reset the zoom transform to identity
auto transform = ContentTransform();
transform.ScaleX(1.0);
transform.ScaleY(1.0);
transform.TranslateX(0.0);
transform.TranslateY(0.0);
ContentWrapper().Opacity(1.0);
// Reparent content back to original parents
for (auto& entry : _reparentedContent)
{
if (entry.content)
{
// Restore original Width/Height (NaN = auto-sizing)
entry.content.Width(entry.originalWidth);
entry.content.Height(entry.originalHeight);
// Restore original RenderTransform and origin
entry.content.RenderTransform(entry.originalRenderTransform);
entry.content.RenderTransformOrigin(entry.originalRenderTransformOrigin);
_DetachContent(entry.content);
// Put it back where it came from
if (entry.originalParent)
{
entry.originalParent.Children().Append(entry.content);
}
}
}
_reparentedContent.clear();
const auto itemsControl = PreviewGrid();
itemsControl.Items().Clear();
}
int32_t OverviewPane::SelectedIndex() const
{
return _selectedIndex;
}
void OverviewPane::SelectedIndex(int32_t value)
{
if (_selectedIndex != value)
{
_selectedIndex = value;
_UpdateSelection();
}
}
bool OverviewPane::UseMica() const
{
return _useMica;
}
void OverviewPane::UseMica(bool value)
{
if (_useMica != value)
{
_useMica = value;
_UpdateBackgroundForMica();
}
}
void OverviewPane::_UpdateBackgroundForMica()
{
auto overlay = BackgroundOverlay();
if (_useMica)
{
// Transparent background — let the Mica backdrop show through
overlay.Background(SolidColorBrush{ winrt::Windows::UI::Colors::Transparent() });
}
else
{
// Opaque background when Mica is not active.
// Use the theme-aware SolidBackgroundFillColorBaseBrush so we
// match the correct color for light / dark theme.
auto res = Application::Current().Resources();
auto brush = res.TryLookup(winrt::box_value(L"SolidBackgroundFillColorBaseBrush"));
if (brush)
{
overlay.Background(brush.as<Brush>());
}
else
{
overlay.Background(SolidColorBrush{ winrt::Windows::UI::ColorHelper::FromArgb(255, 32, 32, 32) });
}
}
}
void OverviewPane::_OnPreviewKeyDown(const IInspectable& /*sender*/, const KeyRoutedEventArgs& e)
{
if (e.OriginalKey() != VirtualKey::Tab)
{
return;
}
const auto items = PreviewGrid().Items();
const auto itemCount = static_cast<int32_t>(items.Size());
if (itemCount == 0)
{
return;
}
const auto shiftPressed = (Windows::UI::Core::CoreWindow::GetForCurrentThread().GetKeyState(VirtualKey::Shift) & Windows::UI::Core::CoreVirtualKeyStates::Down) == Windows::UI::Core::CoreVirtualKeyStates::Down;
if (shiftPressed)
{
if (_selectedIndex > 0)
{
_selectedIndex--;
_UpdateSelection();
}
}
else
{
if (_selectedIndex < itemCount - 1)
{
_selectedIndex++;
_UpdateSelection();
}
}
e.Handled(true);
}
void OverviewPane::_OnKeyDown(const IInspectable& /*sender*/, const KeyRoutedEventArgs& e)
{
const auto items = PreviewGrid().Items();
const auto itemCount = static_cast<int32_t>(items.Size());
if (itemCount == 0)
{
return;
}
auto handled = true;
switch (e.OriginalKey())
{
case VirtualKey::Left:
if (_selectedIndex > 0)
{
_selectedIndex--;
_UpdateSelection();
}
break;
case VirtualKey::Right:
if (_selectedIndex < itemCount - 1)
{
_selectedIndex++;
_UpdateSelection();
}
break;
case VirtualKey::Up:
if (_selectedIndex - _columnCount >= 0)
{
_selectedIndex -= _columnCount;
_UpdateSelection();
}
break;
case VirtualKey::Down:
if (_selectedIndex + _columnCount < itemCount)
{
_selectedIndex += _columnCount;
_UpdateSelection();
}
break;
case VirtualKey::Enter:
_OnItemClicked(_selectedIndex);
break;
case VirtualKey::Escape:
_PlayExitAnimation([weakThis = get_weak()]() {
if (auto self = weakThis.get())
{
self->Dismissed.raise(*self, nullptr);
}
});
break;
default:
handled = false;
break;
}
e.Handled(handled);
}
void OverviewPane::_OnItemClicked(int32_t index)
{
_PlayExitAnimation([weakThis = get_weak(), index]() {
if (auto self = weakThis.get())
{
self->TabSelected.raise(*self, winrt::Windows::Foundation::IReference<int32_t>{ index });
}
});
}
void OverviewPane::_UpdateSelection()
{
const auto items = PreviewGrid().Items();
const auto itemCount = static_cast<int32_t>(items.Size());
// Clamp selection
_selectedIndex = std::clamp(_selectedIndex, 0, std::max(0, itemCount - 1));
for (int32_t i = 0; i < itemCount; i++)
{
if (auto cellElement = items.GetAt(i).try_as<FrameworkElement>())
{
if (auto border = cellElement.try_as<Border>())
{
if (i == _selectedIndex)
{
// Accent-colored border for selected item
const auto accentBrush = Application::Current()
.Resources()
.Lookup(winrt::box_value(L"SystemAccentColor"))
.as<winrt::Windows::UI::Color>();
border.BorderBrush(SolidColorBrush{ accentBrush });
border.BorderThickness(ThicknessHelper::FromUniformLength(2));
// Scroll into view if needed
border.StartBringIntoView();
}
else
{
border.BorderBrush(SolidColorBrush{ winrt::Windows::UI::Colors::Transparent() });
border.BorderThickness(ThicknessHelper::FromUniformLength(2));
}
}
}
}
}
void OverviewPane::_PlayEnterAnimation()
{
// Hide the content wrapper until the LayoutUpdated callback fires
// and we can read cell positions to set up the zoom transform.
ContentWrapper().Opacity(0);
_pendingEnterAnimation = true;
}
void OverviewPane::_StartEnterZoomAnimation()
{
// Start the background fade-in together with the zoom so both
// animations are visible at the same moment (avoids opacity flash).
if (auto bgSb = Resources().Lookup(winrt::box_value(L"BackgroundFadeIn")).try_as<Storyboard>())
{
bgSb.Begin();
}
auto wrapper = ContentWrapper();
auto transform = ContentTransform();
auto zoomParams = _GetZoomParamsForCell(_selectedIndex);
if (!zoomParams)
{
wrapper.Opacity(1.0);
return;
}
auto [scale, tx, ty] = *zoomParams;
// Set the initial transform so the focused cell fills the viewport
transform.ScaleX(scale);
transform.ScaleY(scale);
transform.TranslateX(tx);
transform.TranslateY(ty);
wrapper.Opacity(1.0);
// Animate from the zoomed-in state to identity (zoom out to grid)
Storyboard storyboard;
const auto duration = DurationHelper::FromTimeSpan(EnterAnimDuration);
CubicEase easing;
easing.EasingMode(EasingMode::EaseOut);
_AddDoubleAnimation(storyboard, transform, L"ScaleX", scale, 1.0, duration, easing);
_AddDoubleAnimation(storyboard, transform, L"ScaleY", scale, 1.0, duration, easing);
_AddDoubleAnimation(storyboard, transform, L"TranslateX", tx, 0.0, duration, easing);
_AddDoubleAnimation(storyboard, transform, L"TranslateY", ty, 0.0, duration, easing);
storyboard.Begin();
}
void OverviewPane::_PlayExitAnimation(std::function<void()> onComplete)
{
// Fade out the background overlay
if (auto bgSb = Resources().Lookup(winrt::box_value(L"BackgroundFadeOut")).try_as<Storyboard>())
{
bgSb.Begin();
}
auto zoomParams = _GetZoomParamsForCell(_selectedIndex);
if (!zoomParams)
{
if (onComplete)
{
onComplete();
}
return;
}
auto [scale, tx, ty] = *zoomParams;
// Animate from the current grid view into the selected cell
auto transform = ContentTransform();
Storyboard storyboard;
const auto duration = DurationHelper::FromTimeSpan(ExitAnimDuration);
CubicEase easing;
easing.EasingMode(EasingMode::EaseIn);
_AddDoubleAnimation(storyboard, transform, L"ScaleX", 1.0, scale, duration, easing);
_AddDoubleAnimation(storyboard, transform, L"ScaleY", 1.0, scale, duration, easing);
_AddDoubleAnimation(storyboard, transform, L"TranslateX", 0.0, tx, duration, easing);
_AddDoubleAnimation(storyboard, transform, L"TranslateY", 0.0, ty, duration, easing);
// Revoke any previously registered Completed handler
if (_exitContentStoryboard)
{
_exitContentStoryboard.Completed(_exitAnimationToken);
}
_exitContentStoryboard = storyboard;
_exitAnimationToken = storyboard.Completed([weakThis = get_weak(), onComplete](auto&&, auto&&) {
if (auto self = weakThis.get())
{
if (onComplete)
{
onComplete();
}
}
});
storyboard.Begin();
}
std::optional<std::tuple<double, double, double>> OverviewPane::_GetZoomParamsForCell(int32_t index)
{
auto wrapper = ContentWrapper();
const auto vpW = wrapper.ActualWidth();
const auto vpH = wrapper.ActualHeight();
if (vpW <= 0 || vpH <= 0)
{
return std::nullopt;
}
auto items = PreviewGrid().Items();
if (index < 0 || index >= static_cast<int32_t>(items.Size()))
{
return std::nullopt;
}
auto cell = items.GetAt(static_cast<uint32_t>(index)).try_as<FrameworkElement>();
if (!cell || cell.ActualWidth() <= 0 || cell.ActualHeight() <= 0)
{
return std::nullopt;
}
const auto cellW = cell.ActualWidth();
const auto cellH = cell.ActualHeight();
// Cell center relative to ContentWrapper (accounts for scroll offset)
auto cellTransform = cell.TransformToVisual(wrapper);
auto topLeft = cellTransform.TransformPoint({ 0.0f, 0.0f });
const auto cellCX = static_cast<double>(topLeft.X) + cellW / 2.0;
const auto cellCY = static_cast<double>(topLeft.Y) + cellH / 2.0;
// Scale so the cell fits the viewport
const auto scale = std::min(vpW / cellW, vpH / cellH);
// With RenderTransformOrigin={0.5,0.5} the scale origin is the
// center of ContentWrapper. Translate so the cell center lands
// at the viewport center after scaling.
const auto translateX = (vpW / 2.0 - cellCX) * scale;
const auto translateY = (vpH / 2.0 - cellCY) * scale;
return std::tuple{ scale, translateX, translateY };
}
FrameworkElement OverviewPane::_BuildPreviewCell(const TerminalApp::Tab& tab, int32_t index, double referenceWidth, double referenceHeight)
{
// Outer border — serves as the selection indicator
Border outerBorder;
outerBorder.BorderBrush(SolidColorBrush{ winrt::Windows::UI::Colors::Transparent() });
outerBorder.BorderThickness(ThicknessHelper::FromUniformLength(2));
outerBorder.CornerRadius({ 8, 8, 8, 8 });
outerBorder.Padding(ThicknessHelper::FromUniformLength(4));
outerBorder.Margin(ThicknessHelper::FromUniformLength(6));
// Vertical stack: preview box + title
StackPanel cellStack;
cellStack.Orientation(Orientation::Vertical);
cellStack.HorizontalAlignment(HorizontalAlignment::Center);
// Preview container with a dark background
Border previewBorder;
previewBorder.Width(PreviewCellWidth);
previewBorder.Height(PreviewCellHeight);
previewBorder.CornerRadius({ 6, 6, 6, 6 });
auto bgBrush = Application::Current().Resources().TryLookup(winrt::box_value(L"SystemControlBackgroundChromeMediumBrush"));
if (bgBrush)
{
previewBorder.Background(bgBrush.as<Brush>());
}
else
{
previewBorder.Background(SolidColorBrush{ winrt::Windows::UI::ColorHelper::FromArgb(255, 30, 30, 30) });
}
// Get the tab's content and reparent it with a ScaleTransform
auto tabContent = tab.Content();
if (tabContent)
{
// Save the Width/Height *property* values (likely NaN for auto-sized
// elements). These differ from ActualWidth/Height (the rendered size).
// We need the property values to restore auto-sizing on exit.
const auto origWidthProp = tabContent.Width();
const auto origHeightProp = tabContent.Height();
const auto origRenderTransform = tabContent.RenderTransform();
const auto origRenderTransformOrigin = tabContent.RenderTransformOrigin();
// Use ActualWidth/Height if the content is currently laid out (active tab).
// Inactive tabs aren't in the visual tree and report zero — fall back
// to the reference size from the active tab's content area.
auto layoutWidth = tabContent.ActualWidth();
auto layoutHeight = tabContent.ActualHeight();
if (layoutWidth <= 0 || layoutHeight <= 0)
{
layoutWidth = referenceWidth;
layoutHeight = referenceHeight;
}
auto originalParent = VisualTreeHelper::GetParent(tabContent).try_as<Panel>();
// XAML single-parent rule: remove from current parent first
_DetachContent(tabContent);
// Lock the content to the layout size — this prevents
// TermControl from seeing a resize and reflowing its buffer
if (layoutWidth > 0 && layoutHeight > 0)
{
tabContent.Width(layoutWidth);
tabContent.Height(layoutHeight);
// Calculate uniform scale to fit in preview
const double previewWidth = PreviewCellWidth;
const double previewHeight = PreviewCellHeight;
const double scale = std::min(previewWidth / layoutWidth, previewHeight / layoutHeight);
// RenderTransform is applied AFTER layout — the content still
// thinks it's at its original size
ScaleTransform scaleTransform;
scaleTransform.ScaleX(scale);
scaleTransform.ScaleY(scale);
tabContent.RenderTransform(scaleTransform);
tabContent.RenderTransformOrigin({ 0.0f, 0.0f });
// Use a Canvas so the content is not constrained by the preview
// container's layout. Canvas gives children infinite measure
// space and arranges at desired size.
Canvas canvas;
canvas.Width(previewWidth);
canvas.Height(previewHeight);
// Clip to preview bounds so the scaled content doesn't overflow
RectangleGeometry clipGeometry;
clipGeometry.Rect({ 0, 0, static_cast<float>(previewWidth), static_cast<float>(previewHeight) });
canvas.Clip(clipGeometry);
canvas.Children().Append(tabContent);
// Layer the canvas behind a transparent overlay so
// pointer events never reach the TermControl content.
Grid previewGrid;
previewGrid.Children().Append(canvas);
Border inputOverlay;
inputOverlay.Background(SolidColorBrush{ winrt::Windows::UI::Colors::Transparent() });
previewGrid.Children().Append(inputOverlay);
previewBorder.Child(previewGrid);
}
_reparentedContent.push_back({ tabContent, originalParent, origWidthProp, origHeightProp, origRenderTransform, origRenderTransformOrigin });
}
cellStack.Children().Append(previewBorder);
// Tab title text
TextBlock titleBlock;
titleBlock.Text(tab.Title());
titleBlock.FontSize(14);
titleBlock.Foreground(SolidColorBrush{ winrt::Windows::UI::Colors::White() });
titleBlock.HorizontalAlignment(HorizontalAlignment::Center);
titleBlock.TextTrimming(TextTrimming::CharacterEllipsis);
titleBlock.MaxWidth(PreviewCellWidth);
titleBlock.Margin({ 0, 6, 0, 0 });
cellStack.Children().Append(titleBlock);
outerBorder.Child(cellStack);
// Click handler
outerBorder.PointerPressed([weakThis = get_weak(), index](auto&&, auto&&) {
if (auto self = weakThis.get())
{
self->_selectedIndex = index;
self->_UpdateSelection();
self->_OnItemClicked(index);
}
});
// Hover effect
outerBorder.PointerEntered([weakThis = get_weak(), index](auto&&, auto&&) {
if (auto self = weakThis.get())
{
self->_selectedIndex = index;
self->_UpdateSelection();
}
});
return outerBorder;
}
void OverviewPane::_AddDoubleAnimation(
const Storyboard& storyboard,
const CompositeTransform& target,
const hstring& property,
double from,
double to,
const Duration& duration,
const EasingFunctionBase& easing)
{
DoubleAnimation anim;
anim.From(from);
anim.To(to);
anim.Duration(duration);
anim.EasingFunction(easing);
Storyboard::SetTarget(anim, target);
Storyboard::SetTargetProperty(anim, property);
storyboard.Children().Append(anim);
}
void OverviewPane::_DetachContent(const FrameworkElement& content)
{
// Try removing from various XAML container types
if (auto parent = VisualTreeHelper::GetParent(content))
{
if (auto panel = parent.try_as<Panel>())
{
uint32_t idx;
if (panel.Children().IndexOf(content, idx))
{
panel.Children().RemoveAt(idx);
}
}
else if (auto border = parent.try_as<Border>())
{
border.Child(nullptr);
}
else if (auto contentControl = parent.try_as<ContentControl>())
{
contentControl.Content(nullptr);
}
else if (auto viewbox = parent.try_as<Viewbox>())
{
viewbox.Child(nullptr);
}
}
}
}

View File

@@ -1,77 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#include <limits>
#include <optional>
#include "OverviewPane.g.h"
namespace winrt::TerminalApp::implementation
{
struct OverviewPane : OverviewPaneT<OverviewPane>
{
OverviewPane();
~OverviewPane();
void UpdateTabContent(Windows::Foundation::Collections::IVector<TerminalApp::Tab> tabs, int32_t focusedIndex);
void ClearTabContent();
int32_t SelectedIndex() const;
void SelectedIndex(int32_t value);
bool UseMica() const;
void UseMica(bool value);
// Events
til::typed_event<Windows::Foundation::IInspectable, Windows::Foundation::IReference<int32_t>> TabSelected;
til::typed_event<> Dismissed;
private:
friend struct OverviewPaneT<OverviewPane>; // for Xaml to bind events
void _OnKeyDown(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::Input::KeyRoutedEventArgs& e);
void _OnPreviewKeyDown(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::Input::KeyRoutedEventArgs& e);
void _OnItemClicked(int32_t index);
void _UpdateSelection();
void _PlayEnterAnimation();
void _PlayExitAnimation(std::function<void()> onComplete = nullptr);
void _StartEnterZoomAnimation();
std::optional<std::tuple<double, double, double>> _GetZoomParamsForCell(int32_t index);
Windows::UI::Xaml::FrameworkElement _BuildPreviewCell(const TerminalApp::Tab& tab, int32_t index, double referenceWidth, double referenceHeight);
void _DetachContent(const Windows::UI::Xaml::FrameworkElement& content);
static void _AddDoubleAnimation(
const Windows::UI::Xaml::Media::Animation::Storyboard& storyboard,
const Windows::UI::Xaml::Media::CompositeTransform& target,
const hstring& property,
double from,
double to,
const Windows::UI::Xaml::Duration& duration,
const Windows::UI::Xaml::Media::Animation::EasingFunctionBase& easing);
int32_t _selectedIndex{ 0 };
int32_t _columnCount{ 3 }; // must match WrapGrid MaximumRowsOrColumns in OverviewPane.xaml
bool _pendingEnterAnimation{ false };
bool _useMica{ false };
void _UpdateBackgroundForMica();
winrt::event_token _exitAnimationToken{};
Windows::UI::Xaml::Media::Animation::Storyboard _exitContentStoryboard{ nullptr };
struct ReparentedEntry
{
Windows::UI::Xaml::FrameworkElement content{ nullptr };
Windows::UI::Xaml::Controls::Panel originalParent{ nullptr };
double originalWidth{ std::numeric_limits<double>::quiet_NaN() };
double originalHeight{ std::numeric_limits<double>::quiet_NaN() };
Windows::UI::Xaml::Media::Transform originalRenderTransform{ nullptr };
Windows::Foundation::Point originalRenderTransformOrigin{ 0.0f, 0.0f };
};
std::vector<ReparentedEntry> _reparentedContent;
};
}
namespace winrt::TerminalApp::factory_implementation
{
BASIC_FACTORY(OverviewPane);
}

View File

@@ -1,19 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import "Tab.idl";
namespace TerminalApp
{
[default_interface] runtimeclass OverviewPane : Windows.UI.Xaml.Controls.UserControl
{
OverviewPane();
void UpdateTabContent(Windows.Foundation.Collections.IVector<Tab> tabs, Int32 focusedIndex);
void ClearTabContent();
Int32 SelectedIndex;
Boolean UseMica;
event Windows.Foundation.TypedEventHandler<Object, Windows.Foundation.IReference<Int32> > TabSelected;
event Windows.Foundation.TypedEventHandler<Object, Object> Dismissed;
}
}

View File

@@ -1,88 +0,0 @@
<!--
Copyright (c) Microsoft Corporation. All rights reserved. Licensed under
the MIT License. See LICENSE in the project root for license information.
-->
<UserControl x:Class="TerminalApp.OverviewPane"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:TerminalApp"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
AllowFocusOnInteraction="True"
IsTabStop="True"
KeyDown="_OnKeyDown"
PreviewKeyDown="_OnPreviewKeyDown"
mc:Ignorable="d">
<UserControl.Resources>
<ResourceDictionary>
<Storyboard x:Key="BackgroundFadeIn">
<DoubleAnimation Duration="0:0:0.2"
Storyboard.TargetName="BackgroundOverlay"
Storyboard.TargetProperty="Opacity"
From="0.0"
To="1.0">
<DoubleAnimation.EasingFunction>
<QuadraticEase EasingMode="EaseOut" />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
<Storyboard x:Key="BackgroundFadeOut">
<DoubleAnimation Duration="0:0:0.15"
Storyboard.TargetName="BackgroundOverlay"
Storyboard.TargetProperty="Opacity"
From="1.0"
To="0.0">
<DoubleAnimation.EasingFunction>
<QuadraticEase EasingMode="EaseIn" />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</ResourceDictionary>
</UserControl.Resources>
<Grid x:Name="RootGrid"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<!-- Semi-transparent dark overlay -->
<Border x:Name="BackgroundOverlay"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="{ThemeResource SmokeFillColorDefaultBrush}" />
<!-- Zoom-animated wrapper around the scrollable content -->
<Grid x:Name="ContentWrapper"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
RenderTransformOrigin="0.5,0.5">
<Grid.RenderTransform>
<CompositeTransform x:Name="ContentTransform" />
</Grid.RenderTransform>
<ScrollViewer x:Name="PreviewScrollViewer"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<!-- WrapGrid-style layout using ItemsControl with WrapGrid panel -->
<ItemsControl x:Name="PreviewGrid"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="24">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapGrid HorizontalChildrenAlignment="Left"
ItemHeight="270"
ItemWidth="332"
MaximumRowsOrColumns="3"
Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</ScrollViewer>
</Grid>
</Grid>
</UserControl>

View File

@@ -1131,13 +1131,6 @@ namespace winrt::TerminalApp::implementation
{
if (!_rearranging && !_removing)
{
// If the user clicked a tab in the tab row while the overview is
// open, the overview is still holding that tab's Content reparented
// into one of its preview cells. Tear down the overview visuals
// first so _UpdatedSelectedTab can mount the content into the
// active content area without hitting the XAML single-parent rule.
_DismissOverviewVisuals();
auto tabView = sender.as<MUX::Controls::TabView>();
auto selectedIndex = tabView.SelectedIndex();
if (selectedIndex >= 0 && selectedIndex < gsl::narrow_cast<int32_t>(_tabs.Size()))

View File

@@ -71,9 +71,6 @@
<Page Include="CommandPalette.xaml">
<SubType>Designer</SubType>
</Page>
<Page Include="OverviewPane.xaml">
<SubType>Designer</SubType>
</Page>
<Page Include="SuggestionsControl.xaml">
<SubType>Designer</SubType>
</Page>
@@ -135,9 +132,6 @@
<ClInclude Include="CommandPalette.h">
<DependentUpon>CommandPalette.xaml</DependentUpon>
</ClInclude>
<ClInclude Include="OverviewPane.h">
<DependentUpon>OverviewPane.xaml</DependentUpon>
</ClInclude>
<ClInclude Include="FilteredCommand.h" />
<ClInclude Include="Pane.h" />
<ClInclude Include="../fzf/fzf.h" />
@@ -249,9 +243,6 @@
<ClCompile Include="CommandPalette.cpp">
<DependentUpon>CommandPalette.xaml</DependentUpon>
</ClCompile>
<ClCompile Include="OverviewPane.cpp">
<DependentUpon>OverviewPane.xaml</DependentUpon>
</ClCompile>
<ClCompile Include="FilteredCommand.cpp" />
<ClCompile Include="Pane.cpp" />
<ClCompile Include="Pane.LayoutSizeNode.cpp" />
@@ -363,10 +354,6 @@
<DependentUpon>CommandPalette.xaml</DependentUpon>
<SubType>Code</SubType>
</Midl>
<Midl Include="OverviewPane.idl">
<DependentUpon>OverviewPane.xaml</DependentUpon>
<SubType>Code</SubType>
</Midl>
<Midl Include="SuggestionsControl.idl">
<DependentUpon>SuggestionsControl.xaml</DependentUpon>
<SubType>Code</SubType>

View File

@@ -1838,17 +1838,6 @@ namespace winrt::TerminalApp::implementation
return;
}
// If the overview pane is open, most actions mutate tab state whose
// Content() is currently reparented under the overview. Tear down the
// overview's visuals first so the content is back under its original
// parent before the action runs. ToggleOverview is the exception — it
// handles its own exit.
if (_isInOverviewMode &&
cmd.ActionAndArgs().Action() != ShortcutAction::ToggleOverview)
{
_DismissOverviewVisuals();
}
if (!_actionDispatch->DoAction(cmd.ActionAndArgs()))
{
return;
@@ -4280,120 +4269,6 @@ namespace winrt::TerminalApp::implementation
AlwaysOnTopChanged.raise(*this, nullptr);
}
void TerminalPage::ToggleOverview()
{
if (_isInOverviewMode)
{
_ExitOverview(std::nullopt);
}
else
{
_EnterOverview();
}
}
void TerminalPage::_EnterOverview()
{
_isInOverviewMode = true;
// Use FindName to lazily load the OverviewPane (same pattern as CommandPalette)
auto overview = FindName(L"OverviewPaneElement").try_as<OverviewPane>();
if (!overview)
{
return;
}
const auto focusedIdx = _GetFocusedTabIndex();
const auto idx = focusedIdx.has_value() ? static_cast<int32_t>(focusedIdx.value()) : 0;
// Wire up events to handle tab selection and dismissal
_overviewTabSelectedToken = overview.TabSelected([weakThis = get_weak()](auto&&, const auto& args) {
if (auto self = weakThis.get())
{
if (args)
{
self->_ExitOverview(static_cast<uint32_t>(args.Value()));
}
}
});
_overviewDismissedToken = overview.Dismissed([weakThis = get_weak()](auto&&, auto&&) {
if (auto self = weakThis.get())
{
self->_ExitOverview(std::nullopt);
}
});
// _tabs is already IObservableVector<Tab>, which inherits from IVector<Tab>
const auto theme = _settings.GlobalSettings().CurrentTheme();
const auto windowTheme = theme ? theme.Window() : nullptr;
overview.UseMica(windowTheme ? windowTheme.UseMica() : false);
overview.UpdateTabContent(_tabs, idx);
overview.Visibility(WUX::Visibility::Visible);
overview.Focus(WUX::FocusState::Programmatic);
}
void TerminalPage::_ExitOverview(const std::optional<uint32_t>& selectedIndex)
{
auto overview = FindName(L"OverviewPaneElement").try_as<OverviewPane>();
// Determine which tab to switch to. Prefer the explicitly passed index
// (from TabSelected event), but fall back to the overview pane's
// current selection — this covers ToggleOverview and Dismiss paths so
// the user always lands on whichever tab they navigated to.
std::optional<uint32_t> tabToSelect = selectedIndex;
if (!tabToSelect.has_value() && overview)
{
const auto overviewIdx = overview.SelectedIndex();
if (overviewIdx >= 0 && overviewIdx < static_cast<int32_t>(_tabs.Size()))
{
tabToSelect = static_cast<uint32_t>(overviewIdx);
}
}
_DismissOverviewVisuals();
if (tabToSelect.has_value())
{
_SelectTab(tabToSelect.value());
}
_UpdatedSelectedTab(_GetFocusedTab());
}
// Method Description:
// - Tears down the overview's reparented content and hides the overlay,
// without changing tab selection. Safe to call when not in overview mode.
// - Used by both _ExitOverview (which then selects a tab) and by
// _OnTabSelectionChanged (where the TabView has already updated the
// selection and we just need to release the reparented content before
// _UpdatedSelectedTab tries to mount it back into the content area).
void TerminalPage::_DismissOverviewVisuals()
{
if (!_isInOverviewMode)
{
return;
}
if (auto overview = FindName(L"OverviewPaneElement").try_as<OverviewPane>())
{
// Revoke event handlers to avoid stale callbacks
overview.TabSelected(_overviewTabSelectedToken);
overview.Dismissed(_overviewDismissedToken);
_overviewTabSelectedToken = {};
_overviewDismissedToken = {};
overview.ClearTabContent();
overview.Visibility(WUX::Visibility::Collapsed);
}
_isInOverviewMode = false;
}
bool TerminalPage::OverviewMode() const
{
return _isInOverviewMode;
}
// Method Description:
// - Sets the tab split button color when a new tab color is selected
// Arguments:

View File

@@ -138,11 +138,9 @@ namespace winrt::TerminalApp::implementation
void ToggleFocusMode();
void ToggleFullscreen();
void ToggleAlwaysOnTop();
void ToggleOverview();
bool FocusMode() const;
bool Fullscreen() const;
bool AlwaysOnTop() const;
bool OverviewMode() const;
bool ShowTabsFullscreen() const;
void SetShowTabsFullscreen(bool newShowTabsFullscreen);
void SetFullscreen(bool);
@@ -252,9 +250,6 @@ namespace winrt::TerminalApp::implementation
TerminalApp::Tab _settingsTab{ nullptr };
bool _isInFocusMode{ false };
bool _isInOverviewMode{ false };
winrt::event_token _overviewTabSelectedToken{};
winrt::event_token _overviewDismissedToken{};
bool _isFullscreen{ false };
bool _isMaximized{ false };
bool _isAlwaysOnTop{ false };
@@ -380,9 +375,6 @@ namespace winrt::TerminalApp::implementation
void _SelectNextTab(const bool bMoveRight, const Windows::Foundation::IReference<Microsoft::Terminal::Settings::Model::TabSwitcherMode>& customTabSwitcherMode);
bool _SelectTab(uint32_t tabIndex);
void _EnterOverview();
void _ExitOverview(const std::optional<uint32_t>& selectedIndex);
void _DismissOverviewVisuals();
bool _MoveFocus(const Microsoft::Terminal::Settings::Model::FocusDirection& direction);
bool _SwapPane(const Microsoft::Terminal::Settings::Model::FocusDirection& direction);
bool _MovePane(const Microsoft::Terminal::Settings::Model::MovePaneArgs args);

View File

@@ -61,7 +61,6 @@ namespace TerminalApp
Boolean FocusMode { get; };
Boolean Fullscreen { get; };
Boolean AlwaysOnTop { get; };
Boolean OverviewMode { get; };
WindowProperties WindowProperties { get; };
void IdentifyWindow();

View File

@@ -155,12 +155,6 @@
</TextBlock>
</ContentDialog>
<local:OverviewPane x:Name="OverviewPaneElement"
Grid.Row="2"
x:Load="False"
PreviewKeyDown="_KeyDownHandler"
Visibility="Collapsed" />
<local:CommandPalette x:Name="CommandPaletteElement"
Grid.Row="2"
VerticalAlignment="Stretch"

View File

@@ -2906,10 +2906,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation
}
else
{
// Yes, this is reachable: when a pane is split, the new TermControl
// is added to the XAML tree before its Loaded event fires, so
// _initializedTerminal is still false when layout queries MinimumSize().
// Return a small fallback — the real size will be used once initialized.
return { 10, 10 };
}
}

View File

@@ -81,7 +81,7 @@
CurrentValueAccessibleName="{x:Bind Appearance.CurrentColorScheme.Name, Mode=OneWay}"
HasSettingValue="{x:Bind Appearance.HasDarkColorSchemeName, Mode=OneWay}"
SettingOverrideSource="{x:Bind Appearance.DarkColorSchemeNameOverrideSource, Mode=OneWay}"
Style="{StaticResource ExpanderSettingContainerStyleWithComplexPreview}">
Style="{StaticResource ExpanderSettingContainerStyle}">
<local:SettingContainer.CurrentValue>
<ComboBox Padding="4"
ItemsSource="{x:Bind Appearance.SchemesList, Mode=OneWay}"
@@ -220,7 +220,7 @@
CurrentValueTemplate="{StaticResource ColorPreviewTemplate}"
HasSettingValue="{x:Bind Appearance.HasForeground, Mode=OneWay}"
SettingOverrideSource="{x:Bind Appearance.ForegroundOverrideSource, Mode=OneWay}"
Style="{StaticResource ExpanderSettingContainerStyleWithComplexPreview}">
Style="{StaticResource ExpanderSettingContainerStyle}">
<local:NullableColorPicker x:Uid="Profile_Foreground_NullableColorPicker"
ColorSchemeVM="{x:Bind Appearance.CurrentColorScheme, Mode=OneWay}"
CurrentColor="{x:Bind Appearance.Foreground, Mode=TwoWay}"
@@ -236,7 +236,7 @@
CurrentValueTemplate="{StaticResource ColorPreviewTemplate}"
HasSettingValue="{x:Bind Appearance.HasBackground, Mode=OneWay}"
SettingOverrideSource="{x:Bind Appearance.BackgroundOverrideSource, Mode=OneWay}"
Style="{StaticResource ExpanderSettingContainerStyleWithComplexPreview}">
Style="{StaticResource ExpanderSettingContainerStyle}">
<local:NullableColorPicker x:Uid="Profile_Background_NullableColorPicker"
ColorSchemeVM="{x:Bind Appearance.CurrentColorScheme, Mode=OneWay}"
CurrentColor="{x:Bind Appearance.Background, Mode=TwoWay}"
@@ -252,7 +252,7 @@
CurrentValueTemplate="{StaticResource ColorPreviewTemplate}"
HasSettingValue="{x:Bind Appearance.HasSelectionBackground, Mode=OneWay}"
SettingOverrideSource="{x:Bind Appearance.SelectionBackgroundOverrideSource, Mode=OneWay}"
Style="{StaticResource ExpanderSettingContainerStyleWithComplexPreview}">
Style="{StaticResource ExpanderSettingContainerStyle}">
<local:NullableColorPicker x:Uid="Profile_SelectionBackground_NullableColorPicker"
ColorSchemeVM="{x:Bind Appearance.CurrentColorScheme, Mode=OneWay}"
CurrentColor="{x:Bind Appearance.SelectionBackground, Mode=TwoWay}"
@@ -536,7 +536,7 @@
CurrentValueTemplate="{StaticResource ColorPreviewTemplate}"
HasSettingValue="{x:Bind Appearance.HasCursorColor, Mode=OneWay}"
SettingOverrideSource="{x:Bind Appearance.CursorColorOverrideSource, Mode=OneWay}"
Style="{StaticResource ExpanderSettingContainerStyleWithComplexPreview}">
Style="{StaticResource ExpanderSettingContainerStyle}">
<local:NullableColorPicker x:Uid="Profile_CursorColor_NullableColorPicker"
ColorSchemeVM="{x:Bind Appearance.CurrentColorScheme, Mode=OneWay}"
CurrentColor="{x:Bind Appearance.CursorColor, Mode=TwoWay}"

View File

@@ -8,8 +8,9 @@
xmlns:mtu="using:Microsoft.Terminal.UI"
xmlns:muxc="using:Microsoft.UI.Xaml.Controls">
<!-- Merge SettingContainerStyle here to give every page access to the SettingContainer -->
<!-- Merge SettingsControls and SettingContainer styles here to give every page access to them -->
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="SettingsControlsStyle.xaml" />
<ResourceDictionary Source="SettingContainerStyle.xaml" />
</ResourceDictionary.MergedDictionaries>
@@ -1200,87 +1201,6 @@
</Setter>
</Style>
<Style x:Key="NavigatorButtonStyle"
TargetType="Button">
<Setter Property="Background" Value="{ThemeResource ExpanderHeaderBackground}" />
<Setter Property="MinWidth" Value="{ThemeResource FlyoutThemeMinWidth}" />
<Setter Property="MinHeight" Value="64" />
<Setter Property="BorderThickness" Value="{ThemeResource ExpanderHeaderBorderThickness}" />
<Setter Property="BorderBrush" Value="{ThemeResource ExpanderHeaderBorderBrush}" />
<Setter Property="Padding" Value="16,0,8,0" />
<Setter Property="Margin" Value="0,4,0,0" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}" />
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Grid x:Name="Grid"
Padding="{TemplateBinding Padding}"
AutomationProperties.AccessibilityView="Raw"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ContentPresenter x:Name="ContentPresenter"
Grid.Column="0"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
ContentTransitions="{TemplateBinding ContentTransitions}" />
<FontIcon Grid.Column="1"
Margin="20,0,8,0"
HorizontalAlignment="Right"
FontSize="10"
FontWeight="Black"
Glyph="&#xE76C;" />
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="PointerOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Grid"
Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleButtonBackgroundPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Grid"
Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleButtonBorderBrushPointerOver}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Pressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Grid"
Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleButtonBackgroundPressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Grid"
Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource ToggleButtonBorderBrushPressed}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="NewInfoBadge"
TargetType="muxc:InfoBadge">
<Setter Property="Padding" Value="5,1,5,2" />

View File

@@ -0,0 +1,139 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "ControlSizeTrigger.h"
#include "ControlSizeTrigger.g.cpp"
#include <limits>
using namespace winrt::Windows::Foundation;
using namespace winrt::Windows::UI::Xaml;
namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
DependencyProperty ControlSizeTrigger::_CanTriggerProperty{ nullptr };
DependencyProperty ControlSizeTrigger::_MinWidthProperty{ nullptr };
DependencyProperty ControlSizeTrigger::_MaxWidthProperty{ nullptr };
DependencyProperty ControlSizeTrigger::_MinHeightProperty{ nullptr };
DependencyProperty ControlSizeTrigger::_MaxHeightProperty{ nullptr };
DependencyProperty ControlSizeTrigger::_TargetElementProperty{ nullptr };
ControlSizeTrigger::ControlSizeTrigger()
{
_InitializeProperties();
}
void ControlSizeTrigger::_InitializeProperties()
{
// Defaults mirror the toolkit: trigger is always evaluatable, bounds
// are wide open, no target element until one is bound.
if (!_CanTriggerProperty)
{
_CanTriggerProperty = DependencyProperty::Register(
L"CanTrigger",
xaml_typename<bool>(),
xaml_typename<Editor::ControlSizeTrigger>(),
PropertyMetadata{ box_value(true), PropertyChangedCallback{ &ControlSizeTrigger::_OnTriggerInputChanged } });
}
if (!_MinWidthProperty)
{
_MinWidthProperty = DependencyProperty::Register(
L"MinWidth",
xaml_typename<double>(),
xaml_typename<Editor::ControlSizeTrigger>(),
PropertyMetadata{ box_value(0.0), PropertyChangedCallback{ &ControlSizeTrigger::_OnTriggerInputChanged } });
}
if (!_MaxWidthProperty)
{
_MaxWidthProperty = DependencyProperty::Register(
L"MaxWidth",
xaml_typename<double>(),
xaml_typename<Editor::ControlSizeTrigger>(),
PropertyMetadata{ box_value(std::numeric_limits<double>::infinity()), PropertyChangedCallback{ &ControlSizeTrigger::_OnTriggerInputChanged } });
}
if (!_MinHeightProperty)
{
_MinHeightProperty = DependencyProperty::Register(
L"MinHeight",
xaml_typename<double>(),
xaml_typename<Editor::ControlSizeTrigger>(),
PropertyMetadata{ box_value(0.0), PropertyChangedCallback{ &ControlSizeTrigger::_OnTriggerInputChanged } });
}
if (!_MaxHeightProperty)
{
_MaxHeightProperty = DependencyProperty::Register(
L"MaxHeight",
xaml_typename<double>(),
xaml_typename<Editor::ControlSizeTrigger>(),
PropertyMetadata{ box_value(std::numeric_limits<double>::infinity()), PropertyChangedCallback{ &ControlSizeTrigger::_OnTriggerInputChanged } });
}
if (!_TargetElementProperty)
{
_TargetElementProperty = DependencyProperty::Register(
L"TargetElement",
xaml_typename<FrameworkElement>(),
xaml_typename<Editor::ControlSizeTrigger>(),
PropertyMetadata{ nullptr, PropertyChangedCallback{ &ControlSizeTrigger::_OnTargetElementChanged } });
}
}
void ControlSizeTrigger::_OnTriggerInputChanged(const DependencyObject& d, const DependencyPropertyChangedEventArgs& /*e*/)
{
if (const auto obj{ d.try_as<Editor::ControlSizeTrigger>() })
{
get_self<ControlSizeTrigger>(obj)->_UpdateTrigger();
}
}
void ControlSizeTrigger::_OnTargetElementChanged(const DependencyObject& d, const DependencyPropertyChangedEventArgs& e)
{
const auto obj{ d.try_as<Editor::ControlSizeTrigger>() };
if (!obj)
{
return;
}
const auto oldElement = e.OldValue().try_as<FrameworkElement>();
const auto newElement = e.NewValue().try_as<FrameworkElement>();
get_self<ControlSizeTrigger>(obj)->_UpdateTargetElement(oldElement, newElement);
}
void ControlSizeTrigger::_UpdateTargetElement(const FrameworkElement& /*oldValue*/, const FrameworkElement& newValue)
{
// Revoking handles both unhooking the previous element and a null `newValue`.
_sizeChangedRevoker.revoke();
if (newValue)
{
_sizeChangedRevoker = newValue.SizeChanged(winrt::auto_revoke, [weakThis = get_weak()](auto&&, auto&&) {
if (const auto strongThis = weakThis.get())
{
strongThis->_UpdateTrigger();
}
});
}
_UpdateTrigger();
}
void ControlSizeTrigger::_UpdateTrigger()
{
const auto target = TargetElement();
if (!target || !CanTrigger())
{
_isActive = false;
SetActive(false);
return;
}
const auto width = target.ActualWidth();
const auto height = target.ActualHeight();
const bool activate =
MinWidth() <= width &&
width < MaxWidth() &&
MinHeight() <= height &&
height < MaxHeight();
_isActive = activate;
SetActive(activate);
}
}

View File

@@ -0,0 +1,64 @@
/*++
Copyright (c) Microsoft Corporation
Licensed under the MIT license.
Module Name:
- ControlSizeTrigger
Abstract:
- A conditional state trigger that activates based on the size (width and/or
height) of a target FrameworkElement. Lets XAML visual states swap based on
the live size of a templated part. Ported from the Windows Community Toolkit
primitive `CommunityToolkit.WinUI.ControlSizeTrigger`.
The trigger is "active" when:
MinWidth <= TargetElement.ActualWidth < MaxWidth AND
MinHeight <= TargetElement.ActualHeight < MaxHeight
Defaults: MinWidth = MinHeight = 0; MaxWidth = MaxHeight = +inf, which makes
the trigger always active unless `CanTrigger` is false or `TargetElement` is
null.
Author(s):
- Carlos Zamora - May 2026 (port from CommunityToolkit.WinUI.ControlSizeTrigger)
--*/
#pragma once
#include "ControlSizeTrigger.g.h"
#include "Utils.h"
namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
struct ControlSizeTrigger : ControlSizeTriggerT<ControlSizeTrigger>
{
public:
ControlSizeTrigger();
bool IsActive() const { return _isActive; }
DEPENDENCY_PROPERTY(bool, CanTrigger);
DEPENDENCY_PROPERTY(double, MinWidth);
DEPENDENCY_PROPERTY(double, MaxWidth);
DEPENDENCY_PROPERTY(double, MinHeight);
DEPENDENCY_PROPERTY(double, MaxHeight);
DEPENDENCY_PROPERTY(Windows::UI::Xaml::FrameworkElement, TargetElement);
private:
static void _InitializeProperties();
static void _OnTriggerInputChanged(const Windows::UI::Xaml::DependencyObject& d, const Windows::UI::Xaml::DependencyPropertyChangedEventArgs& e);
static void _OnTargetElementChanged(const Windows::UI::Xaml::DependencyObject& d, const Windows::UI::Xaml::DependencyPropertyChangedEventArgs& e);
void _UpdateTargetElement(const Windows::UI::Xaml::FrameworkElement& oldValue, const Windows::UI::Xaml::FrameworkElement& newValue);
void _UpdateTrigger();
Windows::UI::Xaml::FrameworkElement::SizeChanged_revoker _sizeChangedRevoker;
bool _isActive{ false };
};
}
namespace winrt::Microsoft::Terminal::Settings::Editor::factory_implementation
{
BASIC_FACTORY(ControlSizeTrigger);
}

View File

@@ -0,0 +1,30 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
namespace Microsoft.Terminal.Settings.Editor
{
[default_interface] runtimeclass ControlSizeTrigger : Windows.UI.Xaml.StateTriggerBase
{
ControlSizeTrigger();
Boolean CanTrigger;
static Windows.UI.Xaml.DependencyProperty CanTriggerProperty { get; };
Double MinWidth;
static Windows.UI.Xaml.DependencyProperty MinWidthProperty { get; };
Double MaxWidth;
static Windows.UI.Xaml.DependencyProperty MaxWidthProperty { get; };
Double MinHeight;
static Windows.UI.Xaml.DependencyProperty MinHeightProperty { get; };
Double MaxHeight;
static Windows.UI.Xaml.DependencyProperty MaxHeightProperty { get; };
Windows.UI.Xaml.FrameworkElement TargetElement;
static Windows.UI.Xaml.DependencyProperty TargetElementProperty { get; };
Boolean IsActive { get; };
}
}

View File

@@ -0,0 +1,58 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "CornerRadiusFilterConverters.h"
#include "CornerRadiusConverter.g.cpp"
#include "TopCornerRadiusFilterConverter.g.cpp"
#include "BottomCornerRadiusFilterConverter.g.cpp"
using namespace winrt::Windows::UI::Xaml;
namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
winrt::Windows::Foundation::IInspectable CornerRadiusConverter::Convert(const winrt::Windows::Foundation::IInspectable& value, const Interop::TypeName& /*targetType*/, const winrt::Windows::Foundation::IInspectable& /*parameter*/, const hstring& /*language*/)
{
if (!value)
{
return value;
}
const auto cr = unbox_value_or<CornerRadius>(value, CornerRadius{ 0, 0, 0, 0 });
return box_value(CornerRadius{ 0, 0, cr.BottomRight, cr.BottomLeft });
}
winrt::Windows::Foundation::IInspectable CornerRadiusConverter::ConvertBack(const winrt::Windows::Foundation::IInspectable& value, const Interop::TypeName& /*targetType*/, const winrt::Windows::Foundation::IInspectable& /*parameter*/, const hstring& /*language*/)
{
return value;
}
winrt::Windows::Foundation::IInspectable TopCornerRadiusFilterConverter::Convert(const winrt::Windows::Foundation::IInspectable& value, const Interop::TypeName& /*targetType*/, const winrt::Windows::Foundation::IInspectable& /*parameter*/, const hstring& /*language*/)
{
if (!value)
{
return value;
}
const auto cr = unbox_value_or<CornerRadius>(value, CornerRadius{ 0, 0, 0, 0 });
return box_value(CornerRadius{ cr.TopLeft, cr.TopRight, 0, 0 });
}
winrt::Windows::Foundation::IInspectable TopCornerRadiusFilterConverter::ConvertBack(const winrt::Windows::Foundation::IInspectable& value, const Interop::TypeName& /*targetType*/, const winrt::Windows::Foundation::IInspectable& /*parameter*/, const hstring& /*language*/)
{
return value;
}
winrt::Windows::Foundation::IInspectable BottomCornerRadiusFilterConverter::Convert(const winrt::Windows::Foundation::IInspectable& value, const Interop::TypeName& /*targetType*/, const winrt::Windows::Foundation::IInspectable& /*parameter*/, const hstring& /*language*/)
{
if (!value)
{
return value;
}
const auto cr = unbox_value_or<CornerRadius>(value, CornerRadius{ 0, 0, 0, 0 });
return box_value(CornerRadius{ 0, 0, cr.BottomRight, cr.BottomLeft });
}
winrt::Windows::Foundation::IInspectable BottomCornerRadiusFilterConverter::ConvertBack(const winrt::Windows::Foundation::IInspectable& value, const Interop::TypeName& /*targetType*/, const winrt::Windows::Foundation::IInspectable& /*parameter*/, const hstring& /*language*/)
{
return value;
}
}

View File

@@ -0,0 +1,42 @@
// Copyright (c) Microsoft Corporation
// Licensed under the MIT license.
#pragma once
#include "CornerRadiusConverter.g.h"
#include "TopCornerRadiusFilterConverter.g.h"
#include "BottomCornerRadiusFilterConverter.g.h"
namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
struct CornerRadiusConverter : CornerRadiusConverterT<CornerRadiusConverter>
{
CornerRadiusConverter() = default;
Windows::Foundation::IInspectable Convert(const Windows::Foundation::IInspectable& value, const Windows::UI::Xaml::Interop::TypeName& targetType, const Windows::Foundation::IInspectable& parameter, const hstring& language);
Windows::Foundation::IInspectable ConvertBack(const Windows::Foundation::IInspectable& value, const Windows::UI::Xaml::Interop::TypeName& targetType, const Windows::Foundation::IInspectable& parameter, const hstring& language);
};
struct TopCornerRadiusFilterConverter : TopCornerRadiusFilterConverterT<TopCornerRadiusFilterConverter>
{
TopCornerRadiusFilterConverter() = default;
Windows::Foundation::IInspectable Convert(const Windows::Foundation::IInspectable& value, const Windows::UI::Xaml::Interop::TypeName& targetType, const Windows::Foundation::IInspectable& parameter, const hstring& language);
Windows::Foundation::IInspectable ConvertBack(const Windows::Foundation::IInspectable& value, const Windows::UI::Xaml::Interop::TypeName& targetType, const Windows::Foundation::IInspectable& parameter, const hstring& language);
};
struct BottomCornerRadiusFilterConverter : BottomCornerRadiusFilterConverterT<BottomCornerRadiusFilterConverter>
{
BottomCornerRadiusFilterConverter() = default;
Windows::Foundation::IInspectable Convert(const Windows::Foundation::IInspectable& value, const Windows::UI::Xaml::Interop::TypeName& targetType, const Windows::Foundation::IInspectable& parameter, const hstring& language);
Windows::Foundation::IInspectable ConvertBack(const Windows::Foundation::IInspectable& value, const Windows::UI::Xaml::Interop::TypeName& targetType, const Windows::Foundation::IInspectable& parameter, const hstring& language);
};
}
namespace winrt::Microsoft::Terminal::Settings::Editor::factory_implementation
{
BASIC_FACTORY(CornerRadiusConverter);
BASIC_FACTORY(TopCornerRadiusFilterConverter);
BASIC_FACTORY(BottomCornerRadiusFilterConverter);
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
namespace Microsoft.Terminal.Settings.Editor
{
[default_interface] runtimeclass CornerRadiusConverter : Windows.UI.Xaml.Data.IValueConverter
{
CornerRadiusConverter();
}
[default_interface] runtimeclass TopCornerRadiusFilterConverter : Windows.UI.Xaml.Data.IValueConverter
{
TopCornerRadiusFilterConverter();
}
[default_interface] runtimeclass BottomCornerRadiusFilterConverter : Windows.UI.Xaml.Data.IValueConverter
{
BottomCornerRadiusFilterConverter();
}
}

View File

@@ -79,7 +79,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
void Extensions::ExtensionNavigator_Click(const IInspectable& sender, const RoutedEventArgs& /*args*/)
{
const auto extPkgVM = sender.as<Controls::Button>().Tag().as<Editor::ExtensionPackageViewModel>();
const auto extPkgVM = sender.as<FrameworkElement>().Tag().as<Editor::ExtensionPackageViewModel>();
_ViewModel.CurrentExtensionPackage(extPkgVM);
}

View File

@@ -127,78 +127,58 @@
<DataTemplate x:Key="DefaultExtensionNavigatorTemplate"
x:DataType="local:ExtensionPackageViewModel">
<Button AutomationProperties.Name="{x:Bind AccessibleName}"
Click="ExtensionNavigator_Click"
Style="{StaticResource NavigatorButtonStyle}"
Tag="{x:Bind}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ContentPresenter Content="{x:Bind}"
ContentTemplate="{StaticResource DefaultExtensionIdentifierTemplate}" />
<ToggleSwitch Grid.Column="1"
HorizontalAlignment="Right"
VerticalAlignment="Center"
AutomationProperties.Name="{x:Bind AccessibleName}"
IsOn="{x:Bind Enabled, Mode=TwoWay}"
Style="{StaticResource ToggleSwitchInExpanderStyle}" />
</Grid>
</Button>
<local:SettingsCard AutomationProperties.Name="{x:Bind AccessibleName}"
Click="ExtensionNavigator_Click"
Header="{x:Bind Package.Source}"
IsClickEnabled="True"
Tag="{x:Bind}">
<local:SettingsCard.HeaderIcon>
<FontIcon Glyph="&#xE74C;" />
</local:SettingsCard.HeaderIcon>
<ToggleSwitch HorizontalAlignment="Right"
VerticalAlignment="Center"
AutomationProperties.Name="{x:Bind AccessibleName}"
IsOn="{x:Bind Enabled, Mode=TwoWay}"
Style="{StaticResource ToggleSwitchInExpanderStyle}" />
</local:SettingsCard>
</DataTemplate>
<DataTemplate x:Key="ComplexExtensionNavigatorTemplate"
x:DataType="local:ExtensionPackageViewModel">
<Button AutomationProperties.Name="{x:Bind AccessibleName}"
Click="ExtensionNavigator_Click"
Style="{StaticResource NavigatorButtonStyle}"
Tag="{x:Bind}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ContentPresenter Content="{x:Bind}"
ContentTemplate="{StaticResource ComplexExtensionIdentifierTemplate}" />
<ToggleSwitch Grid.Column="1"
HorizontalAlignment="Right"
VerticalAlignment="Center"
AutomationProperties.Name="{x:Bind AccessibleName}"
IsOn="{x:Bind Enabled, Mode=TwoWay}"
Style="{StaticResource ToggleSwitchInExpanderStyle}" />
</Grid>
</Button>
<local:SettingsCard AutomationProperties.Name="{x:Bind AccessibleName}"
Click="ExtensionNavigator_Click"
Description="{x:Bind Package.Source}"
Header="{x:Bind Package.DisplayName}"
IsClickEnabled="True"
Tag="{x:Bind}">
<local:SettingsCard.HeaderIcon>
<IconSourceElement IconSource="{x:Bind mtu:IconPathConverter.IconSourceWUX(Package.Icon)}" />
</local:SettingsCard.HeaderIcon>
<ToggleSwitch HorizontalAlignment="Right"
VerticalAlignment="Center"
AutomationProperties.Name="{x:Bind AccessibleName}"
IsOn="{x:Bind Enabled, Mode=TwoWay}"
Style="{StaticResource ToggleSwitchInExpanderStyle}" />
</local:SettingsCard>
</DataTemplate>
<DataTemplate x:Key="ComplexExtensionNavigatorTemplateWithFontIcon"
x:DataType="local:ExtensionPackageViewModel">
<Button AutomationProperties.Name="{x:Bind AccessibleName}"
Click="ExtensionNavigator_Click"
Style="{StaticResource NavigatorButtonStyle}"
Tag="{x:Bind}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ContentPresenter Content="{x:Bind}"
ContentTemplate="{StaticResource ComplexExtensionIdentifierTemplateWithFontIcon}" />
<ToggleSwitch Grid.Column="1"
HorizontalAlignment="Right"
VerticalAlignment="Center"
AutomationProperties.Name="{x:Bind AccessibleName}"
IsOn="{x:Bind Enabled, Mode=TwoWay}"
Style="{StaticResource ToggleSwitchInExpanderStyle}" />
</Grid>
</Button>
<local:SettingsCard AutomationProperties.Name="{x:Bind AccessibleName}"
Click="ExtensionNavigator_Click"
Description="{x:Bind Package.Source}"
Header="{x:Bind Package.DisplayName}"
IsClickEnabled="True"
Tag="{x:Bind}">
<local:SettingsCard.HeaderIcon>
<FontIcon Glyph="{x:Bind Package.Icon}" />
</local:SettingsCard.HeaderIcon>
<ToggleSwitch HorizontalAlignment="Right"
VerticalAlignment="Center"
AutomationProperties.Name="{x:Bind AccessibleName}"
IsOn="{x:Bind Enabled, Mode=TwoWay}"
Style="{StaticResource ToggleSwitchInExpanderStyle}" />
</local:SettingsCard>
</DataTemplate>
<DataTemplate x:Key="FragmentProfileViewModelTemplate"
@@ -502,9 +482,11 @@
<!-- Scope -->
<local:SettingContainer x:Name="Scope"
x:Uid="Extensions_Scope"
Content="{x:Bind ViewModel.CurrentExtensionPackage.Scope, Mode=OneWay}"
IsTabStop="False"
Style="{StaticResource SettingContainerWithTextContent}" />
IsTabStop="False">
<TextBlock VerticalAlignment="Center"
Style="{ThemeResource SecondaryTextBlockStyle}"
Text="{x:Bind ViewModel.CurrentExtensionPackage.Scope, Mode=OneWay}" />
</local:SettingContainer>
<!-- JSON -->
<ItemsControl IsTabStop="False"
ItemTemplate="{StaticResource JsonTemplate}"

View File

@@ -62,9 +62,9 @@
<!-- Always show tabs -->
<local:SettingContainer x:Name="AlwaysShowTabs"
x:Uid="Globals_AlwaysShowTabs">
<ToggleSwitch IsEnabled="{x:Bind mtu:Converters.InvertBoolean(ViewModel.ShowTabsInTitlebar), Mode=OneWay}"
IsOn="{x:Bind ViewModel.AlwaysShowTabs, Mode=TwoWay}"
x:Uid="Globals_AlwaysShowTabs"
IsEnabled="{x:Bind mtu:Converters.InvertBoolean(ViewModel.ShowTabsInTitlebar), Mode=OneWay}">
<ToggleSwitch IsOn="{x:Bind ViewModel.AlwaysShowTabs, Mode=TwoWay}"
Style="{StaticResource ToggleSwitchInExpanderStyle}" />
</local:SettingContainer>

View File

@@ -173,6 +173,24 @@
<ClInclude Include="SettingContainer.h">
<DependentUpon>SettingContainer.idl</DependentUpon>
</ClInclude>
<ClInclude Include="SettingsCard.h">
<DependentUpon>SettingsCard.idl</DependentUpon>
</ClInclude>
<ClInclude Include="SettingsExpander.h">
<DependentUpon>SettingsExpander.idl</DependentUpon>
</ClInclude>
<ClInclude Include="StyleExtensions.h">
<DependentUpon>StyleExtensions.idl</DependentUpon>
</ClInclude>
<ClInclude Include="ControlSizeTrigger.h">
<DependentUpon>ControlSizeTrigger.idl</DependentUpon>
</ClInclude>
<ClInclude Include="CornerRadiusFilterConverters.h">
<DependentUpon>CornerRadiusFilterConverters.idl</DependentUpon>
</ClInclude>
<ClInclude Include="StringDefaultTemplateSelector.h">
<DependentUpon>StringDefaultTemplateSelector.idl</DependentUpon>
</ClInclude>
<ClInclude Include="Utils.h" />
<ClInclude Include="PreviewConnection.h" />
<ClInclude Include="$(GeneratedFilesDir)GeneratedSettingsIndex.g.h" />
@@ -252,6 +270,12 @@
<Page Include="SettingContainerStyle.xaml">
<Type>DefaultStyle</Type>
</Page>
<Page Include="SettingsControlsStyle.xaml">
<Type>DefaultStyle</Type>
</Page>
<Page Include="SettingsControlsImplicitStyles.xaml">
<Type>DefaultStyle</Type>
</Page>
</ItemGroup>
<!-- ========================= Cpp Files ======================== -->
<ItemGroup>
@@ -385,6 +409,24 @@
<ClCompile Include="SettingContainer.cpp">
<DependentUpon>SettingContainer.idl</DependentUpon>
</ClCompile>
<ClCompile Include="SettingsCard.cpp">
<DependentUpon>SettingsCard.idl</DependentUpon>
</ClCompile>
<ClCompile Include="SettingsExpander.cpp">
<DependentUpon>SettingsExpander.idl</DependentUpon>
</ClCompile>
<ClCompile Include="StyleExtensions.cpp">
<DependentUpon>StyleExtensions.idl</DependentUpon>
</ClCompile>
<ClCompile Include="ControlSizeTrigger.cpp">
<DependentUpon>ControlSizeTrigger.idl</DependentUpon>
</ClCompile>
<ClCompile Include="CornerRadiusFilterConverters.cpp">
<DependentUpon>CornerRadiusFilterConverters.idl</DependentUpon>
</ClCompile>
<ClCompile Include="StringDefaultTemplateSelector.cpp">
<DependentUpon>StringDefaultTemplateSelector.idl</DependentUpon>
</ClCompile>
<ClCompile Include="Utils.cpp" />
<ClCompile Include="PreviewConnection.cpp">
<DependentUpon>PreviewConnection.h</DependentUpon>
@@ -492,6 +534,24 @@
<Midl Include="SettingContainer.idl">
<SubType>Code</SubType>
</Midl>
<Midl Include="SettingsCard.idl">
<SubType>Code</SubType>
</Midl>
<Midl Include="SettingsExpander.idl">
<SubType>Code</SubType>
</Midl>
<Midl Include="StyleExtensions.idl">
<SubType>Code</SubType>
</Midl>
<Midl Include="ControlSizeTrigger.idl">
<SubType>Code</SubType>
</Midl>
<Midl Include="CornerRadiusFilterConverters.idl">
<SubType>Code</SubType>
</Midl>
<Midl Include="StringDefaultTemplateSelector.idl">
<SubType>Code</SubType>
</Midl>
</ItemGroup>
<!-- ========================= Misc Files ======================== -->
<ItemGroup>

View File

@@ -30,6 +30,12 @@
<Midl Include="LaunchViewModel.idl" />
<Midl Include="EnumEntry.idl" />
<Midl Include="SettingContainer.idl" />
<Midl Include="SettingsCard.idl" />
<Midl Include="SettingsExpander.idl" />
<Midl Include="StyleExtensions.idl" />
<Midl Include="ControlSizeTrigger.idl" />
<Midl Include="CornerRadiusFilterConverters.idl" />
<Midl Include="StringDefaultTemplateSelector.idl" />
<Midl Include="TerminalColorConverters.idl" />
<Midl Include="NewTabMenuViewModel.idl" />
</ItemGroup>
@@ -52,6 +58,8 @@
<Page Include="Actions.xaml" />
<Page Include="EditAction.xaml" />
<Page Include="SettingContainerStyle.xaml" />
<Page Include="SettingsControlsStyle.xaml" />
<Page Include="SettingsControlsImplicitStyles.xaml" />
<Page Include="AddProfile.xaml" />
<Page Include="KeyChordListener.xaml" />
<Page Include="NullableColorPicker.xaml" />

View File

@@ -332,7 +332,7 @@
<local:SettingContainer x:Name="CurrentFolderIcon"
x:Uid="NewTabMenu_CurrentFolderIcon"
CurrentValueAccessibleName="{x:Bind ViewModel.CurrentFolderLocalizedIcon, Mode=OneWay}"
Style="{StaticResource ExpanderSettingContainerStyleWithComplexPreview}">
Style="{StaticResource ExpanderSettingContainerStyle}">
<local:SettingContainer.CurrentValue>
<Grid>
<ContentControl Width="16"
@@ -378,8 +378,7 @@
<!-- Add Profile -->
<local:SettingContainer x:Name="AddProfile"
x:Uid="NewTabMenu_AddProfile"
FontIconGlyph="&#xE756;"
Style="{StaticResource SettingContainerWithIcon}">
FontIconGlyph="&#xE756;">
<StackPanel Orientation="Horizontal"
Spacing="4">
@@ -428,8 +427,7 @@
<!-- Add Separator -->
<local:SettingContainer x:Name="AddSeparator"
x:Uid="NewTabMenu_AddSeparator"
FontIconGlyph="&#xE76f;"
Style="{StaticResource SettingContainerWithIcon}">
FontIconGlyph="&#xE76f;">
<Button x:Name="AddSeparatorButton"
x:Uid="NewTabMenu_AddSeparatorButton"
HorizontalAlignment="Stretch"
@@ -445,8 +443,7 @@
<!-- Add Folder -->
<local:SettingContainer x:Name="AddFolder"
x:Uid="NewTabMenu_AddFolder"
FontIconGlyph="&#xF12B;"
Style="{StaticResource SettingContainerWithIcon}">
FontIconGlyph="&#xF12B;">
<StackPanel Orientation="Horizontal"
Spacing="5">
<TextBox x:Name="FolderNameTextBox"
@@ -473,7 +470,7 @@
<local:SettingContainer x:Name="AddMatchProfiles"
x:Uid="NewTabMenu_AddMatchProfiles"
FontIconGlyph="&#xE748;"
Style="{StaticResource ExpanderSettingContainerStyleWithIcon}">
Style="{StaticResource ExpanderSettingContainerStyle}">
<StackPanel Spacing="8">
<HyperlinkButton x:Uid="NewTabMenu_AddMatchProfiles_Help"
NavigateUri="https://learn.microsoft.com/en-us/dotnet/standard/base-types/regular-expression-language-quick-reference" />
@@ -500,8 +497,7 @@
<!-- Add Remaining Profiles -->
<local:SettingContainer x:Name="AddRemainingProfiles"
x:Uid="NewTabMenu_AddRemainingProfiles"
FontIconGlyph="&#xE902;"
Style="{StaticResource SettingContainerWithIcon}">
FontIconGlyph="&#xE902;">
<Button x:Name="AddRemainingProfilesButton"
x:Uid="NewTabMenu_AddRemainingProfilesButton"
HorizontalAlignment="Stretch"

View File

@@ -22,9 +22,9 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
Automation::AutomationProperties::SetFullDescription(StartingDirectoryUseParentCheckbox(), unbox_value<hstring>(startingDirCheckboxTooltip));
Automation::AutomationProperties::SetName(DeleteButton(), RS_(L"Profile_DeleteButton/Text"));
AppearanceNavigator().Content(box_value(RS_(L"Profile_Appearance/Header")));
TerminalNavigator().Content(box_value(RS_(L"Profile_Terminal/Header")));
AdvancedNavigator().Content(box_value(RS_(L"Profile_Advanced/Header")));
AppearanceNavigator().Header(box_value(RS_(L"Profile_Appearance/Header")));
TerminalNavigator().Header(box_value(RS_(L"Profile_Terminal/Header")));
AdvancedNavigator().Header(box_value(RS_(L"Profile_Advanced/Header")));
}
void Profiles_Base::OnNavigatedTo(const NavigationEventArgs& e)

View File

@@ -106,7 +106,7 @@
CurrentValueAccessibleName="{x:Bind Profile.LocalizedIcon, Mode=OneWay}"
HasSettingValue="{x:Bind Profile.HasIcon, Mode=OneWay}"
SettingOverrideSource="{x:Bind Profile.IconOverrideSource, Mode=OneWay}"
Style="{StaticResource ExpanderSettingContainerStyleWithComplexPreview}">
Style="{StaticResource ExpanderSettingContainerStyle}">
<local:SettingContainer.CurrentValue>
<Grid>
<ContentControl Width="16"
@@ -150,7 +150,7 @@
CurrentValueTemplate="{StaticResource ColorPreviewTemplate}"
HasSettingValue="{x:Bind Profile.HasTabColor, Mode=OneWay}"
SettingOverrideSource="{x:Bind Profile.TabColorOverrideSource, Mode=OneWay}"
Style="{StaticResource ExpanderSettingContainerStyleWithComplexPreview}">
Style="{StaticResource ExpanderSettingContainerStyle}">
<local:NullableColorPicker x:Uid="Profile_TabColor_NullableColorPicker"
ColorSchemeVM="{x:Bind Profile.DefaultAppearance.CurrentColorScheme, Mode=OneWay}"
CurrentColor="{x:Bind Profile.TabColor, Mode=TwoWay}"
@@ -179,15 +179,15 @@
Margin="0,32,0,4"
Style="{StaticResource TextBlockSubHeaderStyle}" />
<Button x:Name="AppearanceNavigator"
Click="Appearance_Click"
Style="{StaticResource NavigatorButtonStyle}" />
<Button x:Name="TerminalNavigator"
Click="Terminal_Click"
Style="{StaticResource NavigatorButtonStyle}" />
<Button x:Name="AdvancedNavigator"
Click="Advanced_Click"
Style="{StaticResource NavigatorButtonStyle}" />
<local:SettingsCard x:Name="AppearanceNavigator"
Click="Appearance_Click"
IsClickEnabled="True" />
<local:SettingsCard x:Name="TerminalNavigator"
Click="Terminal_Click"
IsClickEnabled="True" />
<local:SettingsCard x:Name="AdvancedNavigator"
Click="Advanced_Click"
IsClickEnabled="True" />
<!-- Delete Button -->
<Border MaxWidth="{StaticResource StandardControlMaxWidth}">
<Button x:Name="DeleteButton"

View File

@@ -3,6 +3,7 @@
#include "pch.h"
#include "SettingContainer.h"
#include "SettingsExpander.h"
#include "SettingContainer.g.cpp"
using namespace winrt::Windows::UI::Xaml;
@@ -54,7 +55,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
L"FontIconGlyph",
xaml_typename<hstring>(),
xaml_typename<Editor::SettingContainer>(),
PropertyMetadata{ box_value(L"") });
PropertyMetadata{ box_value(L""), PropertyChangedCallback{ &SettingContainer::_OnFontIconGlyphChanged } });
}
if (!_CurrentValueProperty)
{
@@ -134,13 +135,30 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
void SettingContainer::_UpdateHelpText()
{
const auto helpText{ HelpText() };
// Forward HelpText into the SettingsCard / SettingsExpander Description
// slot so it renders through the WCT-ported PART_DescriptionPresenter.
// Setting it to nullptr when empty collapses the description visually so we don't
// reserve vertical space for a blank line.
const auto description{ helpText.empty() ? Windows::Foundation::IInspectable{ nullptr } : box_value(helpText) };
// Get the correct base to apply automation properties to
std::vector<DependencyObject> base;
base.reserve(2);
base.reserve(3);
if (const auto& child{ GetTemplateChild(L"Card") })
{
if (const auto& card{ child.try_as<Editor::SettingsCard>() })
{
card.Description(description);
base.push_back(child);
}
}
if (const auto& child{ GetTemplateChild(L"Expander") })
{
if (const auto& expander{ child.try_as<Microsoft::UI::Xaml::Controls::Expander>() })
if (const auto& expander{ child.try_as<Editor::SettingsExpander>() })
{
expander.Description(description);
base.push_back(child);
}
}
@@ -160,7 +178,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
Automation::AutomationProperties::SetName(obj, _GenerateAccessibleName());
// apply help text as tooltip and full description (automation property)
if (const auto& helpText{ HelpText() }; !helpText.empty())
if (!helpText.empty())
{
Automation::AutomationProperties::SetFullDescription(obj, helpText);
}
@@ -170,15 +188,6 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
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()
@@ -223,13 +232,54 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
_UpdateOverrideSystem();
_UpdateHelpText();
_UpdateHeaderIcon();
}
void SettingContainer::_UpdateHeaderIcon()
{
// Only forward FontIconGlyph into HeaderIcon when the caller actually
// supplied a glyph. Some templates (Warning/Error) already set
// SettingsCard.HeaderIcon in XAML to a stacked severity glyph; if we
// unconditionally wrote nullptr here we'd clobber that XAML default for
// every callsite that doesn't set FontIconGlyph.
const auto glyph{ FontIconGlyph() };
if (glyph.empty())
{
return;
}
Controls::FontIcon fi;
fi.Glyph(glyph);
const Controls::IconElement icon{ fi };
if (const auto& cardChild{ GetTemplateChild(L"Card") })
{
if (const auto& card{ cardChild.try_as<Editor::SettingsCard>() })
{
card.HeaderIcon(icon);
return;
}
}
if (const auto& expanderChild{ GetTemplateChild(L"Expander") })
{
if (const auto& expander{ expanderChild.try_as<Editor::SettingsExpander>() })
{
expander.HeaderIcon(icon);
}
}
}
void SettingContainer::_OnFontIconGlyphChanged(const DependencyObject& d, const DependencyPropertyChangedEventArgs& /*args*/)
{
const auto& obj{ d.try_as<Editor::SettingContainer>() };
get_self<SettingContainer>(obj)->_UpdateHeaderIcon();
}
void SettingContainer::SetExpanded(bool expanded)
{
if (const auto& child{ GetTemplateChild(L"Expander") })
{
if (const auto& expander{ child.try_as<Microsoft::UI::Xaml::Controls::Expander>() })
if (const auto& expander{ child.try_as<Editor::SettingsExpander>() })
{
expander.IsExpanded(expanded);
}
@@ -271,7 +321,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
if (const auto& child{ GetTemplateChild(L"Expander") })
{
if (const auto& expander{ child.try_as<Microsoft::UI::Xaml::Controls::Expander>() })
if (const auto& expander{ child.try_as<Editor::SettingsExpander>() })
{
Automation::AutomationProperties::SetName(expander, _GenerateAccessibleName());
}

View File

@@ -48,11 +48,13 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
static void _OnCurrentValueChanged(const Windows::UI::Xaml::DependencyObject& d, const Windows::UI::Xaml::DependencyPropertyChangedEventArgs& e);
static void _OnHasSettingValueChanged(const Windows::UI::Xaml::DependencyObject& d, const Windows::UI::Xaml::DependencyPropertyChangedEventArgs& e);
static void _OnHelpTextChanged(const Windows::UI::Xaml::DependencyObject& d, const Windows::UI::Xaml::DependencyPropertyChangedEventArgs& e);
static void _OnFontIconGlyphChanged(const Windows::UI::Xaml::DependencyObject& d, const Windows::UI::Xaml::DependencyPropertyChangedEventArgs& e);
static hstring _GenerateOverrideMessage(const IInspectable& settingOrigin);
hstring _GenerateAccessibleName();
void _UpdateOverrideSystem();
void _UpdateHelpText();
void _UpdateCurrentValueAutoProp();
void _UpdateHeaderIcon();
};
}

View File

@@ -18,8 +18,6 @@
</Style>
<SolidColorBrush x:Key="SubgroupHeaderBrush"
Color="{StaticResource TextFillColorSecondary}" />
<StaticResource x:Key="ExpanderHeaderBorderBrush"
ResourceKey="CardStrokeColorDefaultBrush" />
<StaticResource x:Key="SettingContainerErrorSeverityBackgroundBrush"
ResourceKey="SystemFillColorCriticalBackgroundBrush" />
@@ -31,28 +29,14 @@
<StaticResource x:Key="SettingContainerWarningSeverityIconBackground"
ResourceKey="SystemFillColorCautionBrush" />
<StaticResource x:Key="SettingContainerErrorSeverityIconForeground"
ResourceKey="TextFillColorInverseBrush" />
<StaticResource x:Key="SettingContainerWarningSeverityIconForeground"
ResourceKey="TextFillColorInverseBrush" />
<StaticResource x:Key="SettingContainerTitleForeground"
ResourceKey="TextFillColorPrimaryBrush" />
<StaticResource x:Key="SettingContainerMessageForeground"
ResourceKey="TextFillColorPrimaryBrush" />
<StaticResource x:Key="SettingContainerResetButtonIconForeground"
ResourceKey="SystemAccentColorDark2" />
</ResourceDictionary>
<ResourceDictionary x:Key="HighContrast">
<Style x:Key="SecondaryTextBlockStyle"
TargetType="TextBlock" />
<!-- Do not mess with the foreground color for High Contrast. Let it ride as is. -->
<SolidColorBrush x:Key="SubgroupHeaderBrush"
Color="{ThemeResource SystemColorWindowTextColor}" />
<StaticResource x:Key="ExpanderHeaderBorderBrush"
ResourceKey="SystemColorButtonTextColorBrush" />
<StaticResource x:Key="SettingContainerErrorSeverityBackgroundBrush"
ResourceKey="SystemColorWindowColorBrush" />
@@ -64,16 +48,6 @@
<StaticResource x:Key="SettingContainerWarningSeverityIconBackground"
ResourceKey="SystemColorHighlightColorBrush" />
<StaticResource x:Key="SettingContainerErrorSeverityIconForeground"
ResourceKey="SystemColorHighlightTextColorBrush" />
<StaticResource x:Key="SettingContainerWarningSeverityIconForeground"
ResourceKey="SystemColorHighlightTextColorBrush" />
<StaticResource x:Key="SettingContainerTitleForeground"
ResourceKey="SystemColorWindowTextColorBrush" />
<StaticResource x:Key="SettingContainerMessageForeground"
ResourceKey="SystemColorWindowTextColorBrush" />
<StaticResource x:Key="SettingContainerResetButtonIconForeground"
ResourceKey="SystemAccentColorLight1" />
</ResourceDictionary>
@@ -84,8 +58,6 @@
</Style>
<SolidColorBrush x:Key="SubgroupHeaderBrush"
Color="{StaticResource TextFillColorSecondary}" />
<StaticResource x:Key="ExpanderHeaderBorderBrush"
ResourceKey="CardStrokeColorDefaultBrush" />
<StaticResource x:Key="SettingContainerErrorSeverityBackgroundBrush"
ResourceKey="SystemFillColorCriticalBackgroundBrush" />
@@ -97,36 +69,16 @@
<StaticResource x:Key="SettingContainerWarningSeverityIconBackground"
ResourceKey="SystemFillColorCautionBrush" />
<StaticResource x:Key="SettingContainerErrorSeverityIconForeground"
ResourceKey="TextFillColorInverseBrush" />
<StaticResource x:Key="SettingContainerWarningSeverityIconForeground"
ResourceKey="TextFillColorInverseBrush" />
<StaticResource x:Key="SettingContainerTitleForeground"
ResourceKey="TextFillColorPrimaryBrush" />
<StaticResource x:Key="SettingContainerMessageForeground"
ResourceKey="TextFillColorPrimaryBrush" />
<StaticResource x:Key="SettingContainerResetButtonIconForeground"
ResourceKey="SystemAccentColorLight2" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
<FontWeight x:Key="SettingContainerTitleFontWeight">SemiBold</FontWeight>
<x:String x:Key="SettingContainerIconBackgroundGlyph">&#xF136;</x:String>
<x:String x:Key="SettingContainerErrorIconGlyph">&#xF13D;</x:String>
<x:String x:Key="SettingContainerWarningIconGlyph">&#xF13C;</x:String>
<Thickness x:Key="SettingContainerIconMargin">0,4,8,4</Thickness>
<x:Double x:Key="SettingContainerIconFontSize">16</x:Double>
<Style x:Key="StackPanelInExpanderStyle"
TargetType="StackPanel">
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Padding" Value="0,12,0,12" />
</Style>
<Style x:Key="SettingContainerResetButtonStyle"
BasedOn="{StaticResource DefaultButtonStyle}"
TargetType="Button">
@@ -145,18 +97,6 @@
<Setter Property="FontFamily" Value="{ThemeResource SymbolThemeFontFamily}" />
</Style>
<Style x:Key="NonExpanderGrid"
TargetType="Grid">
<Setter Property="Background" Value="{ThemeResource ExpanderHeaderBackground}" />
<Setter Property="MinWidth" Value="{ThemeResource FlyoutThemeMinWidth}" />
<Setter Property="MinHeight" Value="64" />
<Setter Property="BorderThickness" Value="{ThemeResource ExpanderHeaderBorderThickness}" />
<Setter Property="BorderBrush" Value="{ThemeResource ExpanderHeaderBorderBrush}" />
<Setter Property="Padding" Value="16,0,8,0" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}" />
</Style>
<Style x:Key="SettingsPageItemHeaderStyle"
BasedOn="{StaticResource BodyTextBlockStyle}"
TargetType="TextBlock">
@@ -178,7 +118,7 @@
BasedOn="{StaticResource SettingsPageItemDescriptionStyle}"
TargetType="TextBlock">
<Setter Property="MaxWidth" Value="248" />
<Setter Property="Margin" Value="0,0,-16,0" />
<Setter Property="Margin" Value="0" />
<Setter Property="HorizontalAlignment" Value="Right" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="FontFamily" Value="Segoe UI, Segoe Fluent Icons, Segoe MDL2 Assets" />
@@ -189,7 +129,9 @@
Text="{Binding}" />
</DataTemplate>
<!-- A setting container for a setting that has no additional options -->
<local:StringDefaultTemplateSelector x:Key="ExpanderPreviewTemplateSelector"
StringTemplate="{StaticResource ExpanderSettingContainerStringPreviewTemplate}" />
<Style TargetType="local:SettingContainer">
<Setter Property="Margin" Value="0,4,0,0" />
<Setter Property="IsTabStop" Value="False" />
@@ -197,274 +139,64 @@
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:SettingContainer">
<Grid AutomationProperties.Name="{TemplateBinding Header}"
Style="{StaticResource NonExpanderGrid}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0"
Style="{StaticResource StackPanelInExpanderStyle}">
<StackPanel Orientation="Horizontal">
<TextBlock Style="{StaticResource SettingsPageItemHeaderStyle}"
Text="{TemplateBinding Header}" />
<local:SettingsCard x:Name="Card"
Content="{TemplateBinding Content}">
<local:SettingsCard.Header>
<StackPanel VerticalAlignment="Center"
Orientation="Horizontal">
<ContentControl Content="{TemplateBinding Header}"
IsTabStop="False" />
<Button x:Name="ResetButton"
Style="{StaticResource SettingContainerResetButtonStyle}">
<FontIcon Glyph="&#xE845;"
Style="{StaticResource SettingContainerFontIconStyle}" />
</Button>
</StackPanel>
<TextBlock x:Name="HelpTextBlock"
Style="{StaticResource SettingsPageItemDescriptionStyle}"
Text="{TemplateBinding HelpText}" />
</StackPanel>
<ContentPresenter Grid.Column="1"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Content="{TemplateBinding Content}" />
</Grid>
</local:SettingsCard.Header>
</local:SettingsCard>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- A basic setting container displaying immutable text as content -->
<Style x:Key="SettingContainerWithTextContent"
TargetType="local:SettingContainer">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:SettingContainer">
<Grid AutomationProperties.Name="{TemplateBinding Header}"
Style="{StaticResource NonExpanderGrid}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0"
Style="{StaticResource StackPanelInExpanderStyle}">
<StackPanel Orientation="Horizontal">
<TextBlock Style="{StaticResource SettingsPageItemHeaderStyle}"
Text="{TemplateBinding Header}" />
<Button x:Name="ResetButton"
Style="{StaticResource SettingContainerResetButtonStyle}">
<FontIcon Glyph="&#xE845;"
Style="{StaticResource SettingContainerFontIconStyle}" />
</Button>
</StackPanel>
<TextBlock x:Name="HelpTextBlock"
Style="{StaticResource SettingsPageItemDescriptionStyle}"
Text="{TemplateBinding HelpText}" />
</StackPanel>
<TextBlock Grid.Column="1"
Margin="0,0,8,0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Style="{ThemeResource SecondaryTextBlockStyle}"
Text="{TemplateBinding Content}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!--
A setting container for a setting that has no additional options.
Includes space for an icon on the left side of the header.
XAML applies the margin/padding of the icon regardless of its
existence (which caused inconsistent padding). It's easier to just create
another style and move on.
-->
<Style x:Key="SettingContainerWithIcon"
TargetType="local:SettingContainer">
<Setter Property="Margin" Value="0,4,0,0" />
<Setter Property="IsTabStop" Value="False" />
<Setter Property="MaxWidth" Value="1000" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:SettingContainer">
<Grid AutomationProperties.Name="{TemplateBinding Header}"
Style="{StaticResource NonExpanderGrid}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<FontIcon Grid.Column="0"
Glyph="{TemplateBinding FontIconGlyph}" />
<StackPanel Grid.Column="1"
Padding="14,12,0,12"
Style="{StaticResource StackPanelInExpanderStyle}">
<StackPanel Orientation="Horizontal">
<TextBlock Style="{StaticResource SettingsPageItemHeaderStyle}"
Text="{TemplateBinding Header}" />
<Button x:Name="ResetButton"
Style="{StaticResource SettingContainerResetButtonStyle}">
<FontIcon Glyph="&#xE845;"
Style="{StaticResource SettingContainerFontIconStyle}" />
</Button>
</StackPanel>
<TextBlock x:Name="HelpTextBlock"
Style="{StaticResource SettingsPageItemDescriptionStyle}"
Text="{TemplateBinding HelpText}" />
</StackPanel>
<ContentPresenter Grid.Column="2"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Content="{TemplateBinding Content}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- A setting container which can expand -->
<Style x:Key="ExpanderSettingContainerStyle"
TargetType="local:SettingContainer">
<Setter Property="Margin" Value="0,4,0,0" />
<Setter Property="MaxWidth" Value="1000" />
<Setter Property="IsTabStop" Value="False" />
<Setter Property="AutomationProperties.AccessibilityView" Value="Raw" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:SettingContainer">
<muxc:Expander x:Name="Expander"
Margin="0,4,0,0"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Content="{TemplateBinding Content}"
IsExpanded="{TemplateBinding StartExpanded}">
<muxc:Expander.Header>
<Grid MinHeight="64">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0"
Style="{StaticResource StackPanelInExpanderStyle}">
<StackPanel Orientation="Horizontal">
<ContentPresenter Content="{TemplateBinding Header}" />
<Button x:Name="ResetButton"
Style="{StaticResource SettingContainerResetButtonStyle}">
<FontIcon Glyph="&#xE845;"
Style="{StaticResource SettingContainerFontIconStyle}" />
</Button>
</StackPanel>
<TextBlock x:Name="HelpTextBlock"
Style="{StaticResource SettingsPageItemDescriptionStyle}"
Text="{TemplateBinding HelpText}" />
</StackPanel>
<ContentPresenter Grid.Column="1"
Content="{TemplateBinding CurrentValue}"
ContentTemplate="{StaticResource ExpanderSettingContainerStringPreviewTemplate}" />
</Grid>
</muxc:Expander.Header>
</muxc:Expander>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!--
A setting container which can expand. Includes space for an icon on the left side of the header.
XAML applies the margin/padding of the icon regardless of its
existence (which caused inconsistent padding). It's easier to just create
another style and move on.
-->
<Style x:Key="ExpanderSettingContainerStyleWithIcon"
TargetType="local:SettingContainer">
<Setter Property="MaxWidth" Value="1000" />
<Setter Property="IsTabStop" Value="False" />
<Setter Property="AutomationProperties.AccessibilityView" Value="Raw" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:SettingContainer">
<muxc:Expander x:Name="Expander"
Margin="0,4,0,0"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Content="{TemplateBinding Content}"
IsExpanded="{TemplateBinding StartExpanded}">
<muxc:Expander.Header>
<Grid MinHeight="64">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<FontIcon Grid.Column="0"
Glyph="{TemplateBinding FontIconGlyph}" />
<StackPanel Grid.Column="1"
Padding="14,12,0,12"
Style="{StaticResource StackPanelInExpanderStyle}">
<StackPanel Orientation="Horizontal">
<TextBlock Style="{StaticResource SettingsPageItemHeaderStyle}"
Text="{TemplateBinding Header}" />
<Button x:Name="ResetButton"
Style="{StaticResource SettingContainerResetButtonStyle}">
<FontIcon Glyph="&#xE845;"
Style="{StaticResource SettingContainerFontIconStyle}" />
</Button>
</StackPanel>
<TextBlock x:Name="HelpTextBlock"
Style="{StaticResource SettingsPageItemDescriptionStyle}"
Text="{TemplateBinding HelpText}" />
</StackPanel>
<ContentPresenter Grid.Column="2"
Content="{TemplateBinding CurrentValue}"
ContentTemplate="{StaticResource ExpanderSettingContainerStringPreviewTemplate}" />
</Grid>
</muxc:Expander.Header>
</muxc:Expander>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- A setting container which can expand. Supports data template override for preview -->
<Style x:Key="ExpanderSettingContainerStyleWithComplexPreview"
TargetType="local:SettingContainer">
<Setter Property="MaxWidth" Value="1000" />
<Setter Property="IsTabStop" Value="False" />
<Setter Property="AutomationProperties.AccessibilityView" Value="Raw" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:SettingContainer">
<muxc:Expander x:Name="Expander"
Margin="0,4,0,0"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Content="{TemplateBinding Content}"
IsExpanded="{TemplateBinding StartExpanded}">
<muxc:Expander.Header>
<Grid MinHeight="64">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0"
Style="{StaticResource StackPanelInExpanderStyle}">
<StackPanel Orientation="Horizontal">
<TextBlock Style="{StaticResource SettingsPageItemHeaderStyle}"
Text="{TemplateBinding Header}" />
<Button x:Name="ResetButton"
Style="{StaticResource SettingContainerResetButtonStyle}">
<FontIcon Glyph="&#xE845;"
Style="{StaticResource SettingContainerFontIconStyle}" />
</Button>
</StackPanel>
<TextBlock x:Name="HelpTextBlock"
Style="{StaticResource SettingsPageItemDescriptionStyle}"
Text="{TemplateBinding HelpText}" />
</StackPanel>
<ContentPresenter Grid.Column="1"
MaxWidth="248"
Margin="0,0,-16,0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Content="{TemplateBinding CurrentValue}"
ContentTemplate="{TemplateBinding CurrentValueTemplate}" />
</Grid>
</muxc:Expander.Header>
</muxc:Expander>
<local:SettingsExpander x:Name="Expander"
IsExpanded="{TemplateBinding StartExpanded}">
<local:SettingsExpander.Header>
<StackPanel VerticalAlignment="Center"
Orientation="Horizontal">
<ContentControl Content="{TemplateBinding Header}"
IsTabStop="False" />
<Button x:Name="ResetButton"
Style="{StaticResource SettingContainerResetButtonStyle}">
<FontIcon Glyph="&#xE845;"
Style="{StaticResource SettingContainerFontIconStyle}" />
</Button>
</StackPanel>
</local:SettingsExpander.Header>
<local:SettingsExpander.Content>
<ContentControl MaxWidth="248"
HorizontalAlignment="Right"
VerticalAlignment="Center"
HorizontalContentAlignment="Right"
Content="{TemplateBinding CurrentValue}"
ContentTemplate="{TemplateBinding CurrentValueTemplate}"
ContentTemplateSelector="{StaticResource ExpanderPreviewTemplateSelector}"
IsTabStop="False" />
</local:SettingsExpander.Content>
<local:SettingsExpander.ItemsHeader>
<ContentPresenter Padding="16,12,16,16"
Content="{TemplateBinding Content}" />
</local:SettingsExpander.ItemsHeader>
</local:SettingsExpander>
</ControlTemplate>
</Setter.Value>
</Setter>
@@ -479,47 +211,19 @@
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:SettingContainer">
<Grid AutomationProperties.Name="{TemplateBinding Header}"
Background="{ThemeResource SettingContainerWarningSeverityBackgroundBrush}"
Style="{StaticResource NonExpanderGrid}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Style="{StaticResource StackPanelInExpanderStyle}">
<StackPanel Orientation="Horizontal">
<Grid>
<TextBlock x:Name="IconBackground"
Grid.Column="0"
Margin="{StaticResource SettingContainerIconMargin}"
VerticalAlignment="Center"
AutomationProperties.AccessibilityView="Raw"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
FontSize="{StaticResource SettingContainerIconFontSize}"
Foreground="{ThemeResource SettingContainerWarningSeverityIconBackground}"
Text="{StaticResource SettingContainerIconBackgroundGlyph}" />
<TextBlock x:Name="StandardIcon"
Grid.Column="0"
Margin="{StaticResource SettingContainerIconMargin}"
VerticalAlignment="Center"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
FontSize="{StaticResource SettingContainerIconFontSize}"
Foreground="{ThemeResource SettingContainerWarningSeverityIconForeground}"
Text="{StaticResource SettingContainerWarningIconGlyph}" />
</Grid>
<TextBlock VerticalAlignment="Center"
FontWeight="{StaticResource SettingContainerTitleFontWeight}"
Foreground="{ThemeResource SettingContainerTitleForeground}"
Style="{StaticResource SettingsPageItemHeaderStyle}"
Text="{TemplateBinding Header}" />
</StackPanel>
<TextBlock x:Name="HelpTextBlock"
Foreground="{ThemeResource SettingContainerMessageForeground}"
Style="{StaticResource SettingsPageItemDescriptionStyle}"
Text="{TemplateBinding HelpText}" />
</StackPanel>
</Grid>
<local:SettingsCard x:Name="Card"
Background="{ThemeResource SettingContainerWarningSeverityBackgroundBrush}">
<local:SettingsCard.Header>
<ContentControl Content="{TemplateBinding Header}"
IsTabStop="False" />
</local:SettingsCard.Header>
<local:SettingsCard.HeaderIcon>
<FontIcon FontFamily="{ThemeResource SymbolThemeFontFamily}"
FontSize="{StaticResource SettingContainerIconFontSize}"
Foreground="{ThemeResource SettingContainerWarningSeverityIconBackground}"
Glyph="{StaticResource SettingContainerWarningIconGlyph}" />
</local:SettingsCard.HeaderIcon>
</local:SettingsCard>
</ControlTemplate>
</Setter.Value>
</Setter>
@@ -534,47 +238,19 @@
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:SettingContainer">
<Grid AutomationProperties.Name="{TemplateBinding Header}"
Background="{ThemeResource SettingContainerErrorSeverityBackgroundBrush}"
Style="{StaticResource NonExpanderGrid}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Style="{StaticResource StackPanelInExpanderStyle}">
<StackPanel Orientation="Horizontal">
<Grid>
<TextBlock x:Name="IconBackground"
Grid.Column="0"
Margin="{StaticResource SettingContainerIconMargin}"
VerticalAlignment="Center"
AutomationProperties.AccessibilityView="Raw"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
FontSize="{StaticResource SettingContainerIconFontSize}"
Foreground="{ThemeResource SettingContainerErrorSeverityIconBackground}"
Text="{StaticResource SettingContainerIconBackgroundGlyph}" />
<TextBlock x:Name="StandardIcon"
Grid.Column="0"
Margin="{StaticResource SettingContainerIconMargin}"
VerticalAlignment="Center"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
FontSize="{StaticResource SettingContainerIconFontSize}"
Foreground="{ThemeResource SettingContainerErrorSeverityIconForeground}"
Text="{StaticResource SettingContainerErrorIconGlyph}" />
</Grid>
<TextBlock VerticalAlignment="Center"
FontWeight="{StaticResource SettingContainerTitleFontWeight}"
Foreground="{ThemeResource SettingContainerTitleForeground}"
Style="{StaticResource SettingsPageItemHeaderStyle}"
Text="{TemplateBinding Header}" />
</StackPanel>
<TextBlock x:Name="HelpTextBlock"
Foreground="{ThemeResource SettingContainerMessageForeground}"
Style="{StaticResource SettingsPageItemDescriptionStyle}"
Text="{TemplateBinding HelpText}" />
</StackPanel>
</Grid>
<local:SettingsCard x:Name="Card"
Background="{ThemeResource SettingContainerErrorSeverityBackgroundBrush}">
<local:SettingsCard.Header>
<ContentControl Content="{TemplateBinding Header}"
IsTabStop="False" />
</local:SettingsCard.Header>
<local:SettingsCard.HeaderIcon>
<FontIcon FontFamily="{ThemeResource SymbolThemeFontFamily}"
FontSize="{StaticResource SettingContainerIconFontSize}"
Foreground="{ThemeResource SettingContainerErrorSeverityIconBackground}"
Glyph="{StaticResource SettingContainerErrorIconGlyph}" />
</local:SettingsCard.HeaderIcon>
</local:SettingsCard>
</ControlTemplate>
</Setter.Value>
</Setter>

View File

@@ -0,0 +1,670 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "SettingsCard.h"
#include "SettingsCard.g.cpp"
#include "SettingsCardAutomationPeer.g.cpp"
#include "StyleExtensions.h"
using namespace winrt::Windows::Foundation;
using namespace winrt::Windows::System;
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;
using namespace winrt::Windows::UI::Xaml::Controls::Primitives;
using namespace winrt::Windows::UI::Xaml::Input;
using namespace winrt::Windows::UI::Xaml::Media;
namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
DependencyProperty SettingsCard::_HeaderProperty{ nullptr };
DependencyProperty SettingsCard::_DescriptionProperty{ nullptr };
DependencyProperty SettingsCard::_HeaderIconProperty{ nullptr };
DependencyProperty SettingsCard::_ActionIconProperty{ nullptr };
DependencyProperty SettingsCard::_ActionIconToolTipProperty{ nullptr };
DependencyProperty SettingsCard::_IsClickEnabledProperty{ nullptr };
DependencyProperty SettingsCard::_IsActionIconVisibleProperty{ nullptr };
DependencyProperty SettingsCard::_ContentAlignmentProperty{ nullptr };
static constexpr std::wstring_view NormalState{ L"Normal" };
static constexpr std::wstring_view PointerOverState{ L"PointerOver" };
static constexpr std::wstring_view PressedState{ L"Pressed" };
static constexpr std::wstring_view DisabledState{ L"Disabled" };
static constexpr std::wstring_view BitmapHeaderIconEnabledState{ L"BitmapHeaderIconEnabled" };
static constexpr std::wstring_view BitmapHeaderIconDisabledState{ L"BitmapHeaderIconDisabled" };
static constexpr std::wstring_view RightState{ L"Right" };
static constexpr std::wstring_view LeftState{ L"Left" };
static constexpr std::wstring_view VerticalState{ L"Vertical" };
static constexpr std::wstring_view NoContentSpacingState{ L"NoContentSpacing" };
static constexpr std::wstring_view ContentSpacingState{ L"ContentSpacing" };
static constexpr std::wstring_view ContentAlignmentStatesGroup{ L"ContentAlignmentStates" };
static constexpr std::wstring_view ActionIconPresenterHolder{ L"PART_ActionIconPresenterHolder" };
static constexpr std::wstring_view HeaderPresenter{ L"PART_HeaderPresenter" };
static constexpr std::wstring_view DescriptionPresenter{ L"PART_DescriptionPresenter" };
static constexpr std::wstring_view HeaderIconPresenterHolder{ L"PART_HeaderIconPresenterHolder" };
static constexpr std::wstring_view ContentPresenterPart{ L"PART_ContentPresenter" };
static constexpr std::wstring_view RootGridPart{ L"PART_RootGrid" };
static constexpr double SettingsCardVerticalHeaderContentSpacing{ 8.0 };
// Returns true if the given object is null, or is a string that is empty.
// Non-string non-null objects (e.g. a TextBlock) are considered "non-empty".
static bool _isNullOrEmpty(const winrt::Windows::Foundation::IInspectable& obj)
{
if (!obj)
{
return true;
}
if (const auto pv{ obj.try_as<IPropertyValue>() }; pv && pv.Type() == PropertyType::String)
{
return unbox_value_or<hstring>(obj, hstring{}).empty();
}
return false;
}
SettingsCard::SettingsCard()
{
_InitializeProperties();
}
void SettingsCard::_InitializeProperties()
{
if (!_HeaderProperty)
{
_HeaderProperty = DependencyProperty::Register(
L"Header",
xaml_typename<IInspectable>(),
xaml_typename<Editor::SettingsCard>(),
PropertyMetadata{ nullptr, PropertyChangedCallback{ &SettingsCard::_OnHeaderChanged } });
}
if (!_DescriptionProperty)
{
_DescriptionProperty = DependencyProperty::Register(
L"Description",
xaml_typename<IInspectable>(),
xaml_typename<Editor::SettingsCard>(),
PropertyMetadata{ nullptr, PropertyChangedCallback{ &SettingsCard::_OnDescriptionChanged } });
}
if (!_HeaderIconProperty)
{
_HeaderIconProperty = DependencyProperty::Register(
L"HeaderIcon",
xaml_typename<IconElement>(),
xaml_typename<Editor::SettingsCard>(),
PropertyMetadata{ nullptr, PropertyChangedCallback{ &SettingsCard::_OnHeaderIconChanged } });
}
if (!_ActionIconProperty)
{
// No metadata default value — a fresh FontIcon is allocated per
// card in OnApplyTemplate when the user hasn't set ActionIcon
// explicitly. We cannot use a default FontIcon here because
// UIElements have a single-parent constraint (one instance can't
// be shared across cards) and a default string was rendering
// through ContentPresenter -> TextBlock, whose line-height
// padding made the Viewbox-scaled glyph visibly smaller than
// WCT's SymbolIcon default.
_ActionIconProperty = DependencyProperty::Register(
L"ActionIcon",
xaml_typename<IInspectable>(),
xaml_typename<Editor::SettingsCard>(),
PropertyMetadata{ nullptr });
}
if (!_ActionIconToolTipProperty)
{
_ActionIconToolTipProperty = DependencyProperty::Register(
L"ActionIconToolTip",
xaml_typename<hstring>(),
xaml_typename<Editor::SettingsCard>(),
PropertyMetadata{ box_value(hstring{}) });
}
if (!_IsClickEnabledProperty)
{
_IsClickEnabledProperty = DependencyProperty::Register(
L"IsClickEnabled",
xaml_typename<bool>(),
xaml_typename<Editor::SettingsCard>(),
PropertyMetadata{ box_value(false), PropertyChangedCallback{ &SettingsCard::_OnIsClickEnabledChanged } });
}
if (!_IsActionIconVisibleProperty)
{
_IsActionIconVisibleProperty = DependencyProperty::Register(
L"IsActionIconVisible",
xaml_typename<bool>(),
xaml_typename<Editor::SettingsCard>(),
PropertyMetadata{ box_value(true), PropertyChangedCallback{ &SettingsCard::_OnIsActionIconVisibleChanged } });
}
if (!_ContentAlignmentProperty)
{
_ContentAlignmentProperty = DependencyProperty::Register(
L"ContentAlignment",
xaml_typename<Editor::SettingsCardContentAlignment>(),
xaml_typename<Editor::SettingsCard>(),
PropertyMetadata{ box_value(Editor::SettingsCardContentAlignment::Right), PropertyChangedCallback{ &SettingsCard::_OnContentAlignmentChanged } });
}
}
AutomationPeer SettingsCard::OnCreateAutomationPeer()
{
return winrt::make<implementation::SettingsCardAutomationPeer>(*this);
}
// Pointer overrides: gate the ButtonBase click pipeline on IsClickEnabled.
// When IsClickEnabled=false, we do NOT call the base method, so the event
// is left unhandled and bubbles up the visual tree. This is what lets the
// SettingsExpander header (a ToggleButton hosting a SettingsCard with
// IsClickEnabled=false) toggle on a click anywhere across the header row,
// not just on the chevron. Mirrors the Community Toolkit's SettingsCard.cs.
void SettingsCard::OnPointerPressed(const winrt::Windows::UI::Xaml::Input::PointerRoutedEventArgs& e)
{
if (IsClickEnabled())
{
base_type::OnPointerPressed(e);
_GoToCommonState(PressedState, true);
}
}
void SettingsCard::OnPointerReleased(const winrt::Windows::UI::Xaml::Input::PointerRoutedEventArgs& e)
{
if (IsClickEnabled())
{
base_type::OnPointerReleased(e);
_GoToCommonState(NormalState, true);
}
}
void SettingsCard::OnApplyTemplate()
{
// Match WCT's SettingsCard.OnApplyTemplate() which calls base first so the
// template parts are fully wired up before any of our post-template setup
// (visibility, visual states, click interaction) runs.
base_type::OnApplyTemplate();
// Inject the shared implicit ToggleSwitch / Slider / ComboBox / TextBox
// dictionary so child controls in our Content slot get the Windows 11
// settings-page defaults without an explicit Style attribute. Bypasses
// the WCT Setter.Value-with-inline-ResourceDictionary pattern, which
// crashes WinUI 2 XAML load in this codebase.
StyleExtensions::EnsureImplicitStylesMergedInto(*this);
// Drop any handlers from a previous template.
_isEnabledChangedRevoker.revoke();
_contentAlignmentStatesChangedRevoker.revoke();
_DisableButtonInteraction();
if (_contentChangedToken != 0)
{
UnregisterPropertyChangedCallback(ContentControl::ContentProperty(), _contentChangedToken);
_contentChangedToken = 0;
}
_UpdateActionIconVisibility();
_UpdateHeaderVisibility();
_UpdateDescriptionVisibility();
_UpdateHeaderIconVisibility();
// Initial visual states.
_CheckInitialVisualState();
_CheckHeaderIconState();
_SetAccessibleContentName();
// Watch for Content changing later (we may need to refresh the AutomationProperties.Name on it).
_contentChangedToken = RegisterPropertyChangedCallback(ContentControl::ContentProperty(), [weakThis = get_weak()](auto&&, auto&&) {
if (const auto strongThis = weakThis.get())
{
strongThis->_SetAccessibleContentName();
}
});
// Apply click-interaction state.
if (IsClickEnabled())
{
_EnableButtonInteraction();
}
_isEnabledChangedRevoker = IsEnabledChanged(winrt::auto_revoke, [weakThis = get_weak()](auto&&, auto&&) {
if (const auto strongThis = weakThis.get())
{
strongThis->_GoToCommonState(strongThis->IsEnabled() ? NormalState : DisabledState, true);
strongThis->_CheckHeaderIconState();
}
});
}
void SettingsCard::_CheckInitialVisualState()
{
VisualStateManager::GoToState(*this, IsEnabled() ? hstring{ NormalState } : hstring{ DisabledState }, true);
_UpdateContentAlignmentState();
// Subscribe to ContentAlignmentStates so we can drive ContentSpacingStates
// whenever the alignment shifts to a stacked layout (Vertical, RightWrapped*).
if (const auto child{ GetTemplateChild(hstring{ ContentAlignmentStatesGroup }) })
{
if (const auto group{ child.try_as<VisualStateGroup>() })
{
_CheckVerticalSpacingState(group.CurrentState());
_contentAlignmentStatesChangedRevoker = group.CurrentStateChanged(winrt::auto_revoke, [weakThis = get_weak()](auto&&, const VisualStateChangedEventArgs& args) {
if (const auto strongThis = weakThis.get())
{
strongThis->_CheckVerticalSpacingState(args.NewState());
}
});
}
}
}
void SettingsCard::_CheckHeaderIconState()
{
// The Disabled common state recolors text/glyph foregrounds via the brush, but a
// BitmapIcon is an image and won't pick up the disabled brush. Lower its opacity
// instead, via the BitmapHeaderIconStates group. Mirrors the toolkit's
// SettingsCard.cs::CheckHeaderIconState.
if (HeaderIcon().try_as<BitmapIcon>())
{
VisualStateManager::GoToState(*this,
hstring{ IsEnabled() ? BitmapHeaderIconEnabledState : BitmapHeaderIconDisabledState },
true);
}
else
{
// Reset to the enabled state when a non-bitmap icon (or none) is present so the
// opacity setter doesn't stick around from a previous bitmap icon.
VisualStateManager::GoToState(*this, hstring{ BitmapHeaderIconEnabledState }, true);
}
}
void SettingsCard::_CheckVerticalSpacingState(const VisualState& state)
{
// Add row spacing whenever the content sits below the header (Vertical or RightWrapped*)
// AND there's both Content and (Header or Description) to space apart.
//
// For the Vertical case we read ContentAlignment() directly rather than
// state.Name(): our Left/Vertical visual states are empty placeholders
// (the actual layout work happens in _UpdateContentAlignmentState via
// direct C++ property writes) and an empty <VisualState> may not have
// its name register as CurrentState when GoToState fires in WinUI 2 —
// leaving stackedLayout incorrectly false. RightWrapped*, in contrast,
// are size-triggered visual states with real setters and ControlSizeTrigger,
// so their CurrentState is reliably observed.
const auto stateName{ state ? state.Name() : hstring{} };
const bool stackedLayout =
ContentAlignment() == Editor::SettingsCardContentAlignment::Vertical ||
stateName == L"RightWrapped" ||
stateName == L"RightWrappedNoIcon";
const bool hasContent{ static_cast<bool>(Content()) };
const bool hasHeaderOrDescription = !_isNullOrEmpty(Header()) || !_isNullOrEmpty(Description());
const bool shouldSpace = stackedLayout && hasContent && hasHeaderOrDescription;
VisualStateManager::GoToState(*this,
hstring{ shouldSpace ? ContentSpacingState : NoContentSpacingState },
true);
// Belt-and-suspenders for the WinUI 2 quirk: VisualState.Setters activated
// by C++ GoToState don't reliably engage. The ContentSpacing setter
// targets PART_RootGrid.RowSpacing; set it directly here as well so the
// Vertical / RightWrapped stacked-layout cards actually get the breathing
// room between header and content that WCT shows.
if (const auto child{ GetTemplateChild(hstring{ RootGridPart }) })
{
if (const auto rootGrid{ child.try_as<Grid>() })
{
rootGrid.RowSpacing(shouldSpace ? SettingsCardVerticalHeaderContentSpacing : 0.0);
}
}
}
void SettingsCard::_SetAccessibleContentName()
{
// If Header is a string and the inner Content lacks an AutomationProperties.Name, propagate the header
// into the content so screen readers can announce something meaningful when focus lands there.
const auto headerObj{ Header() };
if (!headerObj)
{
return;
}
const auto headerString{ unbox_value_or<hstring>(headerObj, hstring{}) };
if (headerString.empty())
{
return;
}
const auto contentObj{ Content() };
if (const auto element{ contentObj.try_as<UIElement>() })
{
if (Automation::AutomationProperties::GetName(element).empty())
{
// Don't override ButtonBase content (would clobber its own name) or plain text blocks.
if (!element.try_as<ButtonBase>() && !element.try_as<TextBlock>())
{
Automation::AutomationProperties::SetName(element, headerString);
}
}
}
}
void SettingsCard::_EnableButtonInteraction()
{
if (_interactionEnabled)
{
return;
}
_interactionEnabled = true;
IsTabStop(true);
_pointerEnteredRevoker = PointerEntered(winrt::auto_revoke, [weakThis = get_weak()](auto&&, auto&&) {
if (const auto strongThis = weakThis.get())
{
strongThis->_GoToCommonState(PointerOverState, true);
}
});
_pointerExitedRevoker = PointerExited(winrt::auto_revoke, [weakThis = get_weak()](auto&&, auto&&) {
if (const auto strongThis = weakThis.get())
{
strongThis->_GoToCommonState(NormalState, true);
}
});
_pointerCaptureLostRevoker = PointerCaptureLost(winrt::auto_revoke, [weakThis = get_weak()](auto&&, auto&&) {
if (const auto strongThis = weakThis.get())
{
strongThis->_GoToCommonState(NormalState, true);
}
});
_pointerCanceledRevoker = PointerCanceled(winrt::auto_revoke, [weakThis = get_weak()](auto&&, auto&&) {
if (const auto strongThis = weakThis.get())
{
strongThis->_GoToCommonState(NormalState, true);
}
});
_previewKeyDownRevoker = PreviewKeyDown(winrt::auto_revoke, [weakThis = get_weak()](auto&&, const Windows::UI::Xaml::Input::KeyRoutedEventArgs& e) {
const auto strongThis = weakThis.get();
if (!strongThis)
{
return;
}
const auto key = e.Key();
if (key == Windows::System::VirtualKey::Enter || key == Windows::System::VirtualKey::Space || key == Windows::System::VirtualKey::GamepadA)
{
const auto focused{ strongThis->_GetFocusedElement() };
if (focused && focused.try_as<Editor::SettingsCard>() == strongThis.as<Editor::SettingsCard>())
{
strongThis->_GoToCommonState(PressedState, true);
}
}
});
_previewKeyUpRevoker = PreviewKeyUp(winrt::auto_revoke, [weakThis = get_weak()](auto&&, const Windows::UI::Xaml::Input::KeyRoutedEventArgs& e) {
const auto strongThis = weakThis.get();
if (!strongThis)
{
return;
}
const auto key = e.Key();
if (key == Windows::System::VirtualKey::Enter || key == Windows::System::VirtualKey::Space || key == Windows::System::VirtualKey::GamepadA)
{
strongThis->_GoToCommonState(NormalState, true);
}
});
}
void SettingsCard::_DisableButtonInteraction()
{
_interactionEnabled = false;
IsTabStop(false);
_pointerEnteredRevoker.revoke();
_pointerExitedRevoker.revoke();
_pointerCaptureLostRevoker.revoke();
_pointerCanceledRevoker.revoke();
_previewKeyDownRevoker.revoke();
_previewKeyUpRevoker.revoke();
}
void SettingsCard::_GoToCommonState(const std::wstring_view& state, bool useTransitions)
{
VisualStateManager::GoToState(*this, hstring{ state }, useTransitions);
}
FrameworkElement SettingsCard::_GetFocusedElement()
{
if (const auto root{ XamlRoot() })
{
return FocusManager::GetFocusedElement(root).try_as<FrameworkElement>();
}
return FocusManager::GetFocusedElement().try_as<FrameworkElement>();
}
void SettingsCard::_UpdateActionIconVisibility()
{
if (const auto child{ GetTemplateChild(hstring{ ActionIconPresenterHolder }) })
{
if (const auto frameworkChild{ child.try_as<FrameworkElement>() })
{
frameworkChild.Visibility((IsClickEnabled() && IsActionIconVisible()) ? Visibility::Visible : Visibility::Collapsed);
}
}
}
void SettingsCard::_UpdateHeaderVisibility()
{
if (const auto child{ GetTemplateChild(hstring{ HeaderPresenter }) })
{
if (const auto frameworkChild{ child.try_as<FrameworkElement>() })
{
frameworkChild.Visibility(_isNullOrEmpty(Header()) ? Visibility::Collapsed : Visibility::Visible);
}
}
}
void SettingsCard::_UpdateDescriptionVisibility()
{
if (const auto child{ GetTemplateChild(hstring{ DescriptionPresenter }) })
{
if (const auto frameworkChild{ child.try_as<FrameworkElement>() })
{
frameworkChild.Visibility(_isNullOrEmpty(Description()) ? Visibility::Collapsed : Visibility::Visible);
}
}
}
void SettingsCard::_UpdateHeaderIconVisibility()
{
if (const auto child{ GetTemplateChild(hstring{ HeaderIconPresenterHolder }) })
{
if (const auto frameworkChild{ child.try_as<FrameworkElement>() })
{
frameworkChild.Visibility(HeaderIcon() ? Visibility::Visible : Visibility::Collapsed);
}
}
}
void SettingsCard::_UpdateContentAlignmentState()
{
// Map ContentAlignment to the visual state name *and* the corresponding
// direct layout values. We drive both: GoToState keeps the visual state
// machine in sync (so _CheckVerticalSpacingState sees the right CurrentState),
// but we *also* apply the layout properties directly to PART_ContentPresenter
// because in WinUI 2, attached-property changes (Grid.Row/Grid.Column) and
// HorizontalAlignment changes inside VisualState.Setters or Storyboards
// don't reliably engage when activated by programmatic VisualStateManager::GoToState
// — only by XAML StateTriggers (which is what WCT uses via tk:IsEqualStateTrigger).
// Until/unless an equivalent state trigger gets ported, the direct C++ set is
// the load-bearing path for Left/Vertical.
std::wstring_view state{ RightState };
int contentRow{ 0 };
int contentColumn{ 2 };
auto contentHA{ HorizontalAlignment::Right };
const auto alignment = ContentAlignment();
switch (alignment)
{
case Editor::SettingsCardContentAlignment::Left:
state = LeftState;
contentRow = 1;
contentColumn = 1;
contentHA = HorizontalAlignment::Left;
break;
case Editor::SettingsCardContentAlignment::Vertical:
state = VerticalState;
contentRow = 1;
contentColumn = 1;
contentHA = HorizontalAlignment::Stretch;
break;
default:
break;
}
VisualStateManager::GoToState(*this, hstring{ state }, true);
if (const auto child{ GetTemplateChild(hstring{ ContentPresenterPart }) })
{
if (const auto contentPresenter{ child.try_as<FrameworkElement>() })
{
Grid::SetRow(contentPresenter, contentRow);
Grid::SetColumn(contentPresenter, contentColumn);
contentPresenter.HorizontalAlignment(contentHA);
// Vertical mirrors WCT's TemplatedParent Binding setter: propagate
// the card's own HorizontalContentAlignment to the inner ContentPresenter
// so e.g. HorizontalContentAlignment="Left" + ContentAlignment="Vertical"
// actually left-aligns the inner content.
if (const auto cp{ contentPresenter.try_as<ContentPresenter>() })
{
cp.HorizontalContentAlignment(HorizontalContentAlignment());
}
}
}
// Left collapses the HeaderIcon column regardless of HeaderIcon content.
if (const auto child{ GetTemplateChild(hstring{ HeaderIconPresenterHolder }) })
{
if (const auto headerIconHolder{ child.try_as<FrameworkElement>() })
{
if (alignment == Editor::SettingsCardContentAlignment::Left)
{
headerIconHolder.Visibility(Visibility::Collapsed);
}
else
{
headerIconHolder.Visibility(HeaderIcon() ? Visibility::Visible : Visibility::Collapsed);
}
}
}
}
void SettingsCard::_OnHeaderChanged(const DependencyObject& d, const DependencyPropertyChangedEventArgs& /*e*/)
{
const auto obj{ d.try_as<Editor::SettingsCard>() };
const auto self = get_self<SettingsCard>(obj);
self->_UpdateHeaderVisibility();
self->_SetAccessibleContentName();
}
void SettingsCard::_OnDescriptionChanged(const DependencyObject& d, const DependencyPropertyChangedEventArgs& /*e*/)
{
const auto obj{ d.try_as<Editor::SettingsCard>() };
get_self<SettingsCard>(obj)->_UpdateDescriptionVisibility();
}
void SettingsCard::_OnHeaderIconChanged(const DependencyObject& d, const DependencyPropertyChangedEventArgs& /*e*/)
{
const auto obj{ d.try_as<Editor::SettingsCard>() };
const auto self = get_self<SettingsCard>(obj);
self->_UpdateHeaderIconVisibility();
// HeaderIcon type may have flipped between BitmapIcon and other icon types — re-evaluate
// the BitmapHeaderIcon visual state so the disabled-opacity setter is applied (or cleared).
self->_CheckHeaderIconState();
}
void SettingsCard::_OnIsClickEnabledChanged(const DependencyObject& d, const DependencyPropertyChangedEventArgs& /*e*/)
{
const auto obj{ d.try_as<Editor::SettingsCard>() };
const auto self = get_self<SettingsCard>(obj);
self->_UpdateActionIconVisibility();
if (self->IsClickEnabled())
{
self->_EnableButtonInteraction();
}
else
{
self->_DisableButtonInteraction();
}
}
void SettingsCard::_OnIsActionIconVisibleChanged(const DependencyObject& d, const DependencyPropertyChangedEventArgs& /*e*/)
{
const auto obj{ d.try_as<Editor::SettingsCard>() };
get_self<SettingsCard>(obj)->_UpdateActionIconVisibility();
}
void SettingsCard::_OnContentAlignmentChanged(const DependencyObject& d, const DependencyPropertyChangedEventArgs& /*e*/)
{
const auto obj{ d.try_as<Editor::SettingsCard>() };
get_self<SettingsCard>(obj)->_UpdateContentAlignmentState();
}
SettingsCardAutomationPeer::SettingsCardAutomationPeer(const Editor::SettingsCard& owner) :
SettingsCardAutomationPeerT<SettingsCardAutomationPeer>(owner)
{
}
AutomationControlType SettingsCardAutomationPeer::GetAutomationControlTypeCore() const
{
if (const auto card{ Owner().try_as<Editor::SettingsCard>() })
{
if (card.IsClickEnabled())
{
return AutomationControlType::Button;
}
}
return AutomationControlType::Group;
}
hstring SettingsCardAutomationPeer::GetClassNameCore() const
{
return hstring{ L"SettingsCard" };
}
hstring SettingsCardAutomationPeer::GetNameCore() const
{
if (const auto card{ Owner().try_as<Editor::SettingsCard>() })
{
if (card.IsClickEnabled())
{
if (const auto manualName{ AutomationProperties::GetName(card) }; !manualName.empty())
{
return manualName;
}
if (const auto headerString{ unbox_value_or<hstring>(card.Header(), hstring{}) }; !headerString.empty())
{
return headerString;
}
}
// Not clickable, or no header text: fall back to AutomationProperties.Name (matching
// FrameworkElementAutomationPeer's default behavior).
return AutomationProperties::GetName(card);
}
return {};
}
winrt::Windows::Foundation::IInspectable SettingsCardAutomationPeer::GetPatternCore(PatternInterface patternInterface) const
{
if (patternInterface == PatternInterface::Invoke)
{
if (const auto card{ Owner().try_as<Editor::SettingsCard>() })
{
if (card.IsClickEnabled())
{
// Only provide Invoke pattern if the card is clickable.
return *this;
}
}
return nullptr;
}
// ButtonBaseAutomationPeer only provides Invoke; everything else returns null.
return nullptr;
}
}

View File

@@ -0,0 +1,100 @@
/*++
Copyright (c) Microsoft Corporation
Licensed under the MIT license.
Module Name:
- SettingsCard
Abstract:
- A base control for building consistent settings experiences. Based
on the Windows Community Toolkit's SettingsCard.
Author(s):
- Carlos Zamora - 2026 May
--*/
#pragma once
#include "SettingsCard.g.h"
#include "SettingsCardAutomationPeer.g.h"
#include "Utils.h"
namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
struct SettingsCard : SettingsCardT<SettingsCard>
{
public:
SettingsCard();
void OnApplyTemplate();
void OnPointerPressed(const Windows::UI::Xaml::Input::PointerRoutedEventArgs& e);
void OnPointerReleased(const Windows::UI::Xaml::Input::PointerRoutedEventArgs& e);
// Automation peer override.
Windows::UI::Xaml::Automation::Peers::AutomationPeer OnCreateAutomationPeer();
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, ActionIcon);
DEPENDENCY_PROPERTY(hstring, ActionIconToolTip);
DEPENDENCY_PROPERTY(bool, IsClickEnabled);
DEPENDENCY_PROPERTY(bool, IsActionIconVisible);
DEPENDENCY_PROPERTY(Editor::SettingsCardContentAlignment, ContentAlignment);
private:
static void _InitializeProperties();
static void _OnHeaderChanged(const Windows::UI::Xaml::DependencyObject& d, const Windows::UI::Xaml::DependencyPropertyChangedEventArgs& e);
static void _OnDescriptionChanged(const Windows::UI::Xaml::DependencyObject& d, const Windows::UI::Xaml::DependencyPropertyChangedEventArgs& e);
static void _OnHeaderIconChanged(const Windows::UI::Xaml::DependencyObject& d, const Windows::UI::Xaml::DependencyPropertyChangedEventArgs& e);
static void _OnIsClickEnabledChanged(const Windows::UI::Xaml::DependencyObject& d, const Windows::UI::Xaml::DependencyPropertyChangedEventArgs& e);
static void _OnIsActionIconVisibleChanged(const Windows::UI::Xaml::DependencyObject& d, const Windows::UI::Xaml::DependencyPropertyChangedEventArgs& e);
static void _OnContentAlignmentChanged(const Windows::UI::Xaml::DependencyObject& d, const Windows::UI::Xaml::DependencyPropertyChangedEventArgs& e);
void _EnableButtonInteraction();
void _DisableButtonInteraction();
void _GoToCommonState(const std::wstring_view& state, bool useTransitions);
void _UpdateActionIconVisibility();
void _UpdateHeaderVisibility();
void _UpdateDescriptionVisibility();
void _UpdateHeaderIconVisibility();
void _UpdateContentAlignmentState();
void _CheckInitialVisualState();
void _CheckHeaderIconState();
void _CheckVerticalSpacingState(const Windows::UI::Xaml::VisualState& state);
void _SetAccessibleContentName();
Windows::UI::Xaml::FrameworkElement _GetFocusedElement();
bool _interactionEnabled{ false };
Windows::UI::Xaml::Controls::Control::IsEnabledChanged_revoker _isEnabledChangedRevoker;
Windows::UI::Xaml::UIElement::PointerEntered_revoker _pointerEnteredRevoker;
Windows::UI::Xaml::UIElement::PointerExited_revoker _pointerExitedRevoker;
Windows::UI::Xaml::UIElement::PointerCaptureLost_revoker _pointerCaptureLostRevoker;
Windows::UI::Xaml::UIElement::PointerCanceled_revoker _pointerCanceledRevoker;
Windows::UI::Xaml::UIElement::PreviewKeyDown_revoker _previewKeyDownRevoker;
Windows::UI::Xaml::UIElement::PreviewKeyUp_revoker _previewKeyUpRevoker;
Windows::UI::Xaml::VisualStateGroup::CurrentStateChanged_revoker _contentAlignmentStatesChangedRevoker;
int64_t _contentChangedToken{ 0 };
};
// AutomationPeer for SettingsCard. Mirrors the Community Toolkit's
// SettingsCardAutomationPeer: only exposes Invoke + Button control type when
// the card has IsClickEnabled=true; otherwise reports as a Group.
struct SettingsCardAutomationPeer : SettingsCardAutomationPeerT<SettingsCardAutomationPeer>
{
public:
SettingsCardAutomationPeer(const Editor::SettingsCard& owner);
Windows::UI::Xaml::Automation::Peers::AutomationControlType GetAutomationControlTypeCore() const;
hstring GetClassNameCore() const;
hstring GetNameCore() const;
winrt::Windows::Foundation::IInspectable GetPatternCore(Windows::UI::Xaml::Automation::Peers::PatternInterface patternInterface) const;
};
}
namespace winrt::Microsoft::Terminal::Settings::Editor::factory_implementation
{
BASIC_FACTORY(SettingsCard);
BASIC_FACTORY(SettingsCardAutomationPeer);
}

View File

@@ -0,0 +1,46 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
namespace Microsoft.Terminal.Settings.Editor
{
enum SettingsCardContentAlignment
{
Right,
Left,
Vertical
};
[default_interface] runtimeclass SettingsCard : Windows.UI.Xaml.Controls.Primitives.ButtonBase
{
SettingsCard();
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 ActionIcon;
static Windows.UI.Xaml.DependencyProperty ActionIconProperty { get; };
String ActionIconToolTip;
static Windows.UI.Xaml.DependencyProperty ActionIconToolTipProperty { get; };
Boolean IsClickEnabled;
static Windows.UI.Xaml.DependencyProperty IsClickEnabledProperty { get; };
Boolean IsActionIconVisible;
static Windows.UI.Xaml.DependencyProperty IsActionIconVisibleProperty { get; };
SettingsCardContentAlignment ContentAlignment;
static Windows.UI.Xaml.DependencyProperty ContentAlignmentProperty { get; };
};
[default_interface] runtimeclass SettingsCardAutomationPeer : Windows.UI.Xaml.Automation.Peers.ButtonBaseAutomationPeer
{
SettingsCardAutomationPeer(SettingsCard owner);
};
}

View File

@@ -0,0 +1,57 @@
<!--
Copyright (c) Microsoft Corporation. All rights reserved. Licensed under
the MIT License. See LICENSE in the project root for license information.
Implicit Styles that get copied into every SettingsCard / SettingsExpander's
own Resources collection at runtime via StyleExtensions::EnsureImplicitStylesMergedInto
(called from each control's OnApplyTemplate). This is the WinUI 2 port of WCT's
local:StyleExtensions.Resources Setter pattern.
Why not the WCT Setter.Value-with-inline-ResourceDictionary approach? It
crashes WinUI 2 in this codebase with "CommonResources.xaml is not a valid
absolute URI" during XAML load. The C++ copy-into-Resources bypasses the
broken Setter pipeline entirely.
Why not Append this dictionary to MergedDictionaries directly? WinUI 2
enforces a single-parent constraint on ResourceDictionaries in
MergedDictionaries: the second element to merge the same shared dict
throws "Element is already the child of another element". The C++ helper
instead iterates this dict's entries once (Style references) and copies
them into each target's own Resources collection. Style instances are
not UIElements and can safely be shared across multiple element Resources.
Why no BasedOn references? When a ResourceDictionary is loaded via Source=
(standalone), it has no parent resource scope at parse time, so
{StaticResource SomeKeyInAnotherFile} would fail to resolve. WCT's pattern
works because the dict is parsed inline within the owner's resource scope.
Practical consequence: any implicit style here MUST set every property the
final-rendered control needs, because an implicit style without BasedOn
fully replaces the framework default style — including the default
ControlTemplate. The ToggleSwitch entry below sets a complete custom
template suitable for cards. We intentionally do NOT define implicit
Slider / ComboBox / TextBox styles here: stripping their default template
by way of a minimal implicit style would lose theme-driven CornerRadius,
PointerOver/Pressed brushes, the drop-shadow popup, etc. Call sites that
need a card-specific MinWidth set it explicitly on the instance.
-->
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!--
Right-aligned, compact ToggleSwitch. Replaces the WCT
RightAlignedCompactToggleSwitchStyle the toolkit uses as BasedOn.
OffContent / OnContent are blanked so the default "Off" / "On" labels
don't steal horizontal space next to the switch.
-->
<Style TargetType="ToggleSwitch">
<Setter Property="HorizontalAlignment" Value="Right" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="HorizontalContentAlignment" Value="Right" />
<Setter Property="Margin" Value="0,0,10,0" />
<Setter Property="MinWidth" Value="0" />
<Setter Property="OffContent" Value="" />
<Setter Property="OnContent" Value="" />
</Style>
</ResourceDictionary>

File diff suppressed because it is too large Load Diff

View File

@@ -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<IInspectable>());
}
void SettingsExpander::_InitializeProperties()
{
if (!_HeaderProperty)
{
_HeaderProperty = DependencyProperty::Register(
L"Header",
xaml_typename<IInspectable>(),
xaml_typename<Editor::SettingsExpander>(),
PropertyMetadata{ nullptr });
}
if (!_DescriptionProperty)
{
_DescriptionProperty = DependencyProperty::Register(
L"Description",
xaml_typename<IInspectable>(),
xaml_typename<Editor::SettingsExpander>(),
PropertyMetadata{ nullptr });
}
if (!_HeaderIconProperty)
{
_HeaderIconProperty = DependencyProperty::Register(
L"HeaderIcon",
xaml_typename<IconElement>(),
xaml_typename<Editor::SettingsExpander>(),
PropertyMetadata{ nullptr });
}
if (!_ContentProperty)
{
_ContentProperty = DependencyProperty::Register(
L"Content",
xaml_typename<IInspectable>(),
xaml_typename<Editor::SettingsExpander>(),
PropertyMetadata{ nullptr });
}
if (!_IsExpandedProperty)
{
_IsExpandedProperty = DependencyProperty::Register(
L"IsExpanded",
xaml_typename<bool>(),
xaml_typename<Editor::SettingsExpander>(),
PropertyMetadata{ box_value(false), PropertyChangedCallback{ &SettingsExpander::_OnIsExpandedChanged } });
}
if (!_ItemsHeaderProperty)
{
_ItemsHeaderProperty = DependencyProperty::Register(
L"ItemsHeader",
xaml_typename<UIElement>(),
xaml_typename<Editor::SettingsExpander>(),
PropertyMetadata{ nullptr });
}
if (!_ItemsFooterProperty)
{
_ItemsFooterProperty = DependencyProperty::Register(
L"ItemsFooter",
xaml_typename<UIElement>(),
xaml_typename<Editor::SettingsExpander>(),
PropertyMetadata{ nullptr });
}
if (!_ItemsProperty)
{
_ItemsProperty = DependencyProperty::Register(
L"Items",
xaml_typename<IVector<IInspectable>>(),
xaml_typename<Editor::SettingsExpander>(),
PropertyMetadata{ nullptr, PropertyChangedCallback{ &SettingsExpander::_OnItemsConnectedPropertyChanged } });
}
if (!_ItemsSourceProperty)
{
_ItemsSourceProperty = DependencyProperty::Register(
L"ItemsSource",
xaml_typename<IInspectable>(),
xaml_typename<Editor::SettingsExpander>(),
PropertyMetadata{ nullptr, PropertyChangedCallback{ &SettingsExpander::_OnItemsConnectedPropertyChanged } });
}
if (!_ItemTemplateProperty)
{
_ItemTemplateProperty = DependencyProperty::Register(
L"ItemTemplate",
xaml_typename<IInspectable>(),
xaml_typename<Editor::SettingsExpander>(),
PropertyMetadata{ nullptr });
}
if (!_ItemContainerStyleSelectorProperty)
{
_ItemContainerStyleSelectorProperty = DependencyProperty::Register(
L"ItemContainerStyleSelector",
xaml_typename<StyleSelector>(),
xaml_typename<Editor::SettingsExpander>(),
PropertyMetadata{ nullptr });
}
}
AutomationPeer SettingsExpander::OnCreateAutomationPeer()
{
return winrt::make<implementation::SettingsExpanderAutomationPeer>(*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<Controls::ItemsControl>();
}
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<hstring>(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<IObservableVector<IInspectable>>() })
{
_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<FrameworkElement>() })
{
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<Editor::SettingsExpander>() })
{
get_self<SettingsExpander>(obj)->_UpdateItemsSource();
}
}
void SettingsExpander::_OnIsExpandedChanged(const DependencyObject& d, const DependencyPropertyChangedEventArgs& e)
{
const auto obj{ d.try_as<Editor::SettingsExpander>() };
if (!obj)
{
return;
}
const auto self = get_self<SettingsExpander>(obj);
const auto newValue = unbox_value_or<bool>(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<Editor::SettingsExpanderAutomationPeer>() })
{
peer.RaiseExpandedChangedEvent(newValue);
}
if (newValue)
{
self->Expanded.raise(obj, nullptr);
}
else
{
self->Collapsed.raise(obj, nullptr);
}
}
SettingsExpanderAutomationPeer::SettingsExpanderAutomationPeer(const Editor::SettingsExpander& owner) :
SettingsExpanderAutomationPeerT<SettingsExpanderAutomationPeer>(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<Editor::SettingsExpander>() })
{
if (const auto manualName{ AutomationProperties::GetName(expander) }; !manualName.empty())
{
return manualName;
}
if (const auto headerString{ unbox_value_or<hstring>(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<Editor::SettingsCard>() })
{
if (card.IsClickEnabled())
{
return _ClickableStyle;
}
}
return _DefaultStyle;
}
}

View File

@@ -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<SettingsExpander>
{
public:
SettingsExpander();
void OnApplyTemplate();
Windows::UI::Xaml::Automation::Peers::AutomationPeer OnCreateAutomationPeer();
til::typed_event<Editor::SettingsExpander, Windows::Foundation::IInspectable> Expanded;
til::typed_event<Editor::SettingsExpander, Windows::Foundation::IInspectable> 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<Windows::Foundation::IInspectable>, 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<Windows::Foundation::IInspectable>::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<SettingsExpanderAutomationPeer>
{
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>
{
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);
}

View File

@@ -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<Object> 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<SettingsExpander, Object> Expanded;
event Windows.Foundation.TypedEventHandler<SettingsExpander, Object> 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;
};
}

View File

@@ -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<IPropertyValue>() }; pv && pv.Type() == PropertyType::String)
{
return _StringTemplate;
}
return nullptr;
}
}

View File

@@ -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>
{
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);
}

View File

@@ -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;
}
}

View File

@@ -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<ResourceDictionary>(),
xaml_typename<Editor::StyleExtensions>(),
PropertyMetadata{ nullptr, PropertyChangedCallback{ &StyleExtensions::_OnResourcesChanged } });
}
}
ResourceDictionary StyleExtensions::GetResources(const DependencyObject& target)
{
return target.GetValue(ResourcesProperty()).try_as<ResourceDictionary>();
}
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<FrameworkElement>() };
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<ResourceDictionary>() })
{
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<ResourceDictionary>() })
{
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();
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -57,7 +57,6 @@ static constexpr std::string_view SuggestionsKey{ "showSuggestions" };
static constexpr std::string_view ToggleFocusModeKey{ "toggleFocusMode" };
static constexpr std::string_view SetFocusModeKey{ "setFocusMode" };
static constexpr std::string_view ToggleFullscreenKey{ "toggleFullscreen" };
static constexpr std::string_view ToggleOverviewKey{ "toggleOverview" };
static constexpr std::string_view SetFullScreenKey{ "setFullScreen" };
static constexpr std::string_view SetMaximizedKey{ "setMaximized" };
static constexpr std::string_view TogglePaneZoomKey{ "togglePaneZoom" };

View File

@@ -59,7 +59,6 @@
ON_ALL_ACTIONS(ToggleShaderEffects) \
ON_ALL_ACTIONS(ToggleFocusMode) \
ON_ALL_ACTIONS(ToggleFullscreen) \
ON_ALL_ACTIONS(ToggleOverview) \
ON_ALL_ACTIONS(ToggleAlwaysOnTop) \
ON_ALL_ACTIONS(OpenSettings) \
ON_ALL_ACTIONS(SetFocusMode) \

View File

@@ -466,9 +466,6 @@
<value>Toggle focus mode</value>
<comment>"Focus mode" is a mode with minimal UI elements, for a distraction-free experience</comment>
</data>
<data name="ToggleOverviewCommandKey" xml:space="preserve">
<value>Toggle overview mode</value>
</data>
<data name="EnableFocusModeCommandKey" xml:space="preserve">
<value>Enable focus mode</value>
</data>

View File

@@ -517,7 +517,6 @@
{ "command": "closeWindow", "id": "Terminal.CloseWindow" },
{ "command": "toggleFullscreen", "id": "Terminal.ToggleFullscreen" },
{ "command": "toggleFocusMode", "id": "Terminal.ToggleFocusMode" },
{ "command": "toggleOverview", "id": "Terminal.ToggleOverview" },
{ "command": "toggleAlwaysOnTop", "id": "Terminal.ToggleAlwaysOnTop" },
{ "command": "openNewTabDropdown", "id": "Terminal.OpenNewTabDropdown" },
{ "command": { "action": "openSettings", "target": "settingsUI" }, "id": "Terminal.OpenSettingsUI" },

View File

@@ -123,6 +123,7 @@ class ScreenBufferTests
TEST_METHOD(VtResizePreservingAttributes);
TEST_METHOD(VtSoftResetCursorPosition);
TEST_METHOD(VtSoftResetAltBufferCursorState);
TEST_METHOD(VtScrollMarginsNewlineColor);
@@ -1510,6 +1511,30 @@ void ScreenBufferTests::VtSoftResetCursorPosition()
VERIFY_ARE_EQUAL(til::point(1, 1), cursor.GetPosition());
}
void ScreenBufferTests::VtSoftResetAltBufferCursorState()
{
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
gci.LockConsole(); // Lock must be taken to manipulate buffer.
auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); });
auto& si = gci.GetActiveOutputBuffer();
auto& stateMachine = si.GetStateMachine();
Log::Comment(L"Move cursor on the main buffer.");
stateMachine.ProcessString(L"\x1b[4;7H");
VERIFY_ARE_EQUAL(til::point(6, 3), si.GetTextBuffer().GetCursor().GetPosition());
Log::Comment(L"Enter alt buffer, soft reset, and return to main buffer.");
stateMachine.ProcessString(L"\x1b[?1049h");
VERIFY_IS_TRUE(gci.GetActiveOutputBuffer()._IsAltBuffer());
stateMachine.ProcessString(L"\x1b[!p");
stateMachine.ProcessString(L"\x1b[?1049l");
VERIFY_IS_FALSE(gci.GetActiveOutputBuffer()._IsAltBuffer());
Log::Comment(L"Returning from alt buffer should restore the main cursor position.");
VERIFY_ARE_EQUAL(til::point(6, 3), gci.GetActiveOutputBuffer().GetTextBuffer().GetCursor().GetPosition());
}
void ScreenBufferTests::VtScrollMarginsNewlineColor()
{
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();

View File

@@ -3002,17 +3002,15 @@ void AdaptDispatch::SoftReset()
SetGraphicsRendition({}); // Normal rendition.
SetCharacterProtectionAttribute({}); // Default (unprotected)
// Reset the saved cursor state.
// Note that XTerm only resets the main buffer state, but that
// seems likely to be a bug. Most other terminals reset both.
_savedCursorState.at(0) = {}; // Main buffer
_savedCursorState.at(1) = {}; // Alt buffer
// Reset only the active saved cursor state.
// This matches xterm behavior when DECSTR is processed while using
// the alternate screen buffer (GH#19918).
_savedCursorState.at(_usingAltBuffer ? 1 : 0) = {};
// The TerminalOutput state in these buffers must be reset to
// The TerminalOutput state in this buffer must be reset to
// the same state as the _termOutput instance, which is not
// necessarily equivalent to a full reset.
_savedCursorState.at(0).TermOutput = _termOutput;
_savedCursorState.at(1).TermOutput = _termOutput;
_savedCursorState.at(_usingAltBuffer ? 1 : 0).TermOutput = _termOutput;
// Soft reset the Sixel parser if in use.
if (_sixelParser)