2024-03-17 18:37:06 -07:00
|
|
|
using System;
|
|
|
|
|
2024-02-05 23:08:37 -08:00
|
|
|
using Avalonia;
|
|
|
|
using Avalonia.Controls;
|
2024-02-18 23:30:10 -08:00
|
|
|
using Avalonia.Controls.Primitives;
|
2024-04-07 06:19:02 -07:00
|
|
|
using Avalonia.Data;
|
2024-02-05 23:08:37 -08:00
|
|
|
using Avalonia.Media;
|
2024-02-18 23:30:10 -08:00
|
|
|
using Avalonia.VisualTree;
|
2024-02-05 23:08:37 -08:00
|
|
|
|
2024-03-17 18:37:06 -07:00
|
|
|
namespace SourceGit.Views
|
|
|
|
{
|
|
|
|
public class LayoutableGrid : Grid
|
|
|
|
{
|
2024-02-05 23:08:37 -08:00
|
|
|
public static readonly StyledProperty<bool> UseHorizontalProperty =
|
|
|
|
AvaloniaProperty.Register<LayoutableGrid, bool>(nameof(UseHorizontal), false);
|
|
|
|
|
2024-03-17 18:37:06 -07:00
|
|
|
public bool UseHorizontal
|
|
|
|
{
|
2024-02-05 23:08:37 -08:00
|
|
|
get => GetValue(UseHorizontalProperty);
|
|
|
|
set => SetValue(UseHorizontalProperty, value);
|
|
|
|
}
|
|
|
|
|
|
|
|
protected override Type StyleKeyOverride => typeof(Grid);
|
|
|
|
|
2024-03-17 18:37:06 -07:00
|
|
|
static LayoutableGrid()
|
|
|
|
{
|
2024-02-05 23:08:37 -08:00
|
|
|
UseHorizontalProperty.Changed.AddClassHandler<LayoutableGrid>((o, _) => o.RefreshLayout());
|
|
|
|
}
|
|
|
|
|
2024-03-17 18:37:06 -07:00
|
|
|
public override void ApplyTemplate()
|
|
|
|
{
|
2024-02-05 23:08:37 -08:00
|
|
|
base.ApplyTemplate();
|
|
|
|
RefreshLayout();
|
|
|
|
}
|
|
|
|
|
2024-03-17 18:37:06 -07:00
|
|
|
private void RefreshLayout()
|
|
|
|
{
|
|
|
|
if (UseHorizontal)
|
|
|
|
{
|
2024-02-05 23:08:37 -08:00
|
|
|
var rowSpan = RowDefinitions.Count;
|
2024-03-17 18:37:06 -07:00
|
|
|
for (int i = 0; i < Children.Count; i++)
|
|
|
|
{
|
2024-02-05 23:08:37 -08:00
|
|
|
var child = Children[i];
|
|
|
|
child.SetValue(RowProperty, 0);
|
|
|
|
child.SetValue(RowSpanProperty, rowSpan);
|
|
|
|
child.SetValue(ColumnProperty, i);
|
|
|
|
child.SetValue(ColumnSpanProperty, 1);
|
2024-03-26 00:18:34 -07:00
|
|
|
|
2024-03-31 01:54:29 -07:00
|
|
|
if (child is GridSplitter splitter)
|
|
|
|
splitter.BorderThickness = new Thickness(1, 0, 0, 0);
|
2024-02-05 23:08:37 -08:00
|
|
|
}
|
2024-03-17 18:37:06 -07:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2024-02-05 23:08:37 -08:00
|
|
|
var colSpan = ColumnDefinitions.Count;
|
2024-03-17 18:37:06 -07:00
|
|
|
for (int i = 0; i < Children.Count; i++)
|
|
|
|
{
|
2024-02-05 23:08:37 -08:00
|
|
|
var child = Children[i];
|
|
|
|
child.SetValue(RowProperty, i);
|
|
|
|
child.SetValue(RowSpanProperty, 1);
|
|
|
|
child.SetValue(ColumnProperty, 0);
|
|
|
|
child.SetValue(ColumnSpanProperty, colSpan);
|
2024-03-26 00:18:34 -07:00
|
|
|
|
2024-03-31 01:54:29 -07:00
|
|
|
if (child is GridSplitter splitter)
|
|
|
|
splitter.BorderThickness = new Thickness(0, 1, 0, 0);
|
2024-02-05 23:08:37 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-17 18:37:06 -07:00
|
|
|
public class CommitGraph : Control
|
|
|
|
{
|
2024-02-05 23:08:37 -08:00
|
|
|
public static readonly Pen[] Pens = [
|
|
|
|
new Pen(Brushes.Orange, 2),
|
|
|
|
new Pen(Brushes.ForestGreen, 2),
|
|
|
|
new Pen(Brushes.Gold, 2),
|
|
|
|
new Pen(Brushes.Magenta, 2),
|
|
|
|
new Pen(Brushes.Red, 2),
|
|
|
|
new Pen(Brushes.Gray, 2),
|
|
|
|
new Pen(Brushes.Turquoise, 2),
|
|
|
|
new Pen(Brushes.Olive, 2),
|
|
|
|
];
|
|
|
|
|
|
|
|
public static readonly StyledProperty<Models.CommitGraph> GraphProperty =
|
|
|
|
AvaloniaProperty.Register<CommitGraph, Models.CommitGraph>(nameof(Graph));
|
|
|
|
|
2024-03-17 18:37:06 -07:00
|
|
|
public Models.CommitGraph Graph
|
|
|
|
{
|
2024-02-05 23:08:37 -08:00
|
|
|
get => GetValue(GraphProperty);
|
|
|
|
set => SetValue(GraphProperty, value);
|
|
|
|
}
|
|
|
|
|
|
|
|
public static readonly StyledProperty<DataGrid> BindingDataGridProperty =
|
|
|
|
AvaloniaProperty.Register<CommitGraph, DataGrid>(nameof(BindingDataGrid));
|
|
|
|
|
2024-03-17 18:37:06 -07:00
|
|
|
public DataGrid BindingDataGrid
|
|
|
|
{
|
2024-02-05 23:08:37 -08:00
|
|
|
get => GetValue(BindingDataGridProperty);
|
|
|
|
set => SetValue(BindingDataGridProperty, value);
|
|
|
|
}
|
|
|
|
|
2024-04-17 01:16:11 -07:00
|
|
|
public static readonly StyledProperty<IBrush> DotBrushProperty =
|
|
|
|
AvaloniaProperty.Register<CommitGraph, IBrush>(nameof(DotBrush), Brushes.Transparent);
|
|
|
|
|
|
|
|
public IBrush DotBrush
|
2024-03-17 18:37:06 -07:00
|
|
|
{
|
2024-04-17 01:16:11 -07:00
|
|
|
get => GetValue(DotBrushProperty);
|
|
|
|
set => SetValue(DotBrushProperty, value);
|
2024-02-05 23:08:37 -08:00
|
|
|
}
|
|
|
|
|
2024-04-17 01:16:11 -07:00
|
|
|
static CommitGraph()
|
2024-03-17 18:37:06 -07:00
|
|
|
{
|
2024-04-17 01:16:11 -07:00
|
|
|
AffectsRender<CommitGraph>(BindingDataGridProperty, GraphProperty, DotBrushProperty);
|
2024-02-05 23:08:37 -08:00
|
|
|
}
|
|
|
|
|
2024-03-17 18:37:06 -07:00
|
|
|
public override void Render(DrawingContext context)
|
|
|
|
{
|
2024-02-05 23:08:37 -08:00
|
|
|
base.Render(context);
|
|
|
|
|
2024-02-18 23:30:10 -08:00
|
|
|
var graph = Graph;
|
|
|
|
var grid = BindingDataGrid;
|
2024-03-31 01:54:29 -07:00
|
|
|
if (graph == null || grid == null)
|
|
|
|
return;
|
2024-02-18 23:30:10 -08:00
|
|
|
|
|
|
|
var rowsPresenter = grid.FindDescendantOfType<DataGridRowsPresenter>();
|
2024-03-31 01:54:29 -07:00
|
|
|
if (rowsPresenter == null)
|
|
|
|
return;
|
2024-02-18 23:30:10 -08:00
|
|
|
|
|
|
|
// Find the content display offset Y of binding DataGrid.
|
|
|
|
double rowHeight = grid.RowHeight;
|
|
|
|
double startY = 0;
|
2024-03-17 18:37:06 -07:00
|
|
|
foreach (var child in rowsPresenter.Children)
|
|
|
|
{
|
2024-02-18 23:30:10 -08:00
|
|
|
var row = child as DataGridRow;
|
2024-03-17 18:37:06 -07:00
|
|
|
if (row.IsVisible && row.Bounds.Top <= 0 && row.Bounds.Top > -rowHeight)
|
|
|
|
{
|
2024-02-18 23:30:10 -08:00
|
|
|
var test = rowHeight * row.GetIndex() - row.Bounds.Top;
|
2024-03-31 01:54:29 -07:00
|
|
|
if (startY < test)
|
|
|
|
startY = test;
|
2024-02-18 23:30:10 -08:00
|
|
|
}
|
|
|
|
}
|
2024-02-05 23:08:37 -08:00
|
|
|
|
|
|
|
// Apply scroll offset.
|
2024-02-18 23:30:10 -08:00
|
|
|
context.PushClip(new Rect(Bounds.Left, Bounds.Top, grid.Columns[0].ActualWidth, Bounds.Height));
|
|
|
|
context.PushTransform(Matrix.CreateTranslation(0, -startY));
|
2024-02-05 23:08:37 -08:00
|
|
|
|
|
|
|
// Calculate bounds.
|
2024-02-18 23:30:10 -08:00
|
|
|
var top = startY;
|
|
|
|
var bottom = startY + grid.Bounds.Height + rowHeight * 2;
|
2024-02-05 23:08:37 -08:00
|
|
|
|
|
|
|
// Draw all curves
|
|
|
|
DrawCurves(context, top, bottom);
|
|
|
|
|
|
|
|
// Draw connect dots
|
2024-04-17 01:16:11 -07:00
|
|
|
IBrush dotFill = DotBrush;
|
2024-03-17 18:37:06 -07:00
|
|
|
foreach (var dot in graph.Dots)
|
|
|
|
{
|
2024-03-31 01:54:29 -07:00
|
|
|
if (dot.Center.Y < top)
|
|
|
|
continue;
|
|
|
|
if (dot.Center.Y > bottom)
|
|
|
|
break;
|
2024-02-05 23:08:37 -08:00
|
|
|
|
|
|
|
context.DrawEllipse(dotFill, Pens[dot.Color], dot.Center, 3, 3);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-17 18:37:06 -07:00
|
|
|
private void DrawCurves(DrawingContext context, double top, double bottom)
|
|
|
|
{
|
|
|
|
foreach (var line in Graph.Paths)
|
|
|
|
{
|
2024-02-05 23:08:37 -08:00
|
|
|
var last = line.Points[0];
|
|
|
|
var size = line.Points.Count;
|
|
|
|
|
2024-03-31 01:54:29 -07:00
|
|
|
if (line.Points[size - 1].Y < top)
|
|
|
|
continue;
|
|
|
|
if (last.Y > bottom)
|
|
|
|
continue;
|
2024-02-05 23:08:37 -08:00
|
|
|
|
|
|
|
var geo = new StreamGeometry();
|
|
|
|
var pen = Pens[line.Color];
|
2024-03-17 18:37:06 -07:00
|
|
|
using (var ctx = geo.Open())
|
|
|
|
{
|
2024-02-05 23:08:37 -08:00
|
|
|
var started = false;
|
|
|
|
var ended = false;
|
2024-03-17 18:37:06 -07:00
|
|
|
for (int i = 1; i < size; i++)
|
|
|
|
{
|
2024-02-05 23:08:37 -08:00
|
|
|
var cur = line.Points[i];
|
2024-03-17 18:37:06 -07:00
|
|
|
if (cur.Y < top)
|
|
|
|
{
|
2024-02-05 23:08:37 -08:00
|
|
|
last = cur;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2024-03-17 18:37:06 -07:00
|
|
|
if (!started)
|
|
|
|
{
|
2024-02-05 23:08:37 -08:00
|
|
|
ctx.BeginFigure(last, false);
|
|
|
|
started = true;
|
|
|
|
}
|
|
|
|
|
2024-03-17 18:37:06 -07:00
|
|
|
if (cur.Y > bottom)
|
|
|
|
{
|
2024-02-05 23:08:37 -08:00
|
|
|
cur = new Point(cur.X, bottom);
|
|
|
|
ended = true;
|
|
|
|
}
|
|
|
|
|
2024-03-17 18:37:06 -07:00
|
|
|
if (cur.X > last.X)
|
|
|
|
{
|
2024-02-05 23:08:37 -08:00
|
|
|
ctx.QuadraticBezierTo(new Point(cur.X, last.Y), cur);
|
2024-03-17 18:37:06 -07:00
|
|
|
}
|
|
|
|
else if (cur.X < last.X)
|
|
|
|
{
|
|
|
|
if (i < size - 1)
|
|
|
|
{
|
2024-02-05 23:08:37 -08:00
|
|
|
var midY = (last.Y + cur.Y) / 2;
|
2024-04-03 00:32:23 -07:00
|
|
|
ctx.CubicBezierTo(new Point(last.X, midY + 2), new Point(cur.X, midY - 2), cur);
|
2024-03-17 18:37:06 -07:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2024-02-05 23:08:37 -08:00
|
|
|
ctx.QuadraticBezierTo(new Point(last.X, cur.Y), cur);
|
|
|
|
}
|
2024-03-17 18:37:06 -07:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2024-02-05 23:08:37 -08:00
|
|
|
ctx.LineTo(cur);
|
|
|
|
}
|
|
|
|
|
2024-03-31 01:54:29 -07:00
|
|
|
if (ended)
|
|
|
|
break;
|
2024-02-05 23:08:37 -08:00
|
|
|
last = cur;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
context.DrawGeometry(null, pen, geo);
|
|
|
|
}
|
|
|
|
|
2024-03-17 18:37:06 -07:00
|
|
|
foreach (var link in Graph.Links)
|
|
|
|
{
|
2024-03-31 01:54:29 -07:00
|
|
|
if (link.End.Y < top)
|
|
|
|
continue;
|
|
|
|
if (link.Start.Y > bottom)
|
|
|
|
break;
|
2024-02-05 23:08:37 -08:00
|
|
|
|
|
|
|
var geo = new StreamGeometry();
|
2024-03-17 18:37:06 -07:00
|
|
|
using (var ctx = geo.Open())
|
|
|
|
{
|
2024-02-05 23:08:37 -08:00
|
|
|
ctx.BeginFigure(link.Start, false);
|
|
|
|
ctx.QuadraticBezierTo(link.Control, link.End);
|
|
|
|
}
|
|
|
|
|
|
|
|
context.DrawGeometry(null, Pens[link.Color], geo);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-17 18:37:06 -07:00
|
|
|
public partial class Histories : UserControl
|
|
|
|
{
|
2024-04-07 06:19:02 -07:00
|
|
|
public static readonly StyledProperty<long> NavigationIdProperty =
|
|
|
|
AvaloniaProperty.Register<Histories, long>(nameof(NavigationId), 0);
|
|
|
|
|
|
|
|
public long NavigationId
|
|
|
|
{
|
|
|
|
get => GetValue(NavigationIdProperty);
|
|
|
|
set => SetValue(NavigationIdProperty, value);
|
|
|
|
}
|
|
|
|
|
|
|
|
static Histories()
|
|
|
|
{
|
|
|
|
NavigationIdProperty.Changed.AddClassHandler<Histories>((h, _) =>
|
|
|
|
{
|
|
|
|
// Force scroll selected item (current head) into view. see issue #58
|
|
|
|
var datagrid = h.commitDataGrid;
|
|
|
|
if (datagrid != null && datagrid.SelectedItems.Count == 1)
|
|
|
|
datagrid.ScrollIntoView(datagrid.SelectedItems[0], null);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-03-17 18:37:06 -07:00
|
|
|
public Histories()
|
|
|
|
{
|
2024-02-19 01:40:36 -08:00
|
|
|
InitializeComponent();
|
2024-04-07 06:19:02 -07:00
|
|
|
|
|
|
|
this.Bind(NavigationIdProperty, new Binding("NavigationId", BindingMode.OneWay));
|
2024-02-05 23:08:37 -08:00
|
|
|
}
|
|
|
|
|
2024-03-17 18:37:06 -07:00
|
|
|
private void OnCommitDataGridLayoutUpdated(object sender, EventArgs e)
|
|
|
|
{
|
2024-02-18 23:30:10 -08:00
|
|
|
commitGraph.InvalidateVisual();
|
2024-02-05 23:08:37 -08:00
|
|
|
}
|
|
|
|
|
2024-03-17 18:37:06 -07:00
|
|
|
private void OnCommitDataGridSelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
|
|
{
|
|
|
|
if (DataContext is ViewModels.Histories histories)
|
|
|
|
{
|
2024-02-05 23:08:37 -08:00
|
|
|
histories.Select(commitDataGrid.SelectedItems);
|
|
|
|
}
|
|
|
|
e.Handled = true;
|
|
|
|
}
|
|
|
|
|
2024-03-17 18:37:06 -07:00
|
|
|
private void OnCommitDataGridContextRequested(object sender, ContextRequestedEventArgs e)
|
|
|
|
{
|
|
|
|
if (DataContext is ViewModels.Histories histories)
|
|
|
|
{
|
2024-02-05 23:08:37 -08:00
|
|
|
var menu = histories.MakeContextMenu();
|
2024-05-23 06:24:22 -07:00
|
|
|
(sender as Control)?.OpenContextMenu(menu);
|
2024-02-05 23:08:37 -08:00
|
|
|
}
|
|
|
|
e.Handled = true;
|
|
|
|
}
|
|
|
|
}
|
2024-03-31 01:54:29 -07:00
|
|
|
}
|