mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-02-23 19:49:43 +01:00
Adding inline docs
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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 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+(?<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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user