mirror of
https://github.com/sourcegit-scm/sourcegit
synced 2025-05-22 12:45:00 +00:00
feature: subject presenter supports inline codeblock
Signed-off-by: leo <longshuang@msn.cn>
This commit is contained in:
parent
9efbc7dd7a
commit
8c4362a98d
7 changed files with 289 additions and 119 deletions
|
@ -117,6 +117,6 @@ namespace SourceGit.Models
|
||||||
public class CommitFullMessage
|
public class CommitFullMessage
|
||||||
{
|
{
|
||||||
public string Message { get; set; } = string.Empty;
|
public string Message { get; set; } = string.Empty;
|
||||||
public List<Hyperlink> Links { get; set; } = [];
|
public List<InlineElement> Inlines { get; set; } = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,27 @@
|
||||||
namespace SourceGit.Models
|
namespace SourceGit.Models
|
||||||
{
|
{
|
||||||
public class Hyperlink
|
public enum InlineElementType
|
||||||
{
|
{
|
||||||
|
None = 0,
|
||||||
|
Keyword,
|
||||||
|
Link,
|
||||||
|
CommitSHA,
|
||||||
|
Code,
|
||||||
|
}
|
||||||
|
|
||||||
|
public class InlineElement
|
||||||
|
{
|
||||||
|
public InlineElementType Type { get; set; } = InlineElementType.None;
|
||||||
public int Start { get; set; } = 0;
|
public int Start { get; set; } = 0;
|
||||||
public int Length { get; set; } = 0;
|
public int Length { get; set; } = 0;
|
||||||
public string Link { get; set; } = "";
|
public string Link { get; set; } = "";
|
||||||
public bool IsCommitSHA { get; set; } = false;
|
|
||||||
|
|
||||||
public Hyperlink(int start, int length, string link, bool isCommitSHA = false)
|
public InlineElement(InlineElementType type, int start, int length, string link)
|
||||||
{
|
{
|
||||||
|
Type = type;
|
||||||
Start = start;
|
Start = start;
|
||||||
Length = length;
|
Length = length;
|
||||||
Link = link;
|
Link = link;
|
||||||
IsCommitSHA = isCommitSHA;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Intersect(int start, int length)
|
public bool Intersect(int start, int length)
|
|
@ -46,7 +46,7 @@ namespace SourceGit.Models
|
||||||
set => SetProperty(ref _urlTemplate, value);
|
set => SetProperty(ref _urlTemplate, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Matches(List<Hyperlink> outs, string message)
|
public void Matches(List<InlineElement> outs, string message)
|
||||||
{
|
{
|
||||||
if (_regex == null || string.IsNullOrEmpty(_urlTemplate))
|
if (_regex == null || string.IsNullOrEmpty(_urlTemplate))
|
||||||
return;
|
return;
|
||||||
|
@ -81,8 +81,7 @@ namespace SourceGit.Models
|
||||||
link = link.Replace($"${j}", group.Value);
|
link = link.Replace($"${j}", group.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
var range = new Hyperlink(start, len, link);
|
outs.Add(new InlineElement(InlineElementType.Link, start, len, link));
|
||||||
outs.Add(range);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -578,10 +578,10 @@ namespace SourceGit.ViewModels
|
||||||
Task.Run(() =>
|
Task.Run(() =>
|
||||||
{
|
{
|
||||||
var message = new Commands.QueryCommitFullMessage(_repo.FullPath, _commit.SHA).Result();
|
var message = new Commands.QueryCommitFullMessage(_repo.FullPath, _commit.SHA).Result();
|
||||||
var links = ParseLinksInMessage(message);
|
var inlines = ParseInlinesInMessage(message);
|
||||||
|
|
||||||
if (!token.IsCancellationRequested)
|
if (!token.IsCancellationRequested)
|
||||||
Dispatcher.UIThread.Invoke(() => FullMessage = new Models.CommitFullMessage { Message = message, Links = links });
|
Dispatcher.UIThread.Invoke(() => FullMessage = new Models.CommitFullMessage { Message = message, Inlines = inlines });
|
||||||
});
|
});
|
||||||
|
|
||||||
Task.Run(() =>
|
Task.Run(() =>
|
||||||
|
@ -633,13 +633,13 @@ namespace SourceGit.ViewModels
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Models.Hyperlink> ParseLinksInMessage(string message)
|
private List<Models.InlineElement> ParseInlinesInMessage(string message)
|
||||||
{
|
{
|
||||||
var links = new List<Models.Hyperlink>();
|
var inlines = new List<Models.InlineElement>();
|
||||||
if (_repo.Settings.IssueTrackerRules is { Count: > 0 } rules)
|
if (_repo.Settings.IssueTrackerRules is { Count: > 0 } rules)
|
||||||
{
|
{
|
||||||
foreach (var rule in rules)
|
foreach (var rule in rules)
|
||||||
rule.Matches(links, message);
|
rule.Matches(inlines, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
var matches = REG_SHA_FORMAT().Matches(message);
|
var matches = REG_SHA_FORMAT().Matches(message);
|
||||||
|
@ -652,7 +652,7 @@ namespace SourceGit.ViewModels
|
||||||
var start = match.Index;
|
var start = match.Index;
|
||||||
var len = match.Length;
|
var len = match.Length;
|
||||||
var intersect = false;
|
var intersect = false;
|
||||||
foreach (var link in links)
|
foreach (var link in inlines)
|
||||||
{
|
{
|
||||||
if (link.Intersect(start, len))
|
if (link.Intersect(start, len))
|
||||||
{
|
{
|
||||||
|
@ -667,13 +667,13 @@ namespace SourceGit.ViewModels
|
||||||
var sha = match.Groups[1].Value;
|
var sha = match.Groups[1].Value;
|
||||||
var isCommitSHA = new Commands.IsCommitSHA(_repo.FullPath, sha).Result();
|
var isCommitSHA = new Commands.IsCommitSHA(_repo.FullPath, sha).Result();
|
||||||
if (isCommitSHA)
|
if (isCommitSHA)
|
||||||
links.Add(new Models.Hyperlink(start, len, sha, true));
|
inlines.Add(new Models.InlineElement(Models.InlineElementType.CommitSHA, start, len, sha));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (links.Count > 0)
|
if (inlines.Count > 0)
|
||||||
links.Sort((l, r) => l.Start - r.Start);
|
inlines.Sort((l, r) => l.Start - r.Start);
|
||||||
|
|
||||||
return links;
|
return inlines;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RefreshVisibleChanges()
|
private void RefreshVisibleChanges()
|
||||||
|
|
|
@ -39,7 +39,7 @@ namespace SourceGit.Views
|
||||||
if (string.IsNullOrEmpty(message))
|
if (string.IsNullOrEmpty(message))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var links = FullMessage?.Links;
|
var links = FullMessage?.Inlines;
|
||||||
if (links == null || links.Count == 0)
|
if (links == null || links.Count == 0)
|
||||||
{
|
{
|
||||||
Inlines.Add(new Run(message));
|
Inlines.Add(new Run(message));
|
||||||
|
@ -54,7 +54,7 @@ namespace SourceGit.Views
|
||||||
inlines.Add(new Run(message.Substring(pos, link.Start - pos)));
|
inlines.Add(new Run(message.Substring(pos, link.Start - pos)));
|
||||||
|
|
||||||
var run = new Run(message.Substring(link.Start, link.Length));
|
var run = new Run(message.Substring(link.Start, link.Length));
|
||||||
run.Classes.Add(link.IsCommitSHA ? "commit_link" : "issue_link");
|
run.Classes.Add(link.Type == Models.InlineElementType.CommitSHA ? "commit_link" : "issue_link");
|
||||||
inlines.Add(run);
|
inlines.Add(run);
|
||||||
|
|
||||||
pos = link.Start + link.Length;
|
pos = link.Start + link.Length;
|
||||||
|
@ -87,7 +87,7 @@ namespace SourceGit.Views
|
||||||
scrollViewer.LineDown();
|
scrollViewer.LineDown();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (FullMessage is { Links: { Count: > 0 } links })
|
else if (FullMessage is { Inlines: { Count: > 0 } links })
|
||||||
{
|
{
|
||||||
var point = e.GetPosition(this) - new Point(Padding.Left, Padding.Top);
|
var point = e.GetPosition(this) - new Point(Padding.Left, Padding.Top);
|
||||||
var x = Math.Min(Math.Max(point.X, 0), Math.Max(TextLayout.WidthIncludingTrailingWhitespace, 0));
|
var x = Math.Min(Math.Max(point.X, 0), Math.Max(TextLayout.WidthIncludingTrailingWhitespace, 0));
|
||||||
|
@ -106,7 +106,7 @@ namespace SourceGit.Views
|
||||||
SetCurrentValue(CursorProperty, Cursor.Parse("Hand"));
|
SetCurrentValue(CursorProperty, Cursor.Parse("Hand"));
|
||||||
|
|
||||||
_lastHover = link;
|
_lastHover = link;
|
||||||
if (!link.IsCommitSHA)
|
if (link.Type == Models.InlineElementType.Link)
|
||||||
ToolTip.SetTip(this, link.Link);
|
ToolTip.SetTip(this, link.Link);
|
||||||
else
|
else
|
||||||
ProcessHoverCommitLink(link);
|
ProcessHoverCommitLink(link);
|
||||||
|
@ -127,7 +127,7 @@ namespace SourceGit.Views
|
||||||
var link = _lastHover.Link;
|
var link = _lastHover.Link;
|
||||||
e.Pointer.Capture(null);
|
e.Pointer.Capture(null);
|
||||||
|
|
||||||
if (_lastHover.IsCommitSHA)
|
if (_lastHover.Type == Models.InlineElementType.CommitSHA)
|
||||||
{
|
{
|
||||||
var parentView = this.FindAncestorOfType<CommitBaseInfo>();
|
var parentView = this.FindAncestorOfType<CommitBaseInfo>();
|
||||||
if (parentView is { DataContext: ViewModels.CommitDetail detail })
|
if (parentView is { DataContext: ViewModels.CommitDetail detail })
|
||||||
|
@ -252,7 +252,7 @@ namespace SourceGit.Views
|
||||||
ClearHoveredIssueLink();
|
ClearHoveredIssueLink();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ProcessHoverCommitLink(Models.Hyperlink link)
|
private void ProcessHoverCommitLink(Models.InlineElement link)
|
||||||
{
|
{
|
||||||
var sha = link.Link;
|
var sha = link.Link;
|
||||||
|
|
||||||
|
@ -301,7 +301,7 @@ namespace SourceGit.Views
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Models.Hyperlink _lastHover = null;
|
private Models.InlineElement _lastHover = null;
|
||||||
private Dictionary<string, Models.Commit> _inlineCommits = new();
|
private Dictionary<string, Models.Commit> _inlineCommits = new();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,72 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Collections;
|
using Avalonia.Collections;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Controls.Documents;
|
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
using Avalonia.Media.TextFormatting;
|
|
||||||
|
|
||||||
namespace SourceGit.Views
|
namespace SourceGit.Views
|
||||||
{
|
{
|
||||||
public partial class CommitSubjectPresenter : TextBlock
|
public partial class CommitSubjectPresenter : Control
|
||||||
{
|
{
|
||||||
|
public static readonly StyledProperty<FontFamily> FontFamilyProperty =
|
||||||
|
AvaloniaProperty.Register<CommitSubjectPresenter, FontFamily>(nameof(FontFamily));
|
||||||
|
|
||||||
|
public FontFamily FontFamily
|
||||||
|
{
|
||||||
|
get => GetValue(FontFamilyProperty);
|
||||||
|
set => SetValue(FontFamilyProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<FontFamily> CodeFontFamilyProperty =
|
||||||
|
AvaloniaProperty.Register<CommitSubjectPresenter, FontFamily>(nameof(CodeFontFamily));
|
||||||
|
|
||||||
|
public FontFamily CodeFontFamily
|
||||||
|
{
|
||||||
|
get => GetValue(CodeFontFamilyProperty);
|
||||||
|
set => SetValue(CodeFontFamilyProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<double> FontSizeProperty =
|
||||||
|
TextBlock.FontSizeProperty.AddOwner<CommitSubjectPresenter>();
|
||||||
|
|
||||||
|
public double FontSize
|
||||||
|
{
|
||||||
|
get => GetValue(FontSizeProperty);
|
||||||
|
set => SetValue(FontSizeProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<FontWeight> FontWeightProperty =
|
||||||
|
TextBlock.FontWeightProperty.AddOwner<CommitSubjectPresenter>();
|
||||||
|
|
||||||
|
public FontWeight FontWeight
|
||||||
|
{
|
||||||
|
get => GetValue(FontWeightProperty);
|
||||||
|
set => SetValue(FontWeightProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<IBrush> ForegroundProperty =
|
||||||
|
AvaloniaProperty.Register<CommitSubjectPresenter, IBrush>(nameof(Foreground), Brushes.White);
|
||||||
|
|
||||||
|
public IBrush Foreground
|
||||||
|
{
|
||||||
|
get => GetValue(ForegroundProperty);
|
||||||
|
set => SetValue(ForegroundProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<IBrush> LinkForegroundProperty =
|
||||||
|
AvaloniaProperty.Register<CommitSubjectPresenter, IBrush>(nameof(LinkForeground), Brushes.White);
|
||||||
|
|
||||||
|
public IBrush LinkForeground
|
||||||
|
{
|
||||||
|
get => GetValue(LinkForegroundProperty);
|
||||||
|
set => SetValue(LinkForegroundProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
public static readonly StyledProperty<string> SubjectProperty =
|
public static readonly StyledProperty<string> SubjectProperty =
|
||||||
AvaloniaProperty.Register<CommitSubjectPresenter, string>(nameof(Subject));
|
AvaloniaProperty.Register<CommitSubjectPresenter, string>(nameof(Subject));
|
||||||
|
|
||||||
|
@ -31,7 +85,33 @@ namespace SourceGit.Views
|
||||||
set => SetValue(IssueTrackerRulesProperty, value);
|
set => SetValue(IssueTrackerRulesProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Type StyleKeyOverride => typeof(TextBlock);
|
public override void Render(DrawingContext context)
|
||||||
|
{
|
||||||
|
if (_needRebuildInlines)
|
||||||
|
{
|
||||||
|
_needRebuildInlines = false;
|
||||||
|
GenerateFormattedTextElements();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_inlines.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var height = Bounds.Height;
|
||||||
|
foreach (var inline in _inlines)
|
||||||
|
{
|
||||||
|
if (inline.Element is { Type: Models.InlineElementType.Code})
|
||||||
|
{
|
||||||
|
var rect = new Rect(inline.X, (height - inline.Text.Height - 2) * 0.5, inline.Text.WidthIncludingTrailingWhitespace + 8, inline.Text.Height + 2);
|
||||||
|
var roundedRect = new RoundedRect(rect, new CornerRadius(4));
|
||||||
|
context.DrawRectangle(new SolidColorBrush(new Color(52, 101, 108, 118)), null, roundedRect);
|
||||||
|
context.DrawText(inline.Text, new Point(inline.X + 4, (height - inline.Text.Height) * 0.5));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
context.DrawText(inline.Text, new Point(inline.X, (height - inline.Text.Height) * 0.5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||||
{
|
{
|
||||||
|
@ -39,85 +119,65 @@ namespace SourceGit.Views
|
||||||
|
|
||||||
if (change.Property == SubjectProperty || change.Property == IssueTrackerRulesProperty)
|
if (change.Property == SubjectProperty || change.Property == IssueTrackerRulesProperty)
|
||||||
{
|
{
|
||||||
Inlines!.Clear();
|
_elements.Clear();
|
||||||
_matches = null;
|
|
||||||
ClearHoveredIssueLink();
|
ClearHoveredIssueLink();
|
||||||
|
|
||||||
var subject = Subject;
|
var subject = Subject;
|
||||||
if (string.IsNullOrEmpty(subject))
|
if (string.IsNullOrEmpty(subject))
|
||||||
|
{
|
||||||
|
_inlines.Clear();
|
||||||
|
InvalidateVisual();
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var keywordMatch = REG_KEYWORD_FORMAT1().Match(subject);
|
var keywordMatch = REG_KEYWORD_FORMAT1().Match(subject);
|
||||||
if (!keywordMatch.Success)
|
if (!keywordMatch.Success)
|
||||||
keywordMatch = REG_KEYWORD_FORMAT2().Match(subject);
|
keywordMatch = REG_KEYWORD_FORMAT2().Match(subject);
|
||||||
|
|
||||||
|
if (keywordMatch.Success)
|
||||||
|
_elements.Add(new Models.InlineElement(Models.InlineElementType.Keyword, 0, keywordMatch.Length, string.Empty));
|
||||||
|
|
||||||
|
var codeMatches = REG_INLINECODE_FORMAT().Matches(subject);
|
||||||
|
for (var i = 0; i < codeMatches.Count; i++)
|
||||||
|
{
|
||||||
|
var match = codeMatches[i];
|
||||||
|
if (!match.Success)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var start = match.Index;
|
||||||
|
var len = match.Length;
|
||||||
|
var intersect = false;
|
||||||
|
foreach (var exist in _elements)
|
||||||
|
{
|
||||||
|
if (exist.Intersect(start, len))
|
||||||
|
{
|
||||||
|
intersect = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (intersect)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
_elements.Add(new Models.InlineElement(Models.InlineElementType.Code, start, len, string.Empty));
|
||||||
|
}
|
||||||
|
|
||||||
var rules = IssueTrackerRules ?? [];
|
var rules = IssueTrackerRules ?? [];
|
||||||
var matches = new List<Models.Hyperlink>();
|
|
||||||
foreach (var rule in rules)
|
foreach (var rule in rules)
|
||||||
rule.Matches(matches, subject);
|
rule.Matches(_elements, subject);
|
||||||
|
|
||||||
if (matches.Count == 0)
|
_needRebuildInlines = true;
|
||||||
{
|
InvalidateVisual();
|
||||||
if (keywordMatch.Success)
|
}
|
||||||
{
|
else if (change.Property == FontFamilyProperty ||
|
||||||
Inlines.Add(new Run(subject.Substring(0, keywordMatch.Length)) { FontWeight = FontWeight.Bold });
|
change.Property == CodeFontFamilyProperty ||
|
||||||
Inlines.Add(new Run(subject.Substring(keywordMatch.Length)));
|
change.Property == FontSizeProperty ||
|
||||||
}
|
change.Property == FontWeightProperty ||
|
||||||
else
|
change.Property == ForegroundProperty ||
|
||||||
{
|
change.Property == LinkForegroundProperty)
|
||||||
Inlines.Add(new Run(subject));
|
{
|
||||||
}
|
_needRebuildInlines = true;
|
||||||
return;
|
InvalidateVisual();
|
||||||
}
|
|
||||||
|
|
||||||
matches.Sort((l, r) => l.Start - r.Start);
|
|
||||||
_matches = matches;
|
|
||||||
|
|
||||||
var inlines = new List<Inline>();
|
|
||||||
var pos = 0;
|
|
||||||
foreach (var match in matches)
|
|
||||||
{
|
|
||||||
if (match.Start > pos)
|
|
||||||
{
|
|
||||||
if (keywordMatch.Success && pos < keywordMatch.Length)
|
|
||||||
{
|
|
||||||
if (keywordMatch.Length < match.Start)
|
|
||||||
{
|
|
||||||
inlines.Add(new Run(subject.Substring(pos, keywordMatch.Length - pos)) { FontWeight = FontWeight.Bold });
|
|
||||||
inlines.Add(new Run(subject.Substring(keywordMatch.Length, match.Start - keywordMatch.Length)));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
inlines.Add(new Run(subject.Substring(pos, match.Start - pos)) { FontWeight = FontWeight.Bold });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
inlines.Add(new Run(subject.Substring(pos, match.Start - pos)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var link = new Run(subject.Substring(match.Start, match.Length));
|
|
||||||
link.Classes.Add("issue_link");
|
|
||||||
inlines.Add(link);
|
|
||||||
|
|
||||||
pos = match.Start + match.Length;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pos < subject.Length)
|
|
||||||
{
|
|
||||||
if (keywordMatch.Success && pos < keywordMatch.Length)
|
|
||||||
{
|
|
||||||
inlines.Add(new Run(subject.Substring(pos, keywordMatch.Length - pos)) { FontWeight = FontWeight.Bold });
|
|
||||||
inlines.Add(new Run(subject.Substring(keywordMatch.Length)));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
inlines.Add(new Run(subject.Substring(pos)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Inlines.AddRange(inlines);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,31 +185,23 @@ namespace SourceGit.Views
|
||||||
{
|
{
|
||||||
base.OnPointerMoved(e);
|
base.OnPointerMoved(e);
|
||||||
|
|
||||||
if (_matches != null)
|
var point = e.GetPosition(this);
|
||||||
|
foreach (var inline in _inlines)
|
||||||
{
|
{
|
||||||
var point = e.GetPosition(this) - new Point(Padding.Left, Padding.Top);
|
if (inline.Element is not { Type: Models.InlineElementType.Link } link)
|
||||||
var x = Math.Min(Math.Max(point.X, 0), Math.Max(TextLayout.WidthIncludingTrailingWhitespace, 0));
|
continue;
|
||||||
var y = Math.Min(Math.Max(point.Y, 0), Math.Max(TextLayout.Height, 0));
|
|
||||||
point = new Point(x, y);
|
|
||||||
|
|
||||||
var textPosition = TextLayout.HitTestPoint(point).TextPosition;
|
if (inline.X > point.X || inline.X + inline.Text.WidthIncludingTrailingWhitespace < point.X)
|
||||||
foreach (var match in _matches)
|
continue;
|
||||||
{
|
|
||||||
if (!match.Intersect(textPosition, 1))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (match == _lastHover)
|
_lastHover = link;
|
||||||
return;
|
SetCurrentValue(CursorProperty, Cursor.Parse("Hand"));
|
||||||
|
ToolTip.SetTip(this, link.Link);
|
||||||
_lastHover = match;
|
e.Handled = true;
|
||||||
SetCurrentValue(CursorProperty, Cursor.Parse("Hand"));
|
return;
|
||||||
ToolTip.SetTip(this, match.Link);
|
|
||||||
e.Handled = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ClearHoveredIssueLink();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ClearHoveredIssueLink();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnPointerPressed(PointerPressedEventArgs e)
|
protected override void OnPointerPressed(PointerPressedEventArgs e)
|
||||||
|
@ -166,6 +218,94 @@ namespace SourceGit.Views
|
||||||
ClearHoveredIssueLink();
|
ClearHoveredIssueLink();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void GenerateFormattedTextElements()
|
||||||
|
{
|
||||||
|
_inlines.Clear();
|
||||||
|
|
||||||
|
var subject = Subject;
|
||||||
|
if (string.IsNullOrEmpty(subject))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var fontFamily = FontFamily;
|
||||||
|
var codeFontFamily = CodeFontFamily;
|
||||||
|
var fontSize = FontSize;
|
||||||
|
var foreground = Foreground;
|
||||||
|
var linkForeground = LinkForeground;
|
||||||
|
var typeface = new Typeface(fontFamily, FontStyle.Normal, FontWeight);
|
||||||
|
var codeTypeface = new Typeface(codeFontFamily, FontStyle.Normal, FontWeight);
|
||||||
|
var pos = 0;
|
||||||
|
var x = 0.0;
|
||||||
|
foreach (var elem in _elements)
|
||||||
|
{
|
||||||
|
if (elem.Start > pos)
|
||||||
|
{
|
||||||
|
var normal = new FormattedText(
|
||||||
|
subject.Substring(pos, elem.Start - pos),
|
||||||
|
CultureInfo.CurrentCulture,
|
||||||
|
FlowDirection.LeftToRight,
|
||||||
|
typeface,
|
||||||
|
fontSize,
|
||||||
|
foreground);
|
||||||
|
|
||||||
|
_inlines.Add(new Inline(x, normal, null));
|
||||||
|
x += normal.WidthIncludingTrailingWhitespace;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elem.Type == Models.InlineElementType.Keyword)
|
||||||
|
{
|
||||||
|
var keyword = new FormattedText(
|
||||||
|
subject.Substring(elem.Start, elem.Length),
|
||||||
|
CultureInfo.CurrentCulture,
|
||||||
|
FlowDirection.LeftToRight,
|
||||||
|
new Typeface(fontFamily, FontStyle.Normal, FontWeight.Bold),
|
||||||
|
fontSize,
|
||||||
|
foreground);
|
||||||
|
_inlines.Add(new Inline(x, keyword, elem));
|
||||||
|
x += keyword.WidthIncludingTrailingWhitespace;
|
||||||
|
}
|
||||||
|
else if (elem.Type == Models.InlineElementType.Link)
|
||||||
|
{
|
||||||
|
var link = new FormattedText(
|
||||||
|
subject.Substring(elem.Start, elem.Length),
|
||||||
|
CultureInfo.CurrentCulture,
|
||||||
|
FlowDirection.LeftToRight,
|
||||||
|
typeface,
|
||||||
|
fontSize,
|
||||||
|
linkForeground);
|
||||||
|
_inlines.Add(new Inline(x, link, elem));
|
||||||
|
x += link.WidthIncludingTrailingWhitespace;
|
||||||
|
}
|
||||||
|
else if (elem.Type == Models.InlineElementType.Code)
|
||||||
|
{
|
||||||
|
var link = new FormattedText(
|
||||||
|
subject.Substring(elem.Start + 1, elem.Length - 2),
|
||||||
|
CultureInfo.CurrentCulture,
|
||||||
|
FlowDirection.LeftToRight,
|
||||||
|
codeTypeface,
|
||||||
|
fontSize,
|
||||||
|
foreground);
|
||||||
|
_inlines.Add(new Inline(x, link, elem));
|
||||||
|
x += link.WidthIncludingTrailingWhitespace + 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
pos = elem.Start + elem.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pos < subject.Length)
|
||||||
|
{
|
||||||
|
var normal = new FormattedText(
|
||||||
|
subject.Substring(pos),
|
||||||
|
CultureInfo.CurrentCulture,
|
||||||
|
FlowDirection.LeftToRight,
|
||||||
|
typeface,
|
||||||
|
fontSize,
|
||||||
|
foreground);
|
||||||
|
|
||||||
|
_inlines.Add(new Inline(x, normal, null));
|
||||||
|
x += normal.WidthIncludingTrailingWhitespace;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void ClearHoveredIssueLink()
|
private void ClearHoveredIssueLink()
|
||||||
{
|
{
|
||||||
if (_lastHover != null)
|
if (_lastHover != null)
|
||||||
|
@ -176,13 +316,32 @@ namespace SourceGit.Views
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[GeneratedRegex(@"`.*?`")]
|
||||||
|
private static partial Regex REG_INLINECODE_FORMAT();
|
||||||
|
|
||||||
[GeneratedRegex(@"^\[[\w\s]+\]")]
|
[GeneratedRegex(@"^\[[\w\s]+\]")]
|
||||||
private static partial Regex REG_KEYWORD_FORMAT1();
|
private static partial Regex REG_KEYWORD_FORMAT1();
|
||||||
|
|
||||||
[GeneratedRegex(@"^\S+([\<\(][\w\s_\-\*,]+[\>\)])?\!?\s?:\s")]
|
[GeneratedRegex(@"^\S+([\<\(][\w\s_\-\*,]+[\>\)])?\!?\s?:\s")]
|
||||||
private static partial Regex REG_KEYWORD_FORMAT2();
|
private static partial Regex REG_KEYWORD_FORMAT2();
|
||||||
|
|
||||||
private List<Models.Hyperlink> _matches = null;
|
private class Inline
|
||||||
private Models.Hyperlink _lastHover = null;
|
{
|
||||||
|
public double X { get; set; } = 0;
|
||||||
|
public FormattedText Text { get; set; } = null;
|
||||||
|
public Models.InlineElement Element { get; set; } = null;
|
||||||
|
|
||||||
|
public Inline(double x, FormattedText text, Models.InlineElement elem)
|
||||||
|
{
|
||||||
|
X = x;
|
||||||
|
Text = text;
|
||||||
|
Element = elem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Models.InlineElement> _elements = [];
|
||||||
|
private List<Inline> _inlines = [];
|
||||||
|
private Models.InlineElement _lastHover = null;
|
||||||
|
private bool _needRebuildInlines = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -160,7 +160,10 @@
|
||||||
</v:CommitRefsPresenter>
|
</v:CommitRefsPresenter>
|
||||||
|
|
||||||
<v:CommitSubjectPresenter Grid.Column="3"
|
<v:CommitSubjectPresenter Grid.Column="3"
|
||||||
Classes="primary"
|
FontFamily="{DynamicResource Fonts.Primary}"
|
||||||
|
CodeFontFamily="{DynamicResource Fonts.Monospace}"
|
||||||
|
Foreground="{DynamicResource Brush.FG1}"
|
||||||
|
LinkForeground="{DynamicResource Brush.Link}"
|
||||||
Subject="{Binding Subject}"
|
Subject="{Binding Subject}"
|
||||||
IssueTrackerRules="{Binding $parent[v:Histories].IssueTrackerRules}"
|
IssueTrackerRules="{Binding $parent[v:Histories].IssueTrackerRules}"
|
||||||
FontWeight="{Binding FontWeight}"
|
FontWeight="{Binding FontWeight}"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue