mirror of
https://github.com/sourcegit-scm/sourcegit
synced 2025-05-21 12:15:00 +00:00
363 lines
13 KiB
C#
363 lines
13 KiB
C#
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Text.RegularExpressions;
|
|
|
|
using Avalonia;
|
|
using Avalonia.Collections;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Input;
|
|
using Avalonia.Media;
|
|
|
|
namespace SourceGit.Views
|
|
{
|
|
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> InlineCodeBackgroundProperty =
|
|
AvaloniaProperty.Register<CommitSubjectPresenter, IBrush>(nameof(InlineCodeBackground), Brushes.Transparent);
|
|
|
|
public IBrush InlineCodeBackground
|
|
{
|
|
get => GetValue(InlineCodeBackgroundProperty);
|
|
set => SetValue(InlineCodeBackgroundProperty, 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 =
|
|
AvaloniaProperty.Register<CommitSubjectPresenter, string>(nameof(Subject));
|
|
|
|
public string Subject
|
|
{
|
|
get => GetValue(SubjectProperty);
|
|
set => SetValue(SubjectProperty, value);
|
|
}
|
|
|
|
public static readonly StyledProperty<AvaloniaList<Models.IssueTrackerRule>> IssueTrackerRulesProperty =
|
|
AvaloniaProperty.Register<CommitSubjectPresenter, AvaloniaList<Models.IssueTrackerRule>>(nameof(IssueTrackerRules));
|
|
|
|
public AvaloniaList<Models.IssueTrackerRule> IssueTrackerRules
|
|
{
|
|
get => GetValue(IssueTrackerRulesProperty);
|
|
set => SetValue(IssueTrackerRulesProperty, value);
|
|
}
|
|
|
|
public override void Render(DrawingContext context)
|
|
{
|
|
if (_needRebuildInlines)
|
|
{
|
|
_needRebuildInlines = false;
|
|
GenerateFormattedTextElements();
|
|
}
|
|
|
|
if (_inlines.Count == 0)
|
|
return;
|
|
|
|
var height = Bounds.Height;
|
|
var width = Bounds.Width;
|
|
foreach (var inline in _inlines)
|
|
{
|
|
if (inline.X > width)
|
|
return;
|
|
|
|
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(InlineCodeBackground, 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)
|
|
{
|
|
base.OnPropertyChanged(change);
|
|
|
|
if (change.Property == SubjectProperty || change.Property == IssueTrackerRulesProperty)
|
|
{
|
|
_elements.Clear();
|
|
ClearHoveredIssueLink();
|
|
|
|
var subject = Subject;
|
|
if (string.IsNullOrEmpty(subject))
|
|
{
|
|
_needRebuildInlines = true;
|
|
InvalidateVisual();
|
|
return;
|
|
}
|
|
|
|
var keywordMatch = REG_KEYWORD_FORMAT1().Match(subject);
|
|
if (!keywordMatch.Success)
|
|
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 ?? [];
|
|
foreach (var rule in rules)
|
|
rule.Matches(_elements, subject);
|
|
|
|
_needRebuildInlines = true;
|
|
InvalidateVisual();
|
|
}
|
|
else if (change.Property == FontFamilyProperty ||
|
|
change.Property == CodeFontFamilyProperty ||
|
|
change.Property == FontSizeProperty ||
|
|
change.Property == FontWeightProperty ||
|
|
change.Property == ForegroundProperty ||
|
|
change.Property == LinkForegroundProperty)
|
|
{
|
|
_needRebuildInlines = true;
|
|
InvalidateVisual();
|
|
}
|
|
else if (change.Property == InlineCodeBackgroundProperty)
|
|
{
|
|
InvalidateVisual();
|
|
}
|
|
}
|
|
|
|
protected override void OnPointerMoved(PointerEventArgs e)
|
|
{
|
|
base.OnPointerMoved(e);
|
|
|
|
var point = e.GetPosition(this);
|
|
foreach (var inline in _inlines)
|
|
{
|
|
if (inline.Element is not { Type: Models.InlineElementType.Link } link)
|
|
continue;
|
|
|
|
if (inline.X > point.X || inline.X + inline.Text.WidthIncludingTrailingWhitespace < point.X)
|
|
continue;
|
|
|
|
_lastHover = link;
|
|
SetCurrentValue(CursorProperty, Cursor.Parse("Hand"));
|
|
ToolTip.SetTip(this, link.Link);
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
|
|
ClearHoveredIssueLink();
|
|
}
|
|
|
|
protected override void OnPointerPressed(PointerPressedEventArgs e)
|
|
{
|
|
base.OnPointerPressed(e);
|
|
|
|
if (_lastHover != null)
|
|
Native.OS.OpenBrowser(_lastHover.Link);
|
|
}
|
|
|
|
protected override void OnPointerExited(PointerEventArgs e)
|
|
{
|
|
base.OnPointerExited(e);
|
|
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()
|
|
{
|
|
if (_lastHover != null)
|
|
{
|
|
ToolTip.SetTip(this, null);
|
|
SetCurrentValue(CursorProperty, Cursor.Parse("Arrow"));
|
|
_lastHover = null;
|
|
}
|
|
}
|
|
|
|
[GeneratedRegex(@"`.*?`")]
|
|
private static partial Regex REG_INLINECODE_FORMAT();
|
|
|
|
[GeneratedRegex(@"^\[[\w\s]+\]")]
|
|
private static partial Regex REG_KEYWORD_FORMAT1();
|
|
|
|
[GeneratedRegex(@"^\S+([\<\(][\w\s_\-\*,]+[\>\)])?\!?\s?:\s")]
|
|
private static partial Regex REG_KEYWORD_FORMAT2();
|
|
|
|
private class Inline
|
|
{
|
|
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;
|
|
}
|
|
}
|