Adding inline docs

This commit is contained in:
Niels Laute
2025-09-18 15:58:17 +02:00
parent fef6921d9b
commit 70c4016860
3 changed files with 609 additions and 28 deletions

View File

@@ -2,16 +2,48 @@
x:Class="Microsoft.PowerToys.Settings.UI.Controls.SettingsPageControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:animations="using:CommunityToolkit.WinUI.Animations"
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:converters="using:Microsoft.PowerToys.Settings.UI.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:tkconverters="using:CommunityToolkit.WinUI.Converters"
xmlns:toolkit="using:CommunityToolkit.WinUI.Controls"
Loaded="UserControl_Loaded"
mc:Ignorable="d">
<UserControl.Resources>
<x:Double x:Key="PageMaxWidth">1000</x:Double>
<x:Double x:Key="PageHeaderMaxWidth">1020</x:Double>
<tkconverters:DoubleToVisibilityConverter
x:Name="doubleToVisibilityConverter"
FalseValue="Collapsed"
GreaterThan="0"
TrueValue="Visible" />
<animations:ImplicitAnimationSet x:Name="ShowTransitions">
<animations:OffsetAnimation
EasingMode="EaseOut"
From="0,24,0"
To="0"
Duration="0:0:0.4" />
<animations:OpacityAnimation
EasingMode="EaseOut"
From="0"
To="1"
Duration="0:0:0.2" />
</animations:ImplicitAnimationSet>
<animations:ImplicitAnimationSet x:Name="HideTransitions">
<animations:OffsetAnimation
EasingMode="EaseOut"
From="0"
To="0,24,0"
Duration="0:0:0.2" />
<animations:OpacityAnimation
EasingMode="EaseOut"
From="1"
To="0"
Duration="0:0:0.1" />
</animations:ImplicitAnimationSet>
<converters:UriToImageSourceConverter x:Key="UriToImageSourceConverter" />
</UserControl.Resources>
<Grid Padding="20,0,0,0" RowSpacing="24">
@@ -33,14 +65,19 @@
Padding="0,0,20,48"
ChildrenTransitions="{StaticResource SettingsCardsAnimations}"
RowSpacing="24">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Top panel -->
<Grid MaxWidth="{StaticResource PageMaxWidth}" RowSpacing="16">
<Grid
MaxWidth="{StaticResource PageMaxWidth}"
ColumnSpacing="16"
RowSpacing="16">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
@@ -48,6 +85,7 @@
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Border
MaxWidth="160"
@@ -65,13 +103,13 @@
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{x:Bind ModuleDescription}"
TextWrapping="Wrap" />
<ToggleSwitch IsOn="True" />
<ItemsControl
x:Name="PrimaryLinksControl"
Margin="0,8,0,0"
IsTabStop="False"
ItemsSource="{x:Bind PrimaryLinks}"
Visibility="{x:Bind PrimaryLinks.Count, Converter={StaticResource DoubleToVisibilityConverter}}">
Visibility="Collapsed">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="controls:PageLink">
<HyperlinkButton NavigateUri="{x:Bind Link}" Style="{StaticResource TextButtonStyle}">
@@ -88,18 +126,74 @@
</StackPanel>
</Grid>
<!-- Content panel -->
<ContentPresenter
x:Name="ModuleContentPresenter"
<Grid
Grid.Row="1"
MaxWidth="{StaticResource PageMaxWidth}"
Margin="0,12,0,0"
Content="{x:Bind ModuleContent}" />
Margin="0,-48,0,0">
<SelectorBar x:Name="PivotBar">
<SelectorBarItem
IsSelected="True"
Tag="Settings"
Text="Settings" />
<SelectorBarItem Tag="Docs" Text="Documentation" />
</SelectorBar>
</Grid>
<toolkit:SwitchPresenter
Grid.Row="2"
MaxWidth="{StaticResource PageMaxWidth}"
Value="{Binding SelectedItem.Tag, ElementName=PivotBar}">
<toolkit:Case Value="Settings">
<!-- Content panel -->
<ContentPresenter
x:Name="ModuleContentPresenter"
Margin="0,12,0,0"
Content="{x:Bind ModuleContent}" />
</toolkit:Case>
<toolkit:Case Value="Docs">
<Grid
animations:Implicit.HideAnimations="{StaticResource HideTransitions}"
animations:Implicit.ShowAnimations="{StaticResource ShowTransitions}"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="{StaticResource OverlayCornerRadius}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ListView
x:Name="TocList"
Grid.Row="1"
Grid.Column="1"
IsItemClickEnabled="True"
ItemClick="TocList_ItemClick"
SelectionMode="None">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock
Margin="{Binding Indent}"
Tag="{Binding Id}"
Text="{Binding Title}"
TextWrapping="WrapWholeWords" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<ScrollViewer
x:Name="DocScroll"
VerticalScrollBarVisibility="Auto"
VerticalScrollMode="Auto">
<StackPanel x:Name="DocHost" Orientation="Vertical" />
</ScrollViewer>
</Grid>
</toolkit:Case>
</toolkit:SwitchPresenter>
<!-- Bottom panel -->
<StackPanel
x:Name="SecondaryLinksPanel"
Grid.Row="2"
Grid.Row="3"
MaxWidth="{StaticResource PageMaxWidth}"
AutomationProperties.Name="{x:Bind SecondaryLinksHeader}"
Orientation="Vertical"
@@ -142,9 +236,9 @@
<AdaptiveTrigger MinWindowWidth="0" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="DescriptionPanel.(Grid.Row)" Value="1" />
<!--<Setter Target="DescriptionPanel.(Grid.Row)" Value="1" />
<Setter Target="DescriptionPanel.(Grid.Column)" Value="0" />
<Setter Target="DescriptionPanel.(Grid.ColumnSpan)" Value="2" />
<Setter Target="DescriptionPanel.(Grid.ColumnSpan)" Value="2" />-->
</VisualState.Setters>
</VisualState>
</VisualStateGroup>

View File

@@ -3,26 +3,56 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using CommunityToolkit.WinUI.Controls;
using Markdig;
using Markdig.Syntax;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Media;
using Windows.Foundation;
using Windows.System;
namespace Microsoft.PowerToys.Settings.UI.Controls
{
public sealed partial class SettingsPageControl : UserControl
{
private readonly Dictionary<string, FrameworkElement> _anchors = new();
private readonly MarkdownPipeline _pipeline = new MarkdownPipelineBuilder().UsePreciseSourceLocation().Build();
// For section flyouts
private string _fullMarkdown = string.Empty;
private sealed class HeadingInfo
{
#pragma warning disable SA1401 // Fields should be private
public string Id = string.Empty;
public string Title = string.Empty;
public int Level;
public int Start;
public int End;
#pragma warning restore SA1401 // Fields should be private
}
private readonly List<HeadingInfo> _allHeadings = new();
public SettingsPageControl()
{
this.InitializeComponent();
InitializeComponent();
PrimaryLinks = new ObservableCollection<PageLink>();
SecondaryLinks = new ObservableCollection<PageLink>();
}
public string ModuleTitle
{
get { return (string)GetValue(ModuleTitleProperty); }
set { SetValue(ModuleTitleProperty, value); }
get => (string)GetValue(ModuleTitleProperty);
set => SetValue(ModuleTitleProperty, value);
}
public string ModuleDescription
@@ -45,8 +75,8 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
public string SecondaryLinksHeader
{
get { return (string)GetValue(SecondaryLinksHeaderProperty); }
set { SetValue(SecondaryLinksHeaderProperty, value); }
get => (string)GetValue(SecondaryLinksHeaderProperty);
set => SetValue(SecondaryLinksHeaderProperty, value);
}
public ObservableCollection<PageLink> SecondaryLinks
@@ -57,21 +87,471 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
public object ModuleContent
{
get { return (object)GetValue(ModuleContentProperty); }
set { SetValue(ModuleContentProperty, value); }
get => GetValue(ModuleContentProperty);
set => SetValue(ModuleContentProperty, value);
}
public static readonly DependencyProperty ModuleTitleProperty = DependencyProperty.Register(nameof(ModuleTitle), typeof(string), typeof(SettingsPageControl), new PropertyMetadata(defaultValue: null));
public static readonly DependencyProperty ModuleDescriptionProperty = DependencyProperty.Register(nameof(ModuleDescription), typeof(string), typeof(SettingsPageControl), new PropertyMetadata(defaultValue: null));
public static readonly DependencyProperty ModuleImageSourceProperty = DependencyProperty.Register(nameof(ModuleImageSource), typeof(Uri), typeof(SettingsPageControl), new PropertyMetadata(null));
public static readonly DependencyProperty PrimaryLinksProperty = DependencyProperty.Register(nameof(PrimaryLinks), typeof(ObservableCollection<PageLink>), typeof(SettingsPageControl), new PropertyMetadata(new ObservableCollection<PageLink>()));
public static readonly DependencyProperty SecondaryLinksHeaderProperty = DependencyProperty.Register(nameof(SecondaryLinksHeader), typeof(string), typeof(SettingsPageControl), new PropertyMetadata(default(string)));
public static readonly DependencyProperty SecondaryLinksProperty = DependencyProperty.Register(nameof(SecondaryLinks), typeof(ObservableCollection<PageLink>), typeof(SettingsPageControl), new PropertyMetadata(new ObservableCollection<PageLink>()));
public static readonly DependencyProperty ModuleContentProperty = DependencyProperty.Register(nameof(ModuleContent), typeof(object), typeof(SettingsPageControl), new PropertyMetadata(new Grid()));
public static readonly DependencyProperty ModuleTitleProperty =
DependencyProperty.Register(nameof(ModuleTitle), typeof(string), typeof(SettingsPageControl), new PropertyMetadata(default(string)));
public static readonly DependencyProperty ModuleDescriptionProperty =
DependencyProperty.Register(nameof(ModuleDescription), typeof(string), typeof(SettingsPageControl), new PropertyMetadata(default(string)));
public static readonly DependencyProperty ModuleImageSourceProperty =
DependencyProperty.Register(nameof(ModuleImageSource), typeof(Uri), typeof(SettingsPageControl), new PropertyMetadata(null));
public static readonly DependencyProperty PrimaryLinksProperty =
DependencyProperty.Register(nameof(PrimaryLinks), typeof(ObservableCollection<PageLink>), typeof(SettingsPageControl), new PropertyMetadata(new ObservableCollection<PageLink>()));
public static readonly DependencyProperty SecondaryLinksHeaderProperty =
DependencyProperty.Register(nameof(SecondaryLinksHeader), typeof(string), typeof(SettingsPageControl), new PropertyMetadata(default(string)));
public static readonly DependencyProperty SecondaryLinksProperty =
DependencyProperty.Register(nameof(SecondaryLinks), typeof(ObservableCollection<PageLink>), typeof(SettingsPageControl), new PropertyMetadata(new ObservableCollection<PageLink>()));
public static readonly DependencyProperty ModuleContentProperty =
DependencyProperty.Register(nameof(ModuleContent), typeof(object), typeof(SettingsPageControl), new PropertyMetadata(new Grid()));
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
PrimaryLinksControl.Focus(FocusState.Programmatic);
_ = LoadAndRenderAsync("https://raw.githubusercontent.com/MicrosoftDocs/windows-dev-docs/refs/heads/docs/hub/powertoys/advanced-paste.md");
}
private sealed class TocItem
{
public string Id { get; init; } = string.Empty;
public string Title { get; init; } = string.Empty;
public int Level { get; init; }
}
private async Task LoadAndRenderAsync(string requestUrl)
{
using var client = new HttpClient();
var raw = await client.GetStringAsync(requestUrl);
// Preprocess with knowledge of the file URL (so we resolve ../images/...)
var md = PreprocessMarkdown(raw, requestUrl);
var tocItems = BuildDocumentAndAnchors(md);
// Bind ToC (indent H2/H3 a bit)
TocList.ItemsSource = tocItems.Select(i => new
{
i.Id,
i.Title,
Indent = new Thickness((i.Level - 1) * 12, 6, 8, 6),
}).ToList();
}
private List<TocItem> BuildDocumentAndAnchors(string md)
{
_fullMarkdown = md;
DocHost.Children.Clear();
_anchors.Clear();
_allHeadings.Clear();
var doc = Markdig.Markdown.Parse(md, _pipeline);
// Build slugs for ALL headings (H1..H6) so section flyouts can target any level
var rawHeadings = doc.Descendants<HeadingBlock>().ToList();
var seen = new Dictionary<string, int>();
foreach (var hb in rawHeadings)
{
string title = hb.Inline?.FirstChild?.ToString() ?? "Section";
string id = MakeSlug(title);
if (seen.TryGetValue(id, out int n))
{
n++;
seen[id] = n;
id = $"{id}-{n}";
}
else
{
seen[id] = 1;
}
_allHeadings.Add(new HeadingInfo
{
Id = id,
Title = title,
Level = hb.Level,
Start = hb.Span.Start,
End = md.Length, // fixed below
});
}
// Compute section End = next heading with level <= current level (or EOF)
for (int i = 0; i < _allHeadings.Count; i++)
{
for (int j = i + 1; j < _allHeadings.Count; j++)
{
if (_allHeadings[j].Level <= _allHeadings[i].Level)
{
_allHeadings[i].End = _allHeadings[j].Start;
break;
}
}
}
// Render the document in H2/H3 chunks for this UI
var headings = _allHeadings.Where(h => h.Level is 2 or 3).ToList();
var toc = new List<TocItem>();
if (headings.Count == 0)
{
DocHost.Children.Add(new MarkdownTextBlock { Text = md });
return toc;
}
foreach (var h in headings)
{
toc.Add(new TocItem { Id = h.Id, Title = h.Title, Level = h.Level });
// Invisible anchor just before the rendered section
var anchor = new Border { Height = 0, Opacity = 0, Tag = h.Id };
DocHost.Children.Add(anchor);
_anchors[h.Id] = anchor;
// Render this sections markdown (include heading line)
string sectionMd = md.Substring(h.Start, h.End - h.Start);
var mdtb = new MarkdownTextBlock { Text = sectionMd };
// NOTE: some toolkit versions use LinkClicked; you used OnLinkClicked in your snippet.
// Keep your version:
mdtb.OnLinkClicked += Markdown_LinkClicked;
DocHost.Children.Add(mdtb);
}
return toc;
}
private void Markdown_LinkClicked(object sender, CommunityToolkit.WinUI.Controls.LinkClickedEventArgs e)
{
var uri = e.Uri;
if (uri is null)
{
return;
}
string anchorId = null;
#pragma warning disable CA1310 // Specify StringComparison for correctness
#pragma warning disable CA1866 // Use char overload
if (!uri.IsAbsoluteUri && uri.OriginalString.StartsWith("#"))
{
anchorId = uri.OriginalString.TrimStart('#');
}
else if (uri.IsAbsoluteUri && !string.IsNullOrEmpty(uri.Fragment))
{
anchorId = uri.Fragment.TrimStart('#');
}
#pragma warning restore CA1866 // Use char overload
#pragma warning restore CA1310 // Specify StringComparison for correctness
if (!string.IsNullOrEmpty(anchorId) && _anchors.TryGetValue(anchorId, out _))
{
ScrollToAnchor(anchorId, TopOffset);
return;
}
_ = Launcher.LaunchUriAsync(uri);
}
private const double TopOffset = 40; // pixels
// Click in ToC -> scroll to anchor
private void TocList_ItemClick(object sender, ItemClickEventArgs e)
{
if (e.ClickedItem is FrameworkElement fe && fe.Tag is string id)
{
ScrollToAnchor(id, TopOffset);
return;
}
var idProp = e.ClickedItem?.GetType().GetProperty("Id")?.GetValue(e.ClickedItem) as string;
if (!string.IsNullOrEmpty(idProp))
{
ScrollToAnchor(idProp!, TopOffset);
}
}
private void ScrollToAnchor(string id, double topOffset = 0)
{
if (_anchors.TryGetValue(id, out var target))
{
var opts = new BringIntoViewOptions
{
VerticalAlignmentRatio = 0.0,
HorizontalAlignmentRatio = 0.0,
AnimationDesired = true,
};
target.StartBringIntoView(opts);
}
}
// ----------------------------
// Public API: show a section in a flyout (for any H1..H6)
// ----------------------------
public bool TryShowSectionFlyout(FrameworkElement placementTarget, string sectionId, bool includeHeading = false, FlyoutPlacementMode placement = FlyoutPlacementMode.Bottom)
{
if (placementTarget is null || string.IsNullOrWhiteSpace(sectionId))
{
return false;
}
var h = _allHeadings.FirstOrDefault(x => string.Equals(x.Id, sectionId, StringComparison.OrdinalIgnoreCase));
if (h is null)
{
return false;
}
var slice = _fullMarkdown.Substring(h.Start, h.End - h.Start);
if (!includeHeading)
{
int nl = slice.IndexOf('\n');
slice = nl >= 0 ? slice[(nl + 1)..] : string.Empty;
}
if (string.IsNullOrWhiteSpace(slice))
{
return false;
}
var title = new TextBlock
{
Text = h.Title,
TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(0, 0, 0, 8),
Style = (Style)Application.Current.Resources["SubtitleTextBlockStyle"],
};
var body = new MarkdownTextBlock { Text = slice };
body.OnLinkClicked += Markdown_LinkClicked;
var content = new StackPanel { MinWidth = 320, MaxWidth = 560 };
content.Children.Add(title);
content.Children.Add(new ScrollViewer
{
Content = body,
MaxHeight = 420,
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
});
var flyout = new Microsoft.UI.Xaml.Controls.Flyout { Content = content, Placement = placement };
flyout.ShowAt(placementTarget);
return true;
}
// ----------------------------
// Markdown preprocessor (MS Learn → standard Markdown)
// ----------------------------
public static string PreprocessMarkdown(string markdown, string sourceFileUrl)
{
// Compute the *directory* of the md file (guaranteed trailing slash)
var baseDir = new Uri(new Uri(sourceFileUrl), ".");
string Resolve(string url)
{
if (string.IsNullOrWhiteSpace(url))
{
return url;
}
if (Uri.IsWellFormedUriString(url, UriKind.Absolute))
{
return url;
}
return new Uri(baseDir, url).ToString();
}
// 1) Strip YAML front matter at the very top
markdown = Regex.Replace(
markdown,
pattern: @"\A---\s*[\s\S]*?^\s*---\s*$\r?\n?",
replacement: string.Empty,
options: RegexOptions.Multiline);
// 2) Remove specific Learn notice (example you had)
markdown = Regex.Replace(
markdown,
@"^>\s*\[!IMPORTANT\]\s*> - Phi Silica is not available in China\.\s*$\r?\n?",
string.Empty,
RegexOptions.Multiline | RegexOptions.IgnoreCase);
// 3) Convert Learn admonitions to simpler blockquotes with icons
var admonitions = new (string Pattern, string Replacement)[]
{
(@"^>\s*\[!IMPORTANT\]", "> ** Important:**"),
(@"^>\s*\[!NOTE\]", "> **❗ Note:**"),
(@"^>\s*\[!TIP\]", "> **💡 Tip:**"),
(@"^>\s*\[!WARNING\]", "> **⚠️ Warning:**"),
(@"^>\s*\[!CAUTION\]", "> **⚠️ Caution:**"),
};
foreach (var (pat, rep) in admonitions)
markdown = Regex.Replace(markdown, pat, rep, RegexOptions.Multiline | RegexOptions.IgnoreCase);
// 4) Convert :::image ... ::: blocks
markdown = Regex.Replace(
markdown,
@":::image\s+(?<attrs>.*?):::",
m =>
{
string attrs = m.Groups["attrs"].Value;
static string A(string attrs, string name)
{
var mm = Regex.Match(attrs, $@"\b{name}\s*=\s*""([^""]*)""", RegexOptions.IgnoreCase);
return mm.Success ? mm.Groups[1].Value : string.Empty;
}
string src = A(attrs, "source");
string alt = A(attrs, "alt-text");
string lightbox = A(attrs, "lightbox");
string link = A(attrs, "link");
src = Resolve(src);
lightbox = Resolve(lightbox);
link = Resolve(link);
var img = $"![{alt}]";
if (!string.IsNullOrWhiteSpace(src))
{
img += $"({src})";
}
var href = !string.IsNullOrWhiteSpace(link) ? link : lightbox;
if (!string.IsNullOrWhiteSpace(href))
{
img = $"[{img}]({href})";
}
return img + "\n";
},
RegexOptions.Singleline | RegexOptions.IgnoreCase);
// 5) Resolve relative links in standard markdown
markdown = Regex.Replace(
markdown,
@"\]\((?!https?://|mailto:|data:|#)(?<rel>[^)]+)\)",
m =>
{
var rel = m.Groups["rel"].Value.Trim();
var abs = Resolve(rel);
return $"]({abs})";
});
return markdown;
}
private static string MakeSlug(string s)
{
if (string.IsNullOrWhiteSpace(s))
{
return "section";
}
var slug = s.Trim().ToLowerInvariant();
slug = Regex.Replace(slug, @"[^\p{L}\p{Nd}\s-]", string.Empty);
slug = Regex.Replace(slug, @"\s+", "-");
slug = Regex.Replace(slug, @"-+", "-");
return slug;
}
}
// ------------------------------------------------------------
// Attached property: set DocSectionFlyout.SectionId on any Button
// inside ModuleContent, and it will open a flyout for that section.
// ------------------------------------------------------------
#pragma warning disable SA1402 // File may only contain a single type
public static class DocSectionFlyout
#pragma warning restore SA1402 // File may only contain a single type
{
public static readonly DependencyProperty SectionIdProperty =
DependencyProperty.RegisterAttached(
"SectionId",
typeof(string),
typeof(DocSectionFlyout),
new PropertyMetadata(null, OnSectionIdChanged));
public static void SetSectionId(DependencyObject obj, string value) => obj.SetValue(SectionIdProperty, value);
public static string GetSectionId(DependencyObject obj) => (string)obj.GetValue(SectionIdProperty);
public static readonly DependencyProperty IncludeHeadingProperty =
DependencyProperty.RegisterAttached(
"IncludeHeading",
typeof(bool),
typeof(DocSectionFlyout),
new PropertyMetadata(false));
public static void SetIncludeHeading(DependencyObject obj, bool value) => obj.SetValue(IncludeHeadingProperty, value);
public static bool GetIncludeHeading(DependencyObject obj) => (bool)obj.GetValue(IncludeHeadingProperty);
public static readonly DependencyProperty PlacementProperty =
DependencyProperty.RegisterAttached(
"Placement",
typeof(FlyoutPlacementMode),
typeof(DocSectionFlyout),
new PropertyMetadata(FlyoutPlacementMode.Bottom));
public static void SetPlacement(DependencyObject obj, FlyoutPlacementMode value) => obj.SetValue(PlacementProperty, value);
public static FlyoutPlacementMode GetPlacement(DependencyObject obj) => (FlyoutPlacementMode)obj.GetValue(PlacementProperty);
private static void OnSectionIdChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is ButtonBase btn)
{
btn.Click -= Button_Click;
if (e.NewValue is string { Length: > 0 })
{
btn.Click += Button_Click;
}
}
}
private static void Button_Click(object sender, RoutedEventArgs e)
{
if (sender is not FrameworkElement fe)
{
return;
}
// Find nearest SettingsPageControl ancestor
var parent = fe as DependencyObject;
SettingsPageControl host = null;
while (parent is not null)
{
parent = VisualTreeHelper.GetParent(parent);
if (parent is SettingsPageControl spc)
{
host = spc;
break;
}
}
if (host is null)
{
return;
}
var id = GetSectionId(fe);
if (string.IsNullOrWhiteSpace(id))
{
return;
}
var includeHeading = GetIncludeHeading(fe);
var placement = GetPlacement(fe);
host.TryShowSectionFlyout(fe, id, includeHeading, placement);
}
}
}

View File

@@ -46,6 +46,9 @@
ChildrenTransitions="{StaticResource SettingsCardsAnimations}"
Orientation="Vertical"
Spacing="2">
<StackPanel Orientation="Horizontal" Spacing="8">
<!-- Include the heading line in the flyout and place it at the right -->
</StackPanel>
<controls:GPOInfoControl ShowWarning="{x:Bind ViewModel.IsEnabledGpoConfigured, Mode=OneWay}">
<tkcontrols:SettingsCard
Name="AdvancedPasteEnableToggleControlHeaderText"
@@ -86,7 +89,11 @@
<tkcontrols:SettingsCard.Description>
<StackPanel Orientation="Vertical">
<TextBlock x:Uid="AdvancedPaste_EnableAISettingsCardDescription" />
<HyperlinkButton x:Uid="AdvancedPaste_EnableAISettingsCardDescriptionLearnMore" NavigateUri="https://learn.microsoft.com/windows/powertoys/advanced-paste" />
<HyperlinkButton
x:Uid="AdvancedPaste_EnableAISettingsCardDescriptionLearnMore"
controls:DocSectionFlyout.IncludeHeading="True"
controls:DocSectionFlyout.Placement="Right"
controls:DocSectionFlyout.SectionId="paste-text-with-ai" />
</StackPanel>
</tkcontrols:SettingsCard.Description>
</tkcontrols:SettingsCard>