using System; using System.Collections.Generic; using System.Globalization; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Media; using AvaloniaEdit; using AvaloniaEdit.Document; using AvaloniaEdit.Editing; using AvaloniaEdit.Rendering; using AvaloniaEdit.TextMate; using AvaloniaEdit.Utils; namespace SourceGit.Views { public class BlameTextEditor : TextEditor { public class CommitInfoMargin : AbstractMargin { public CommitInfoMargin(BlameTextEditor editor) { _editor = editor; ClipToBounds = true; } public override void Render(DrawingContext context) { if (_editor.BlameData == null) return; var view = TextView; if (view != null && view.VisualLinesValid) { var typeface = view.CreateTypeface(); var underlinePen = new Pen(Brushes.DarkOrange, 1); foreach (var line in view.VisualLines) { var lineNumber = line.FirstDocumentLine.LineNumber; if (lineNumber > _editor.BlameData.LineInfos.Count) break; var info = _editor.BlameData.LineInfos[lineNumber - 1]; var x = 0.0; var y = line.GetTextLineVisualYPosition(line.TextLines[0], VisualYPosition.TextTop) - view.VerticalOffset; if (!info.IsFirstInGroup && y > view.DefaultLineHeight * 0.6) continue; var shaLink = new FormattedText( info.CommitSHA, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeface, _editor.FontSize, Brushes.DarkOrange); context.DrawText(shaLink, new Point(x, y)); context.DrawLine(underlinePen, new Point(x, y + shaLink.Baseline + 2), new Point(x + shaLink.Width, y + shaLink.Baseline + 2)); x += shaLink.Width + 8; var time = new FormattedText( info.Time, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeface, _editor.FontSize, _editor.Foreground); context.DrawText(time, new Point(x, y)); x += time.Width + 8; var author = new FormattedText( info.Author, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeface, _editor.FontSize, _editor.Foreground); context.DrawText(author, new Point(x, y)); } } } protected override Size MeasureOverride(Size availableSize) { var view = TextView; var maxWidth = 0.0; if (view != null && view.VisualLinesValid && _editor.BlameData != null) { var typeface = view.CreateTypeface(); var calculated = new HashSet(); foreach (var line in view.VisualLines) { var lineNumber = line.FirstDocumentLine.LineNumber; if (lineNumber > _editor.BlameData.LineInfos.Count) break; var info = _editor.BlameData.LineInfos[lineNumber - 1]; if (calculated.Contains(info.CommitSHA)) continue; calculated.Add(info.CommitSHA); var x = 0.0; var shaLink = new FormattedText( info.CommitSHA, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeface, _editor.FontSize, Brushes.DarkOrange); x += shaLink.Width + 8; var time = new FormattedText( info.Time, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeface, _editor.FontSize, _editor.Foreground); x += time.Width + 8; var author = new FormattedText( info.Author, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeface, _editor.FontSize, _editor.Foreground); x += author.Width; if (maxWidth < x) maxWidth = x; } } return new Size(maxWidth, 0); } protected override void OnPointerPressed(PointerPressedEventArgs e) { base.OnPointerPressed(e); var view = TextView; if (!e.Handled && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed && view != null && view.VisualLinesValid) { var pos = e.GetPosition(this); var typeface = view.CreateTypeface(); foreach (var line in view.VisualLines) { var lineNumber = line.FirstDocumentLine.LineNumber; if (lineNumber >= _editor.BlameData.LineInfos.Count) break; var info = _editor.BlameData.LineInfos[lineNumber - 1]; var y = line.GetTextLineVisualYPosition(line.TextLines[0], VisualYPosition.TextTop) - view.VerticalOffset; var shaLink = new FormattedText( info.CommitSHA, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeface, _editor.FontSize, Brushes.DarkOrange); var rect = new Rect(0, y, shaLink.Width, shaLink.Height); if (rect.Contains(pos)) { _editor.OnCommitSHAClicked(info.CommitSHA); e.Handled = true; break; } } } } private readonly BlameTextEditor _editor = null; } public class VerticalSeperatorMargin : AbstractMargin { public VerticalSeperatorMargin(BlameTextEditor editor) { _editor = editor; } public override void Render(DrawingContext context) { var pen = new Pen(_editor.BorderBrush, 1); context.DrawLine(pen, new Point(0, 0), new Point(0, Bounds.Height)); } protected override Size MeasureOverride(Size availableSize) { return new Size(1, 0); } private readonly BlameTextEditor _editor = null; } public static readonly StyledProperty BlameDataProperty = AvaloniaProperty.Register(nameof(BlameData)); public Models.BlameData BlameData { get => GetValue(BlameDataProperty); set => SetValue(BlameDataProperty, value); } protected override Type StyleKeyOverride => typeof(TextEditor); public BlameTextEditor() : base(new TextArea(), new TextDocument()) { IsReadOnly = true; ShowLineNumbers = false; WordWrap = false; _textMate = Models.TextMateHelper.CreateForEditor(this); TextArea.LeftMargins.Add(new LineNumberMargin() { Margin = new Thickness(8, 0) }); TextArea.LeftMargins.Add(new VerticalSeperatorMargin(this)); TextArea.LeftMargins.Add(new CommitInfoMargin(this) { Margin = new Thickness(8, 0) }); TextArea.LeftMargins.Add(new VerticalSeperatorMargin(this)); TextArea.TextView.ContextRequested += OnTextViewContextRequested; TextArea.TextView.VisualLinesChanged += OnTextViewVisualLinesChanged; TextArea.TextView.Margin = new Thickness(4, 0); TextArea.TextView.Options.EnableHyperlinks = false; TextArea.TextView.Options.EnableEmailHyperlinks = false; } public void OnCommitSHAClicked(string sha) { if (DataContext is ViewModels.Blame blame) { blame.NavigateToCommit(sha); } } protected override void OnUnloaded(RoutedEventArgs e) { base.OnUnloaded(e); TextArea.LeftMargins.Clear(); TextArea.TextView.ContextRequested -= OnTextViewContextRequested; TextArea.TextView.VisualLinesChanged -= OnTextViewVisualLinesChanged; if (_textMate != null) { _textMate.Dispose(); _textMate = null; } } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == BlameDataProperty) { if (BlameData != null) { Models.TextMateHelper.SetGrammarByFileName(_textMate, BlameData.File); Text = BlameData.Content; } else { Text = string.Empty; } } else if (change.Property.Name == "ActualThemeVariant" && change.NewValue != null) { Models.TextMateHelper.SetThemeByApp(_textMate); } } private void OnTextViewContextRequested(object sender, ContextRequestedEventArgs e) { var selected = SelectedText; if (string.IsNullOrEmpty(selected)) return; var icon = new Avalonia.Controls.Shapes.Path(); icon.Width = 10; icon.Height = 10; icon.Stretch = Stretch.Uniform; icon.Data = App.Current?.FindResource("Icons.Copy") as StreamGeometry; var copy = new MenuItem(); copy.Header = App.Text("Copy"); copy.Icon = icon; copy.Click += (o, ev) => { App.CopyText(selected); ev.Handled = true; }; var menu = new ContextMenu(); menu.Items.Add(copy); TextArea.TextView.OpenContextMenu(menu); e.Handled = true; } private void OnTextViewVisualLinesChanged(object sender, EventArgs e) { foreach (var margin in TextArea.LeftMargins) { if (margin is CommitInfoMargin commitInfo) { commitInfo.InvalidateMeasure(); break; } } } private TextMate.Installation _textMate = null; } public partial class Blame : ChromelessWindow { public Blame() { if (App.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { Owner = desktop.MainWindow; } InitializeComponent(); } private void MaximizeOrRestoreWindow(object sender, TappedEventArgs e) { _pressedTitleBar = false; if (WindowState == WindowState.Maximized) WindowState = WindowState.Normal; else WindowState = WindowState.Maximized; e.Handled = true; } private void BeginMoveWindow(object sender, PointerPressedEventArgs e) { if (e.ClickCount != 2) _pressedTitleBar = true; } private void MoveWindow(object sender, PointerEventArgs e) { if (!_pressedTitleBar) return; var visual = (Visual)e.Source; BeginMoveDrag(new PointerPressedEventArgs( e.Source, e.Pointer, visual, e.GetPosition(visual), e.Timestamp, new PointerPointProperties(RawInputModifiers.None, PointerUpdateKind.LeftButtonPressed), e.KeyModifiers)); } private void EndMoveWindow(object sender, PointerReleasedEventArgs e) { _pressedTitleBar = false; } protected override void OnClosed(EventArgs e) { base.OnClosed(e); GC.Collect(); } private void OnCommitSHAPointerPressed(object sender, PointerPressedEventArgs e) { if (sender is TextBlock txt && DataContext is ViewModels.Blame blame) blame.NavigateToCommit(txt.Text); e.Handled = true; } private bool _pressedTitleBar = false; } }