mirror of
https://github.com/sourcegit-scm/sourcegit
synced 2025-05-20 19:55:00 +00:00
code_style: move some code from Histories.axaml
to separate files
Signed-off-by: leo <longshuang@msn.cn>
This commit is contained in:
parent
bb2284c4c9
commit
0476a825ef
6 changed files with 705 additions and 672 deletions
228
src/Views/CommitGraph.cs
Normal file
228
src/Views/CommitGraph.cs
Normal file
|
@ -0,0 +1,228 @@
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Media;
|
||||||
|
using Avalonia.VisualTree;
|
||||||
|
|
||||||
|
namespace SourceGit.Views
|
||||||
|
{
|
||||||
|
public class CommitGraph : Control
|
||||||
|
{
|
||||||
|
public static readonly StyledProperty<Models.CommitGraph> GraphProperty =
|
||||||
|
AvaloniaProperty.Register<CommitGraph, Models.CommitGraph>(nameof(Graph));
|
||||||
|
|
||||||
|
public Models.CommitGraph Graph
|
||||||
|
{
|
||||||
|
get => GetValue(GraphProperty);
|
||||||
|
set => SetValue(GraphProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<IBrush> DotBrushProperty =
|
||||||
|
AvaloniaProperty.Register<CommitGraph, IBrush>(nameof(DotBrush), Brushes.Transparent);
|
||||||
|
|
||||||
|
public IBrush DotBrush
|
||||||
|
{
|
||||||
|
get => GetValue(DotBrushProperty);
|
||||||
|
set => SetValue(DotBrushProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<bool> OnlyHighlightCurrentBranchProperty =
|
||||||
|
AvaloniaProperty.Register<CommitGraph, bool>(nameof(OnlyHighlightCurrentBranch), true);
|
||||||
|
|
||||||
|
public bool OnlyHighlightCurrentBranch
|
||||||
|
{
|
||||||
|
get => GetValue(OnlyHighlightCurrentBranchProperty);
|
||||||
|
set => SetValue(OnlyHighlightCurrentBranchProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
static CommitGraph()
|
||||||
|
{
|
||||||
|
AffectsRender<CommitGraph>(GraphProperty, DotBrushProperty, OnlyHighlightCurrentBranchProperty);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Render(DrawingContext context)
|
||||||
|
{
|
||||||
|
base.Render(context);
|
||||||
|
|
||||||
|
var graph = Graph;
|
||||||
|
if (graph == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var histories = this.FindAncestorOfType<Histories>();
|
||||||
|
if (histories == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var list = histories.CommitListContainer;
|
||||||
|
if (list == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Calculate drawing area.
|
||||||
|
double width = Bounds.Width - 273 - histories.AuthorNameColumnWidth.Value;
|
||||||
|
double height = Bounds.Height;
|
||||||
|
double startY = list.Scroll?.Offset.Y ?? 0;
|
||||||
|
double endY = startY + height + 28;
|
||||||
|
|
||||||
|
// Apply scroll offset and clip.
|
||||||
|
using (context.PushClip(new Rect(0, 0, width, height)))
|
||||||
|
using (context.PushTransform(Matrix.CreateTranslation(0, -startY)))
|
||||||
|
{
|
||||||
|
// Draw contents
|
||||||
|
DrawCurves(context, graph, startY, endY);
|
||||||
|
DrawAnchors(context, graph, startY, endY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawCurves(DrawingContext context, Models.CommitGraph graph, double top, double bottom)
|
||||||
|
{
|
||||||
|
var grayedPen = new Pen(new SolidColorBrush(Colors.Gray, 0.4), Models.CommitGraph.Pens[0].Thickness);
|
||||||
|
var onlyHighlightCurrentBranch = OnlyHighlightCurrentBranch;
|
||||||
|
|
||||||
|
if (onlyHighlightCurrentBranch)
|
||||||
|
{
|
||||||
|
foreach (var link in graph.Links)
|
||||||
|
{
|
||||||
|
if (link.IsMerged)
|
||||||
|
continue;
|
||||||
|
if (link.End.Y < top)
|
||||||
|
continue;
|
||||||
|
if (link.Start.Y > bottom)
|
||||||
|
break;
|
||||||
|
|
||||||
|
var geo = new StreamGeometry();
|
||||||
|
using (var ctx = geo.Open())
|
||||||
|
{
|
||||||
|
ctx.BeginFigure(link.Start, false);
|
||||||
|
ctx.QuadraticBezierTo(link.Control, link.End);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.DrawGeometry(null, grayedPen, geo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var line in graph.Paths)
|
||||||
|
{
|
||||||
|
var last = line.Points[0];
|
||||||
|
var size = line.Points.Count;
|
||||||
|
|
||||||
|
if (line.Points[size - 1].Y < top)
|
||||||
|
continue;
|
||||||
|
if (last.Y > bottom)
|
||||||
|
break;
|
||||||
|
|
||||||
|
var geo = new StreamGeometry();
|
||||||
|
var pen = Models.CommitGraph.Pens[line.Color];
|
||||||
|
|
||||||
|
using (var ctx = geo.Open())
|
||||||
|
{
|
||||||
|
var started = false;
|
||||||
|
var ended = false;
|
||||||
|
for (int i = 1; i < size; i++)
|
||||||
|
{
|
||||||
|
var cur = line.Points[i];
|
||||||
|
if (cur.Y < top)
|
||||||
|
{
|
||||||
|
last = cur;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!started)
|
||||||
|
{
|
||||||
|
ctx.BeginFigure(last, false);
|
||||||
|
started = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cur.Y > bottom)
|
||||||
|
{
|
||||||
|
cur = new Point(cur.X, bottom);
|
||||||
|
ended = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cur.X > last.X)
|
||||||
|
{
|
||||||
|
ctx.QuadraticBezierTo(new Point(cur.X, last.Y), cur);
|
||||||
|
}
|
||||||
|
else if (cur.X < last.X)
|
||||||
|
{
|
||||||
|
if (i < size - 1)
|
||||||
|
{
|
||||||
|
var midY = (last.Y + cur.Y) / 2;
|
||||||
|
ctx.CubicBezierTo(new Point(last.X, midY + 4), new Point(cur.X, midY - 4), cur);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ctx.QuadraticBezierTo(new Point(last.X, cur.Y), cur);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ctx.LineTo(cur);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ended)
|
||||||
|
break;
|
||||||
|
last = cur;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!line.IsMerged && onlyHighlightCurrentBranch)
|
||||||
|
context.DrawGeometry(null, grayedPen, geo);
|
||||||
|
else
|
||||||
|
context.DrawGeometry(null, pen, geo);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var link in graph.Links)
|
||||||
|
{
|
||||||
|
if (onlyHighlightCurrentBranch && !link.IsMerged)
|
||||||
|
continue;
|
||||||
|
if (link.End.Y < top)
|
||||||
|
continue;
|
||||||
|
if (link.Start.Y > bottom)
|
||||||
|
break;
|
||||||
|
|
||||||
|
var geo = new StreamGeometry();
|
||||||
|
using (var ctx = geo.Open())
|
||||||
|
{
|
||||||
|
ctx.BeginFigure(link.Start, false);
|
||||||
|
ctx.QuadraticBezierTo(link.Control, link.End);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.DrawGeometry(null, Models.CommitGraph.Pens[link.Color], geo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawAnchors(DrawingContext context, Models.CommitGraph graph, double top, double bottom)
|
||||||
|
{
|
||||||
|
var dotFill = DotBrush;
|
||||||
|
var dotFillPen = new Pen(dotFill, 2);
|
||||||
|
var grayedPen = new Pen(Brushes.Gray, Models.CommitGraph.Pens[0].Thickness);
|
||||||
|
var onlyHighlightCurrentBranch = OnlyHighlightCurrentBranch;
|
||||||
|
|
||||||
|
foreach (var dot in graph.Dots)
|
||||||
|
{
|
||||||
|
if (dot.Center.Y < top)
|
||||||
|
continue;
|
||||||
|
if (dot.Center.Y > bottom)
|
||||||
|
break;
|
||||||
|
|
||||||
|
var pen = Models.CommitGraph.Pens[dot.Color];
|
||||||
|
if (!dot.IsMerged && onlyHighlightCurrentBranch)
|
||||||
|
pen = grayedPen;
|
||||||
|
|
||||||
|
switch (dot.Type)
|
||||||
|
{
|
||||||
|
case Models.CommitGraph.DotType.Head:
|
||||||
|
context.DrawEllipse(dotFill, pen, dot.Center, 6, 6);
|
||||||
|
context.DrawEllipse(pen.Brush, null, dot.Center, 3, 3);
|
||||||
|
break;
|
||||||
|
case Models.CommitGraph.DotType.Merge:
|
||||||
|
context.DrawEllipse(pen.Brush, null, dot.Center, 6, 6);
|
||||||
|
context.DrawLine(dotFillPen, new Point(dot.Center.X, dot.Center.Y - 3), new Point(dot.Center.X, dot.Center.Y + 3));
|
||||||
|
context.DrawLine(dotFillPen, new Point(dot.Center.X - 3, dot.Center.Y), new Point(dot.Center.X + 3, dot.Center.Y));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
context.DrawEllipse(dotFill, pen, dot.Center, 3, 3);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
90
src/Views/CommitStatusIndicator.cs
Normal file
90
src/Views/CommitStatusIndicator.cs
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Media;
|
||||||
|
|
||||||
|
namespace SourceGit.Views
|
||||||
|
{
|
||||||
|
public class CommitStatusIndicator : Control
|
||||||
|
{
|
||||||
|
public static readonly StyledProperty<Models.Branch> CurrentBranchProperty =
|
||||||
|
AvaloniaProperty.Register<CommitStatusIndicator, Models.Branch>(nameof(CurrentBranch));
|
||||||
|
|
||||||
|
public Models.Branch CurrentBranch
|
||||||
|
{
|
||||||
|
get => GetValue(CurrentBranchProperty);
|
||||||
|
set => SetValue(CurrentBranchProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<IBrush> AheadBrushProperty =
|
||||||
|
AvaloniaProperty.Register<CommitStatusIndicator, IBrush>(nameof(AheadBrush));
|
||||||
|
|
||||||
|
public IBrush AheadBrush
|
||||||
|
{
|
||||||
|
get => GetValue(AheadBrushProperty);
|
||||||
|
set => SetValue(AheadBrushProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<IBrush> BehindBrushProperty =
|
||||||
|
AvaloniaProperty.Register<CommitStatusIndicator, IBrush>(nameof(BehindBrush));
|
||||||
|
|
||||||
|
public IBrush BehindBrush
|
||||||
|
{
|
||||||
|
get => GetValue(BehindBrushProperty);
|
||||||
|
set => SetValue(BehindBrushProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Status
|
||||||
|
{
|
||||||
|
Normal,
|
||||||
|
Ahead,
|
||||||
|
Behind,
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Render(DrawingContext context)
|
||||||
|
{
|
||||||
|
if (_status == Status.Normal)
|
||||||
|
return;
|
||||||
|
|
||||||
|
context.DrawEllipse(_status == Status.Ahead ? AheadBrush : BehindBrush, null, new Rect(0, 0, 5, 5));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Size MeasureOverride(Size availableSize)
|
||||||
|
{
|
||||||
|
if (DataContext is Models.Commit commit && CurrentBranch is not null)
|
||||||
|
{
|
||||||
|
var sha = commit.SHA;
|
||||||
|
var track = CurrentBranch.TrackStatus;
|
||||||
|
|
||||||
|
if (track.Ahead.Contains(sha))
|
||||||
|
_status = Status.Ahead;
|
||||||
|
else if (track.Behind.Contains(sha))
|
||||||
|
_status = Status.Behind;
|
||||||
|
else
|
||||||
|
_status = Status.Normal;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_status = Status.Normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _status == Status.Normal ? new Size(0, 0) : new Size(9, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDataContextChanged(EventArgs e)
|
||||||
|
{
|
||||||
|
base.OnDataContextChanged(e);
|
||||||
|
InvalidateMeasure();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||||
|
{
|
||||||
|
base.OnPropertyChanged(change);
|
||||||
|
if (change.Property == CurrentBranchProperty)
|
||||||
|
InvalidateMeasure();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Status _status = Status.Normal;
|
||||||
|
}
|
||||||
|
}
|
189
src/Views/CommitSubjectPresenter.cs
Normal file
189
src/Views/CommitSubjectPresenter.cs
Normal file
|
@ -0,0 +1,189 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Collections;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.Documents;
|
||||||
|
using Avalonia.Input;
|
||||||
|
using Avalonia.Media;
|
||||||
|
using Avalonia.Media.TextFormatting;
|
||||||
|
|
||||||
|
namespace SourceGit.Views
|
||||||
|
{
|
||||||
|
public partial class CommitSubjectPresenter : TextBlock
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Type StyleKeyOverride => typeof(TextBlock);
|
||||||
|
|
||||||
|
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||||
|
{
|
||||||
|
base.OnPropertyChanged(change);
|
||||||
|
|
||||||
|
if (change.Property == SubjectProperty || change.Property == IssueTrackerRulesProperty)
|
||||||
|
{
|
||||||
|
Inlines!.Clear();
|
||||||
|
_matches = null;
|
||||||
|
ClearHoveredIssueLink();
|
||||||
|
|
||||||
|
var subject = Subject;
|
||||||
|
if (string.IsNullOrEmpty(subject))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var keywordMatch = REG_KEYWORD_FORMAT1().Match(subject);
|
||||||
|
if (!keywordMatch.Success)
|
||||||
|
keywordMatch = REG_KEYWORD_FORMAT2().Match(subject);
|
||||||
|
|
||||||
|
var rules = IssueTrackerRules ?? [];
|
||||||
|
var matches = new List<Models.Hyperlink>();
|
||||||
|
foreach (var rule in rules)
|
||||||
|
rule.Matches(matches, subject);
|
||||||
|
|
||||||
|
if (matches.Count == 0)
|
||||||
|
{
|
||||||
|
if (keywordMatch.Success)
|
||||||
|
{
|
||||||
|
Inlines.Add(new Run(subject.Substring(0, keywordMatch.Length)) { FontWeight = FontWeight.Bold });
|
||||||
|
Inlines.Add(new Run(subject.Substring(keywordMatch.Length)));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Inlines.Add(new Run(subject));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnPointerMoved(PointerEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnPointerMoved(e);
|
||||||
|
|
||||||
|
if (_matches != null)
|
||||||
|
{
|
||||||
|
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 y = Math.Min(Math.Max(point.Y, 0), Math.Max(TextLayout.Height, 0));
|
||||||
|
point = new Point(x, y);
|
||||||
|
|
||||||
|
var textPosition = TextLayout.HitTestPoint(point).TextPosition;
|
||||||
|
foreach (var match in _matches)
|
||||||
|
{
|
||||||
|
if (!match.Intersect(textPosition, 1))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (match == _lastHover)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_lastHover = match;
|
||||||
|
SetCurrentValue(CursorProperty, Cursor.Parse("Hand"));
|
||||||
|
ToolTip.SetTip(this, match.Link);
|
||||||
|
ToolTip.SetIsOpen(this, true);
|
||||||
|
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 ClearHoveredIssueLink()
|
||||||
|
{
|
||||||
|
if (_lastHover != null)
|
||||||
|
{
|
||||||
|
ToolTip.SetTip(this, null);
|
||||||
|
SetCurrentValue(CursorProperty, Cursor.Parse("Arrow"));
|
||||||
|
_lastHover = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[GeneratedRegex(@"^\[[\w\s]+\]")]
|
||||||
|
private static partial Regex REG_KEYWORD_FORMAT1();
|
||||||
|
|
||||||
|
[GeneratedRegex(@"^\S+([\<\(][\w\s_\-\*,]+[\>\)])?\!?\s?:\s")]
|
||||||
|
private static partial Regex REG_KEYWORD_FORMAT2();
|
||||||
|
|
||||||
|
private List<Models.Hyperlink> _matches = null;
|
||||||
|
private Models.Hyperlink _lastHover = null;
|
||||||
|
}
|
||||||
|
}
|
163
src/Views/CommitTimeTextBlock.cs
Normal file
163
src/Views/CommitTimeTextBlock.cs
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
|
||||||
|
namespace SourceGit.Views
|
||||||
|
{
|
||||||
|
public class CommitTimeTextBlock : TextBlock
|
||||||
|
{
|
||||||
|
public static readonly StyledProperty<bool> ShowAsDateTimeProperty =
|
||||||
|
AvaloniaProperty.Register<CommitTimeTextBlock, bool>(nameof(ShowAsDateTime), true);
|
||||||
|
|
||||||
|
public bool ShowAsDateTime
|
||||||
|
{
|
||||||
|
get => GetValue(ShowAsDateTimeProperty);
|
||||||
|
set => SetValue(ShowAsDateTimeProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<int> DateTimeFormatProperty =
|
||||||
|
AvaloniaProperty.Register<CommitTimeTextBlock, int>(nameof(DateTimeFormat), 0);
|
||||||
|
|
||||||
|
public int DateTimeFormat
|
||||||
|
{
|
||||||
|
get => GetValue(DateTimeFormatProperty);
|
||||||
|
set => SetValue(DateTimeFormatProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<bool> UseAuthorTimeProperty =
|
||||||
|
AvaloniaProperty.Register<CommitTimeTextBlock, bool>(nameof(UseAuthorTime), true);
|
||||||
|
|
||||||
|
public bool UseAuthorTime
|
||||||
|
{
|
||||||
|
get => GetValue(UseAuthorTimeProperty);
|
||||||
|
set => SetValue(UseAuthorTimeProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Type StyleKeyOverride => typeof(TextBlock);
|
||||||
|
|
||||||
|
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||||
|
{
|
||||||
|
base.OnPropertyChanged(change);
|
||||||
|
|
||||||
|
if (change.Property == UseAuthorTimeProperty)
|
||||||
|
{
|
||||||
|
SetCurrentValue(TextProperty, GetDisplayText());
|
||||||
|
}
|
||||||
|
else if (change.Property == ShowAsDateTimeProperty)
|
||||||
|
{
|
||||||
|
SetCurrentValue(TextProperty, GetDisplayText());
|
||||||
|
|
||||||
|
if (ShowAsDateTime)
|
||||||
|
StopTimer();
|
||||||
|
else
|
||||||
|
StartTimer();
|
||||||
|
}
|
||||||
|
else if (change.Property == DateTimeFormatProperty)
|
||||||
|
{
|
||||||
|
if (ShowAsDateTime)
|
||||||
|
SetCurrentValue(TextProperty, GetDisplayText());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnLoaded(RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnLoaded(e);
|
||||||
|
|
||||||
|
if (!ShowAsDateTime)
|
||||||
|
StartTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnUnloaded(RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnUnloaded(e);
|
||||||
|
StopTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDataContextChanged(EventArgs e)
|
||||||
|
{
|
||||||
|
base.OnDataContextChanged(e);
|
||||||
|
SetCurrentValue(TextProperty, GetDisplayText());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StartTimer()
|
||||||
|
{
|
||||||
|
if (_refreshTimer != null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_refreshTimer = DispatcherTimer.Run(() =>
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Invoke(() =>
|
||||||
|
{
|
||||||
|
var text = GetDisplayText();
|
||||||
|
if (!text.Equals(Text, StringComparison.Ordinal))
|
||||||
|
Text = text;
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}, TimeSpan.FromSeconds(10));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StopTimer()
|
||||||
|
{
|
||||||
|
if (_refreshTimer != null)
|
||||||
|
{
|
||||||
|
_refreshTimer.Dispose();
|
||||||
|
_refreshTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetDisplayText()
|
||||||
|
{
|
||||||
|
var commit = DataContext as Models.Commit;
|
||||||
|
if (commit == null)
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
if (ShowAsDateTime)
|
||||||
|
return UseAuthorTime ? commit.AuthorTimeStr : commit.CommitterTimeStr;
|
||||||
|
|
||||||
|
var timestamp = UseAuthorTime ? commit.AuthorTime : commit.CommitterTime;
|
||||||
|
var now = DateTime.Now;
|
||||||
|
var localTime = DateTime.UnixEpoch.AddSeconds(timestamp).ToLocalTime();
|
||||||
|
var span = now - localTime;
|
||||||
|
if (span.TotalMinutes < 1)
|
||||||
|
return App.Text("Period.JustNow");
|
||||||
|
|
||||||
|
if (span.TotalHours < 1)
|
||||||
|
return App.Text("Period.MinutesAgo", (int)span.TotalMinutes);
|
||||||
|
|
||||||
|
if (span.TotalDays < 1)
|
||||||
|
return App.Text("Period.HoursAgo", (int)span.TotalHours);
|
||||||
|
|
||||||
|
var lastDay = now.AddDays(-1).Date;
|
||||||
|
if (localTime >= lastDay)
|
||||||
|
return App.Text("Period.Yesterday");
|
||||||
|
|
||||||
|
if ((localTime.Year == now.Year && localTime.Month == now.Month) || span.TotalDays < 28)
|
||||||
|
{
|
||||||
|
var diffDay = now.Date - localTime.Date;
|
||||||
|
return App.Text("Period.DaysAgo", (int)diffDay.TotalDays);
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastMonth = now.AddMonths(-1).Date;
|
||||||
|
if (localTime.Year == lastMonth.Year && localTime.Month == lastMonth.Month)
|
||||||
|
return App.Text("Period.LastMonth");
|
||||||
|
|
||||||
|
if (localTime.Year == now.Year || localTime > now.AddMonths(-11))
|
||||||
|
{
|
||||||
|
var diffMonth = (12 + now.Month - localTime.Month) % 12;
|
||||||
|
return App.Text("Period.MonthsAgo", diffMonth);
|
||||||
|
}
|
||||||
|
|
||||||
|
var diffYear = now.Year - localTime.Year;
|
||||||
|
if (diffYear == 1)
|
||||||
|
return App.Text("Period.LastYear");
|
||||||
|
|
||||||
|
return App.Text("Period.YearsAgo", diffYear);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IDisposable _refreshTimer = null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,18 +10,18 @@
|
||||||
x:Class="SourceGit.Views.Histories"
|
x:Class="SourceGit.Views.Histories"
|
||||||
x:DataType="vm:Histories"
|
x:DataType="vm:Histories"
|
||||||
x:Name="ThisControl">
|
x:Name="ThisControl">
|
||||||
<v:LayoutableGrid UseHorizontal="{Binding Source={x:Static vm:Preferences.Instance}, Path=UseTwoColumnsLayoutInHistories}">
|
<v:HistoriesLayout UseHorizontal="{Binding Source={x:Static vm:Preferences.Instance}, Path=UseTwoColumnsLayoutInHistories}">
|
||||||
<v:LayoutableGrid.RowDefinitions>
|
<v:HistoriesLayout.RowDefinitions>
|
||||||
<RowDefinition Height="{Binding TopArea, Mode=TwoWay}" MinHeight="100"/>
|
<RowDefinition Height="{Binding TopArea, Mode=TwoWay}" MinHeight="100"/>
|
||||||
<RowDefinition Height="3"/>
|
<RowDefinition Height="3"/>
|
||||||
<RowDefinition Height="{Binding BottomArea, Mode=TwoWay}" MinHeight="200"/>
|
<RowDefinition Height="{Binding BottomArea, Mode=TwoWay}" MinHeight="200"/>
|
||||||
</v:LayoutableGrid.RowDefinitions>
|
</v:HistoriesLayout.RowDefinitions>
|
||||||
|
|
||||||
<v:LayoutableGrid.ColumnDefinitions>
|
<v:HistoriesLayout.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="{Binding LeftArea, Mode=TwoWay}" MinWidth="100"/>
|
<ColumnDefinition Width="{Binding LeftArea, Mode=TwoWay}" MinWidth="100"/>
|
||||||
<ColumnDefinition Width="3"/>
|
<ColumnDefinition Width="3"/>
|
||||||
<ColumnDefinition Width="{Binding RightArea, Mode=TwoWay}" MinWidth="100"/>
|
<ColumnDefinition Width="{Binding RightArea, Mode=TwoWay}" MinWidth="100"/>
|
||||||
</v:LayoutableGrid.ColumnDefinitions>
|
</v:HistoriesLayout.ColumnDefinitions>
|
||||||
|
|
||||||
<Grid Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3">
|
<Grid Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3">
|
||||||
<Grid RowDefinitions="24,*" Grid.IsSharedSizeScope="True">
|
<Grid RowDefinitions="24,*" Grid.IsSharedSizeScope="True">
|
||||||
|
@ -264,5 +264,5 @@
|
||||||
</ContentControl.DataTemplates>
|
</ContentControl.DataTemplates>
|
||||||
</ContentControl>
|
</ContentControl>
|
||||||
</Grid>
|
</Grid>
|
||||||
</v:LayoutableGrid>
|
</v:HistoriesLayout>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|
|
@ -1,24 +1,18 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
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.Interactivity;
|
|
||||||
using Avalonia.Media;
|
|
||||||
using Avalonia.Threading;
|
|
||||||
using Avalonia.VisualTree;
|
using Avalonia.VisualTree;
|
||||||
|
|
||||||
namespace SourceGit.Views
|
namespace SourceGit.Views
|
||||||
{
|
{
|
||||||
public class LayoutableGrid : Grid
|
public class HistoriesLayout : Grid
|
||||||
{
|
{
|
||||||
public static readonly StyledProperty<bool> UseHorizontalProperty =
|
public static readonly StyledProperty<bool> UseHorizontalProperty =
|
||||||
AvaloniaProperty.Register<LayoutableGrid, bool>(nameof(UseHorizontal));
|
AvaloniaProperty.Register<HistoriesLayout, bool>(nameof(UseHorizontal));
|
||||||
|
|
||||||
public bool UseHorizontal
|
public bool UseHorizontal
|
||||||
{
|
{
|
||||||
|
@ -28,17 +22,20 @@ namespace SourceGit.Views
|
||||||
|
|
||||||
protected override Type StyleKeyOverride => typeof(Grid);
|
protected override Type StyleKeyOverride => typeof(Grid);
|
||||||
|
|
||||||
static LayoutableGrid()
|
|
||||||
{
|
|
||||||
UseHorizontalProperty.Changed.AddClassHandler<LayoutableGrid>((o, _) => o.RefreshLayout());
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void ApplyTemplate()
|
public override void ApplyTemplate()
|
||||||
{
|
{
|
||||||
base.ApplyTemplate();
|
base.ApplyTemplate();
|
||||||
RefreshLayout();
|
RefreshLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||||
|
{
|
||||||
|
base.OnPropertyChanged(change);
|
||||||
|
|
||||||
|
if (change.Property == UseHorizontalProperty)
|
||||||
|
RefreshLayout();
|
||||||
|
}
|
||||||
|
|
||||||
private void RefreshLayout()
|
private void RefreshLayout()
|
||||||
{
|
{
|
||||||
if (UseHorizontal)
|
if (UseHorizontal)
|
||||||
|
@ -74,639 +71,6 @@ namespace SourceGit.Views
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CommitStatusIndicator : Control
|
|
||||||
{
|
|
||||||
public static readonly StyledProperty<Models.Branch> CurrentBranchProperty =
|
|
||||||
AvaloniaProperty.Register<CommitStatusIndicator, Models.Branch>(nameof(CurrentBranch));
|
|
||||||
|
|
||||||
public Models.Branch CurrentBranch
|
|
||||||
{
|
|
||||||
get => GetValue(CurrentBranchProperty);
|
|
||||||
set => SetValue(CurrentBranchProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static readonly StyledProperty<IBrush> AheadBrushProperty =
|
|
||||||
AvaloniaProperty.Register<CommitStatusIndicator, IBrush>(nameof(AheadBrush));
|
|
||||||
|
|
||||||
public IBrush AheadBrush
|
|
||||||
{
|
|
||||||
get => GetValue(AheadBrushProperty);
|
|
||||||
set => SetValue(AheadBrushProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static readonly StyledProperty<IBrush> BehindBrushProperty =
|
|
||||||
AvaloniaProperty.Register<CommitStatusIndicator, IBrush>(nameof(BehindBrush));
|
|
||||||
|
|
||||||
public IBrush BehindBrush
|
|
||||||
{
|
|
||||||
get => GetValue(BehindBrushProperty);
|
|
||||||
set => SetValue(BehindBrushProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
enum Status
|
|
||||||
{
|
|
||||||
Normal,
|
|
||||||
Ahead,
|
|
||||||
Behind,
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Render(DrawingContext context)
|
|
||||||
{
|
|
||||||
if (_status == Status.Normal)
|
|
||||||
return;
|
|
||||||
|
|
||||||
context.DrawEllipse(_status == Status.Ahead ? AheadBrush : BehindBrush, null, new Rect(0, 0, 5, 5));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Size MeasureOverride(Size availableSize)
|
|
||||||
{
|
|
||||||
if (DataContext is Models.Commit commit && CurrentBranch is not null)
|
|
||||||
{
|
|
||||||
var sha = commit.SHA;
|
|
||||||
var track = CurrentBranch.TrackStatus;
|
|
||||||
|
|
||||||
if (track.Ahead.Contains(sha))
|
|
||||||
_status = Status.Ahead;
|
|
||||||
else if (track.Behind.Contains(sha))
|
|
||||||
_status = Status.Behind;
|
|
||||||
else
|
|
||||||
_status = Status.Normal;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_status = Status.Normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
return _status == Status.Normal ? new Size(0, 0) : new Size(9, 5);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnDataContextChanged(EventArgs e)
|
|
||||||
{
|
|
||||||
base.OnDataContextChanged(e);
|
|
||||||
InvalidateMeasure();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
|
||||||
{
|
|
||||||
base.OnPropertyChanged(change);
|
|
||||||
if (change.Property == CurrentBranchProperty)
|
|
||||||
InvalidateMeasure();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Status _status = Status.Normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
public partial class CommitSubjectPresenter : TextBlock
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Type StyleKeyOverride => typeof(TextBlock);
|
|
||||||
|
|
||||||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
|
||||||
{
|
|
||||||
base.OnPropertyChanged(change);
|
|
||||||
|
|
||||||
if (change.Property == SubjectProperty || change.Property == IssueTrackerRulesProperty)
|
|
||||||
{
|
|
||||||
Inlines!.Clear();
|
|
||||||
_matches = null;
|
|
||||||
ClearHoveredIssueLink();
|
|
||||||
|
|
||||||
var subject = Subject;
|
|
||||||
if (string.IsNullOrEmpty(subject))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var keywordMatch = REG_KEYWORD_FORMAT1().Match(subject);
|
|
||||||
if (!keywordMatch.Success)
|
|
||||||
keywordMatch = REG_KEYWORD_FORMAT2().Match(subject);
|
|
||||||
|
|
||||||
var rules = IssueTrackerRules ?? [];
|
|
||||||
var matches = new List<Models.Hyperlink>();
|
|
||||||
foreach (var rule in rules)
|
|
||||||
rule.Matches(matches, subject);
|
|
||||||
|
|
||||||
if (matches.Count == 0)
|
|
||||||
{
|
|
||||||
if (keywordMatch.Success)
|
|
||||||
{
|
|
||||||
Inlines.Add(new Run(subject.Substring(0, keywordMatch.Length)) { FontWeight = FontWeight.Bold });
|
|
||||||
Inlines.Add(new Run(subject.Substring(keywordMatch.Length)));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Inlines.Add(new Run(subject));
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnPointerMoved(PointerEventArgs e)
|
|
||||||
{
|
|
||||||
base.OnPointerMoved(e);
|
|
||||||
|
|
||||||
if (_matches != null)
|
|
||||||
{
|
|
||||||
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 y = Math.Min(Math.Max(point.Y, 0), Math.Max(TextLayout.Height, 0));
|
|
||||||
point = new Point(x, y);
|
|
||||||
|
|
||||||
var textPosition = TextLayout.HitTestPoint(point).TextPosition;
|
|
||||||
foreach (var match in _matches)
|
|
||||||
{
|
|
||||||
if (!match.Intersect(textPosition, 1))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (match == _lastHover)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_lastHover = match;
|
|
||||||
SetCurrentValue(CursorProperty, Cursor.Parse("Hand"));
|
|
||||||
ToolTip.SetTip(this, match.Link);
|
|
||||||
ToolTip.SetIsOpen(this, true);
|
|
||||||
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 ClearHoveredIssueLink()
|
|
||||||
{
|
|
||||||
if (_lastHover != null)
|
|
||||||
{
|
|
||||||
ToolTip.SetTip(this, null);
|
|
||||||
SetCurrentValue(CursorProperty, Cursor.Parse("Arrow"));
|
|
||||||
_lastHover = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[GeneratedRegex(@"^\[[\w\s]+\]")]
|
|
||||||
private static partial Regex REG_KEYWORD_FORMAT1();
|
|
||||||
|
|
||||||
[GeneratedRegex(@"^\S+([\<\(][\w\s_\-\*,]+[\>\)])?\!?\s?:\s")]
|
|
||||||
private static partial Regex REG_KEYWORD_FORMAT2();
|
|
||||||
|
|
||||||
private List<Models.Hyperlink> _matches = null;
|
|
||||||
private Models.Hyperlink _lastHover = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class CommitTimeTextBlock : TextBlock
|
|
||||||
{
|
|
||||||
public static readonly StyledProperty<bool> ShowAsDateTimeProperty =
|
|
||||||
AvaloniaProperty.Register<CommitTimeTextBlock, bool>(nameof(ShowAsDateTime), true);
|
|
||||||
|
|
||||||
public bool ShowAsDateTime
|
|
||||||
{
|
|
||||||
get => GetValue(ShowAsDateTimeProperty);
|
|
||||||
set => SetValue(ShowAsDateTimeProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static readonly StyledProperty<int> DateTimeFormatProperty =
|
|
||||||
AvaloniaProperty.Register<CommitTimeTextBlock, int>(nameof(DateTimeFormat), 0);
|
|
||||||
|
|
||||||
public int DateTimeFormat
|
|
||||||
{
|
|
||||||
get => GetValue(DateTimeFormatProperty);
|
|
||||||
set => SetValue(DateTimeFormatProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static readonly StyledProperty<bool> UseAuthorTimeProperty =
|
|
||||||
AvaloniaProperty.Register<CommitTimeTextBlock, bool>(nameof(UseAuthorTime), true);
|
|
||||||
|
|
||||||
public bool UseAuthorTime
|
|
||||||
{
|
|
||||||
get => GetValue(UseAuthorTimeProperty);
|
|
||||||
set => SetValue(UseAuthorTimeProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Type StyleKeyOverride => typeof(TextBlock);
|
|
||||||
|
|
||||||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
|
||||||
{
|
|
||||||
base.OnPropertyChanged(change);
|
|
||||||
|
|
||||||
if (change.Property == UseAuthorTimeProperty)
|
|
||||||
{
|
|
||||||
SetCurrentValue(TextProperty, GetDisplayText());
|
|
||||||
}
|
|
||||||
else if (change.Property == ShowAsDateTimeProperty)
|
|
||||||
{
|
|
||||||
SetCurrentValue(TextProperty, GetDisplayText());
|
|
||||||
|
|
||||||
if (ShowAsDateTime)
|
|
||||||
StopTimer();
|
|
||||||
else
|
|
||||||
StartTimer();
|
|
||||||
}
|
|
||||||
else if (change.Property == DateTimeFormatProperty)
|
|
||||||
{
|
|
||||||
if (ShowAsDateTime)
|
|
||||||
SetCurrentValue(TextProperty, GetDisplayText());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnLoaded(RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
base.OnLoaded(e);
|
|
||||||
|
|
||||||
if (!ShowAsDateTime)
|
|
||||||
StartTimer();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnUnloaded(RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
base.OnUnloaded(e);
|
|
||||||
StopTimer();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnDataContextChanged(EventArgs e)
|
|
||||||
{
|
|
||||||
base.OnDataContextChanged(e);
|
|
||||||
SetCurrentValue(TextProperty, GetDisplayText());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void StartTimer()
|
|
||||||
{
|
|
||||||
if (_refreshTimer != null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_refreshTimer = DispatcherTimer.Run(() =>
|
|
||||||
{
|
|
||||||
Dispatcher.UIThread.Invoke(() =>
|
|
||||||
{
|
|
||||||
var text = GetDisplayText();
|
|
||||||
if (!text.Equals(Text, StringComparison.Ordinal))
|
|
||||||
Text = text;
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}, TimeSpan.FromSeconds(10));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void StopTimer()
|
|
||||||
{
|
|
||||||
if (_refreshTimer != null)
|
|
||||||
{
|
|
||||||
_refreshTimer.Dispose();
|
|
||||||
_refreshTimer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetDisplayText()
|
|
||||||
{
|
|
||||||
var commit = DataContext as Models.Commit;
|
|
||||||
if (commit == null)
|
|
||||||
return string.Empty;
|
|
||||||
|
|
||||||
if (ShowAsDateTime)
|
|
||||||
return UseAuthorTime ? commit.AuthorTimeStr : commit.CommitterTimeStr;
|
|
||||||
|
|
||||||
var timestamp = UseAuthorTime ? commit.AuthorTime : commit.CommitterTime;
|
|
||||||
var now = DateTime.Now;
|
|
||||||
var localTime = DateTime.UnixEpoch.AddSeconds(timestamp).ToLocalTime();
|
|
||||||
var span = now - localTime;
|
|
||||||
if (span.TotalMinutes < 1)
|
|
||||||
return App.Text("Period.JustNow");
|
|
||||||
|
|
||||||
if (span.TotalHours < 1)
|
|
||||||
return App.Text("Period.MinutesAgo", (int)span.TotalMinutes);
|
|
||||||
|
|
||||||
if (span.TotalDays < 1)
|
|
||||||
return App.Text("Period.HoursAgo", (int)span.TotalHours);
|
|
||||||
|
|
||||||
var lastDay = now.AddDays(-1).Date;
|
|
||||||
if (localTime >= lastDay)
|
|
||||||
return App.Text("Period.Yesterday");
|
|
||||||
|
|
||||||
if ((localTime.Year == now.Year && localTime.Month == now.Month) || span.TotalDays < 28)
|
|
||||||
{
|
|
||||||
var diffDay = now.Date - localTime.Date;
|
|
||||||
return App.Text("Period.DaysAgo", (int)diffDay.TotalDays);
|
|
||||||
}
|
|
||||||
|
|
||||||
var lastMonth = now.AddMonths(-1).Date;
|
|
||||||
if (localTime.Year == lastMonth.Year && localTime.Month == lastMonth.Month)
|
|
||||||
return App.Text("Period.LastMonth");
|
|
||||||
|
|
||||||
if (localTime.Year == now.Year || localTime > now.AddMonths(-11))
|
|
||||||
{
|
|
||||||
var diffMonth = (12 + now.Month - localTime.Month) % 12;
|
|
||||||
return App.Text("Period.MonthsAgo", diffMonth);
|
|
||||||
}
|
|
||||||
|
|
||||||
var diffYear = now.Year - localTime.Year;
|
|
||||||
if (diffYear == 1)
|
|
||||||
return App.Text("Period.LastYear");
|
|
||||||
|
|
||||||
return App.Text("Period.YearsAgo", diffYear);
|
|
||||||
}
|
|
||||||
|
|
||||||
private IDisposable _refreshTimer = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class CommitGraph : Control
|
|
||||||
{
|
|
||||||
public static readonly StyledProperty<Models.CommitGraph> GraphProperty =
|
|
||||||
AvaloniaProperty.Register<CommitGraph, Models.CommitGraph>(nameof(Graph));
|
|
||||||
|
|
||||||
public Models.CommitGraph Graph
|
|
||||||
{
|
|
||||||
get => GetValue(GraphProperty);
|
|
||||||
set => SetValue(GraphProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static readonly StyledProperty<IBrush> DotBrushProperty =
|
|
||||||
AvaloniaProperty.Register<CommitGraph, IBrush>(nameof(DotBrush), Brushes.Transparent);
|
|
||||||
|
|
||||||
public IBrush DotBrush
|
|
||||||
{
|
|
||||||
get => GetValue(DotBrushProperty);
|
|
||||||
set => SetValue(DotBrushProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static readonly StyledProperty<bool> OnlyHighlightCurrentBranchProperty =
|
|
||||||
AvaloniaProperty.Register<CommitGraph, bool>(nameof(OnlyHighlightCurrentBranch), true);
|
|
||||||
|
|
||||||
public bool OnlyHighlightCurrentBranch
|
|
||||||
{
|
|
||||||
get => GetValue(OnlyHighlightCurrentBranchProperty);
|
|
||||||
set => SetValue(OnlyHighlightCurrentBranchProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
static CommitGraph()
|
|
||||||
{
|
|
||||||
AffectsRender<CommitGraph>(GraphProperty, DotBrushProperty, OnlyHighlightCurrentBranchProperty);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Render(DrawingContext context)
|
|
||||||
{
|
|
||||||
base.Render(context);
|
|
||||||
|
|
||||||
var graph = Graph;
|
|
||||||
if (graph == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var histories = this.FindAncestorOfType<Histories>();
|
|
||||||
if (histories == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var list = histories.CommitListContainer;
|
|
||||||
if (list == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Calculate drawing area.
|
|
||||||
double width = Bounds.Width - 273 - histories.AuthorNameColumnWidth.Value;
|
|
||||||
double height = Bounds.Height;
|
|
||||||
double startY = list.Scroll?.Offset.Y ?? 0;
|
|
||||||
double endY = startY + height + 28;
|
|
||||||
|
|
||||||
// Apply scroll offset and clip.
|
|
||||||
using (context.PushClip(new Rect(0, 0, width, height)))
|
|
||||||
using (context.PushTransform(Matrix.CreateTranslation(0, -startY)))
|
|
||||||
{
|
|
||||||
// Draw contents
|
|
||||||
DrawCurves(context, graph, startY, endY);
|
|
||||||
DrawAnchors(context, graph, startY, endY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawCurves(DrawingContext context, Models.CommitGraph graph, double top, double bottom)
|
|
||||||
{
|
|
||||||
var grayedPen = new Pen(new SolidColorBrush(Colors.Gray, 0.4), Models.CommitGraph.Pens[0].Thickness);
|
|
||||||
var onlyHighlightCurrentBranch = OnlyHighlightCurrentBranch;
|
|
||||||
|
|
||||||
if (onlyHighlightCurrentBranch)
|
|
||||||
{
|
|
||||||
foreach (var link in graph.Links)
|
|
||||||
{
|
|
||||||
if (link.IsMerged)
|
|
||||||
continue;
|
|
||||||
if (link.End.Y < top)
|
|
||||||
continue;
|
|
||||||
if (link.Start.Y > bottom)
|
|
||||||
break;
|
|
||||||
|
|
||||||
var geo = new StreamGeometry();
|
|
||||||
using (var ctx = geo.Open())
|
|
||||||
{
|
|
||||||
ctx.BeginFigure(link.Start, false);
|
|
||||||
ctx.QuadraticBezierTo(link.Control, link.End);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.DrawGeometry(null, grayedPen, geo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var line in graph.Paths)
|
|
||||||
{
|
|
||||||
var last = line.Points[0];
|
|
||||||
var size = line.Points.Count;
|
|
||||||
|
|
||||||
if (line.Points[size - 1].Y < top)
|
|
||||||
continue;
|
|
||||||
if (last.Y > bottom)
|
|
||||||
break;
|
|
||||||
|
|
||||||
var geo = new StreamGeometry();
|
|
||||||
var pen = Models.CommitGraph.Pens[line.Color];
|
|
||||||
|
|
||||||
using (var ctx = geo.Open())
|
|
||||||
{
|
|
||||||
var started = false;
|
|
||||||
var ended = false;
|
|
||||||
for (int i = 1; i < size; i++)
|
|
||||||
{
|
|
||||||
var cur = line.Points[i];
|
|
||||||
if (cur.Y < top)
|
|
||||||
{
|
|
||||||
last = cur;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!started)
|
|
||||||
{
|
|
||||||
ctx.BeginFigure(last, false);
|
|
||||||
started = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cur.Y > bottom)
|
|
||||||
{
|
|
||||||
cur = new Point(cur.X, bottom);
|
|
||||||
ended = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cur.X > last.X)
|
|
||||||
{
|
|
||||||
ctx.QuadraticBezierTo(new Point(cur.X, last.Y), cur);
|
|
||||||
}
|
|
||||||
else if (cur.X < last.X)
|
|
||||||
{
|
|
||||||
if (i < size - 1)
|
|
||||||
{
|
|
||||||
var midY = (last.Y + cur.Y) / 2;
|
|
||||||
ctx.CubicBezierTo(new Point(last.X, midY + 4), new Point(cur.X, midY - 4), cur);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ctx.QuadraticBezierTo(new Point(last.X, cur.Y), cur);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ctx.LineTo(cur);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ended)
|
|
||||||
break;
|
|
||||||
last = cur;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!line.IsMerged && onlyHighlightCurrentBranch)
|
|
||||||
context.DrawGeometry(null, grayedPen, geo);
|
|
||||||
else
|
|
||||||
context.DrawGeometry(null, pen, geo);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var link in graph.Links)
|
|
||||||
{
|
|
||||||
if (onlyHighlightCurrentBranch && !link.IsMerged)
|
|
||||||
continue;
|
|
||||||
if (link.End.Y < top)
|
|
||||||
continue;
|
|
||||||
if (link.Start.Y > bottom)
|
|
||||||
break;
|
|
||||||
|
|
||||||
var geo = new StreamGeometry();
|
|
||||||
using (var ctx = geo.Open())
|
|
||||||
{
|
|
||||||
ctx.BeginFigure(link.Start, false);
|
|
||||||
ctx.QuadraticBezierTo(link.Control, link.End);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.DrawGeometry(null, Models.CommitGraph.Pens[link.Color], geo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawAnchors(DrawingContext context, Models.CommitGraph graph, double top, double bottom)
|
|
||||||
{
|
|
||||||
var dotFill = DotBrush;
|
|
||||||
var dotFillPen = new Pen(dotFill, 2);
|
|
||||||
var grayedPen = new Pen(Brushes.Gray, Models.CommitGraph.Pens[0].Thickness);
|
|
||||||
var onlyHighlightCurrentBranch = OnlyHighlightCurrentBranch;
|
|
||||||
|
|
||||||
foreach (var dot in graph.Dots)
|
|
||||||
{
|
|
||||||
if (dot.Center.Y < top)
|
|
||||||
continue;
|
|
||||||
if (dot.Center.Y > bottom)
|
|
||||||
break;
|
|
||||||
|
|
||||||
var pen = Models.CommitGraph.Pens[dot.Color];
|
|
||||||
if (!dot.IsMerged && onlyHighlightCurrentBranch)
|
|
||||||
pen = grayedPen;
|
|
||||||
|
|
||||||
switch (dot.Type)
|
|
||||||
{
|
|
||||||
case Models.CommitGraph.DotType.Head:
|
|
||||||
context.DrawEllipse(dotFill, pen, dot.Center, 6, 6);
|
|
||||||
context.DrawEllipse(pen.Brush, null, dot.Center, 3, 3);
|
|
||||||
break;
|
|
||||||
case Models.CommitGraph.DotType.Merge:
|
|
||||||
context.DrawEllipse(pen.Brush, null, dot.Center, 6, 6);
|
|
||||||
context.DrawLine(dotFillPen, new Point(dot.Center.X, dot.Center.Y - 3), new Point(dot.Center.X, dot.Center.Y + 3));
|
|
||||||
context.DrawLine(dotFillPen, new Point(dot.Center.X - 3, dot.Center.Y), new Point(dot.Center.X + 3, dot.Center.Y));
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
context.DrawEllipse(dotFill, pen, dot.Center, 3, 3);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public partial class Histories : UserControl
|
public partial class Histories : UserControl
|
||||||
{
|
{
|
||||||
public static readonly StyledProperty<GridLength> AuthorNameColumnWidthProperty =
|
public static readonly StyledProperty<GridLength> AuthorNameColumnWidthProperty =
|
||||||
|
@ -754,36 +118,34 @@ namespace SourceGit.Views
|
||||||
set => SetValue(NavigationIdProperty, value);
|
set => SetValue(NavigationIdProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Histories()
|
|
||||||
{
|
|
||||||
NavigationIdProperty.Changed.AddClassHandler<Histories>((h, _) =>
|
|
||||||
{
|
|
||||||
if (h.DataContext == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Force scroll selected item (current head) into view. see issue #58
|
|
||||||
var list = h.CommitListContainer;
|
|
||||||
if (list != null && list.SelectedItems.Count == 1)
|
|
||||||
list.ScrollIntoView(list.SelectedIndex);
|
|
||||||
});
|
|
||||||
|
|
||||||
AuthorNameColumnWidthProperty.Changed.AddClassHandler<Histories>((h, _) =>
|
|
||||||
{
|
|
||||||
h.CommitGraph.InvalidateVisual();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public Histories()
|
public Histories()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||||
|
{
|
||||||
|
base.OnPropertyChanged(change);
|
||||||
|
|
||||||
|
if (change.Property == NavigationIdProperty)
|
||||||
|
{
|
||||||
|
if (DataContext is ViewModels.Histories)
|
||||||
|
{
|
||||||
|
var list = CommitListContainer;
|
||||||
|
if (list != null && list.SelectedItems.Count == 1)
|
||||||
|
list.ScrollIntoView(list.SelectedIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void OnCommitListLayoutUpdated(object _1, EventArgs _2)
|
private void OnCommitListLayoutUpdated(object _1, EventArgs _2)
|
||||||
{
|
{
|
||||||
var y = CommitListContainer.Scroll?.Offset.Y ?? 0;
|
var y = CommitListContainer.Scroll?.Offset.Y ?? 0;
|
||||||
if (y != _lastScrollY)
|
var authorNameColumnWidth = AuthorNameColumnWidth.Value;
|
||||||
|
if (y != _lastScrollY || authorNameColumnWidth != _lastAuthorNameColumnWidth)
|
||||||
{
|
{
|
||||||
_lastScrollY = y;
|
_lastScrollY = y;
|
||||||
|
_lastAuthorNameColumnWidth = authorNameColumnWidth;
|
||||||
CommitGraph.InvalidateVisual();
|
CommitGraph.InvalidateVisual();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -863,5 +225,6 @@ namespace SourceGit.Views
|
||||||
}
|
}
|
||||||
|
|
||||||
private double _lastScrollY = 0;
|
private double _lastScrollY = 0;
|
||||||
|
private double _lastAuthorNameColumnWidth = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue