diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/SettingsPageControl.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/SettingsPageControl.xaml
index 7e47cf6a97..b842f06191 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/SettingsPageControl.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/SettingsPageControl.xaml
@@ -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">
1000
1020
+
+
+
+
+
+
+
+
+
@@ -33,14 +65,19 @@
Padding="0,0,20,48"
ChildrenTransitions="{StaticResource SettingsCardsAnimations}"
RowSpacing="24">
+
+
-
+
@@ -48,6 +85,7 @@
+
-
+
+ Visibility="Collapsed">
@@ -88,18 +126,74 @@
-
-
+ Margin="0,-48,0,0">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/SettingsPageControl.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/SettingsPageControl.xaml.cs
index 6e5fef091f..dd470c6ab8 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/SettingsPageControl.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/SettingsPageControl.xaml.cs
@@ -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 _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 _allHeadings = new();
+
public SettingsPageControl()
{
- this.InitializeComponent();
+ InitializeComponent();
PrimaryLinks = new ObservableCollection();
SecondaryLinks = new ObservableCollection();
}
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 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), typeof(SettingsPageControl), new PropertyMetadata(new ObservableCollection()));
- 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), typeof(SettingsPageControl), new PropertyMetadata(new ObservableCollection()));
- 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), typeof(SettingsPageControl), new PropertyMetadata(new ObservableCollection()));
+
+ 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), typeof(SettingsPageControl), new PropertyMetadata(new ObservableCollection()));
+
+ 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 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().ToList();
+ var seen = new Dictionary();
+
+ 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();
+
+ 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 section’s 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+(?.*?):::",
+ 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:|#)(?[^)]+)\)",
+ 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);
}
}
}
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml
index 628df84c01..2f53bf522a 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml
@@ -46,6 +46,9 @@
ChildrenTransitions="{StaticResource SettingsCardsAnimations}"
Orientation="Vertical"
Spacing="2">
+
+
+
-
+