feature<*>: use DataGrid instead of RichTextBox to improve performance

This commit is contained in:
leo 2020-11-24 17:14:44 +08:00
parent 544d819c96
commit cbdebee4c2
9 changed files with 594 additions and 848 deletions

View file

@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
namespace SourceGit.Git { namespace SourceGit.Git {
@ -8,9 +8,9 @@ namespace SourceGit.Git {
public class Blame { public class Blame {
/// <summary> /// <summary>
/// Block content. /// Line content.
/// </summary> /// </summary>
public class Block { public class Line {
public string CommitSHA { get; set; } public string CommitSHA { get; set; }
public string Author { get; set; } public string Author { get; set; }
public string Time { get; set; } public string Time { get; set; }
@ -18,18 +18,13 @@ namespace SourceGit.Git {
} }
/// <summary> /// <summary>
/// Blocks /// Lines
/// </summary> /// </summary>
public List<Block> Blocks { get; set; } = new List<Block>(); public List<Line> Lines { get; set; } = new List<Line>();
/// <summary> /// <summary>
/// Is binary file? /// Is binary file?
/// </summary> /// </summary>
public bool IsBinary { get; set; } = false; public bool IsBinary { get; set; } = false;
/// <summary>
/// Line count.
/// </summary>
public int LineCount { get; set; } = 0;
} }
} }

View file

@ -15,100 +15,36 @@ namespace SourceGit.Git {
/// Line mode. /// Line mode.
/// </summary> /// </summary>
public enum LineMode { public enum LineMode {
None,
Normal, Normal,
Indicator, Indicator,
Empty,
Added, Added,
Deleted, Deleted,
} }
/// <summary> /// <summary>
/// Side /// Line change.
/// </summary> /// </summary>
public enum Side { public class LineChange {
Left,
Right,
Both,
}
/// <summary>
/// Block
/// </summary>
public class Block {
public Side Side = Side.Both;
public LineMode Mode = LineMode.Normal; public LineMode Mode = LineMode.Normal;
public int LeftStart = 0; public string Content = "";
public int RightStart = 0; public string OldLine = "";
public int Count = 0; public string NewLine = "";
public StringBuilder Builder = new StringBuilder();
public bool IsLeftDelete => Side == Side.Left && Mode == LineMode.Deleted; public LineChange(LineMode mode, string content, string oldLine = "", string newLine = "") {
public bool IsRightAdded => Side == Side.Right && Mode == LineMode.Added; Mode = mode;
public bool IsBothSideNormal => Side == Side.Both && Mode == LineMode.Normal; Content = content;
public bool CanShowNumber => Mode != LineMode.Indicator && Mode != LineMode.Empty; OldLine = oldLine;
NewLine = newLine;
public void Append(string data) {
if (Count > 0) Builder.AppendLine();
Builder.Append(data);
Count++;
} }
} }
/// <summary> /// <summary>
/// Text file change. /// Text change.
/// </summary> /// </summary>
public class TextChange { public class TextChange {
public bool IsValid = false; public List<LineChange> Lines = new List<LineChange>();
public bool IsBinary = false; public bool IsBinary = false;
public List<Block> Blocks = new List<Block>();
public int LeftLineCount = 0;
public int RightLineCount = 0;
public void SetBinary() {
IsValid = true;
IsBinary = true;
}
public void Add(Block b) {
if (b.Count == 0) return;
switch (b.Side) {
case Side.Left:
LeftLineCount += b.Count;
break;
case Side.Right:
RightLineCount += b.Count;
break;
default:
LeftLineCount += b.Count;
RightLineCount += b.Count;
break;
}
Blocks.Add(b);
}
public void Fit() {
if (LeftLineCount > RightLineCount) {
var b = new Block();
b.Side = Side.Right;
b.Mode = LineMode.Empty;
var delta = LeftLineCount - RightLineCount;
for (int i = 0; i < delta; i++) b.Append("");
Add(b);
} else if (LeftLineCount < RightLineCount) {
var b = new Block();
b.Side = Side.Left;
b.Mode = LineMode.Empty;
var delta = RightLineCount - LeftLineCount;
for (int i = 0; i < delta; i++) b.Append("");
Add(b);
}
}
} }
/// <summary> /// <summary>
@ -136,103 +72,49 @@ namespace SourceGit.Git {
/// <returns></returns> /// <returns></returns>
public static TextChange GetTextChange(Repository repo, string args) { public static TextChange GetTextChange(Repository repo, string args) {
var rs = new TextChange(); var rs = new TextChange();
var current = new Block(); var started = false;
var left = 0; var oldLine = 0;
var right = 0; var newLine = 0;
repo.RunCommand($"diff --ignore-cr-at-eol {args}", line => { repo.RunCommand($"diff --ignore-cr-at-eol {args}", line => {
if (rs.IsBinary) return; if (rs.IsBinary) return;
if (!rs.IsValid) { if (!started) {
var match = REG_INDICATOR.Match(line); var match = REG_INDICATOR.Match(line);
if (!match.Success) { if (!match.Success) {
if (line.StartsWith("Binary ")) rs.SetBinary(); if (line.StartsWith("Binary ")) rs.IsBinary = true;
return; return;
} }
rs.IsValid = true; started = true;
left = int.Parse(match.Groups[1].Value); oldLine = int.Parse(match.Groups[1].Value);
right = int.Parse(match.Groups[2].Value); newLine = int.Parse(match.Groups[2].Value);
current.Mode = LineMode.Indicator; rs.Lines.Add(new LineChange(LineMode.Indicator, line));
current.Append(line);
} else { } else {
if (line[0] == '-') { if (line[0] == '-') {
if (current.IsLeftDelete) { rs.Lines.Add(new LineChange(LineMode.Deleted, line.Substring(1), $"{oldLine}", ""));
current.Append(line.Substring(1)); oldLine++;
} else {
rs.Add(current);
current = new Block();
current.Side = Side.Left;
current.Mode = LineMode.Deleted;
current.LeftStart = left;
current.Append(line.Substring(1));
}
left++;
} else if (line[0] == '+') { } else if (line[0] == '+') {
if (current.IsRightAdded) { rs.Lines.Add(new LineChange(LineMode.Added, line.Substring(1), "", $"{newLine}"));
current.Append(line.Substring(1)); newLine++;
} else {
rs.Add(current);
current = new Block();
current.Side = Side.Right;
current.Mode = LineMode.Added;
current.RightStart = right;
current.Append(line.Substring(1));
}
right++;
} else if (line[0] == '\\') { } else if (line[0] == '\\') {
var tmp = new Block(); rs.Lines.Add(new LineChange(LineMode.Indicator, line.Substring(1)));
tmp.Side = current.Side;
tmp.Mode = LineMode.Indicator;
tmp.Append(line.Substring(1));
rs.Add(current);
rs.Add(tmp);
rs.Fit();
current = new Block();
current.LeftStart = left;
current.RightStart = right;
} else { } else {
var match = REG_INDICATOR.Match(line); var match = REG_INDICATOR.Match(line);
if (match.Success) { if (match.Success) {
rs.Add(current); oldLine = int.Parse(match.Groups[1].Value);
rs.Fit(); newLine = int.Parse(match.Groups[2].Value);
rs.Lines.Add(new LineChange(LineMode.Indicator, line));
left = int.Parse(match.Groups[1].Value);
right = int.Parse(match.Groups[2].Value);
current = new Block();
current.Mode = LineMode.Indicator;
current.Append(line);
} else { } else {
if (current.IsBothSideNormal) { rs.Lines.Add(new LineChange(LineMode.Normal, line.Substring(1), $"{oldLine}", $"{newLine}"));
current.Append(line.Substring(1)); oldLine++;
} else { newLine++;
rs.Add(current);
rs.Fit();
current = new Block();
current.LeftStart = left;
current.RightStart = right;
current.Append(line.Substring(1));
}
left++;
right++;
} }
} }
} }
}); });
rs.Add(current); if (rs.IsBinary) rs.Lines.Clear();
rs.Fit();
if (rs.IsBinary) rs.Blocks.Clear();
return rs; return rs;
} }

View file

@ -945,7 +945,6 @@ namespace SourceGit.Git {
public Blame BlameFile(string file, string revision) { public Blame BlameFile(string file, string revision) {
var regex = new Regex(@"^\^?([0-9a-f]+)\s+.*\((.*)\s+(\d+)\s+[\-\+]?\d+\s+\d+\) (.*)"); var regex = new Regex(@"^\^?([0-9a-f]+)\s+.*\((.*)\s+(\d+)\s+[\-\+]?\d+\s+\d+\) (.*)");
var blame = new Blame(); var blame = new Blame();
var current = null as Blame.Block;
var errs = RunCommand($"blame -t {revision} -- \"{file}\"", line => { var errs = RunCommand($"blame -t {revision} -- \"{file}\"", line => {
if (blame.IsBinary) return; if (blame.IsBinary) return;
@ -953,7 +952,7 @@ namespace SourceGit.Git {
if (line.IndexOf('\0') >= 0) { if (line.IndexOf('\0') >= 0) {
blame.IsBinary = true; blame.IsBinary = true;
blame.Blocks.Clear(); blame.Lines.Clear();
return; return;
} }
@ -961,25 +960,19 @@ namespace SourceGit.Git {
if (!match.Success) return; if (!match.Success) return;
var commit = match.Groups[1].Value; var commit = match.Groups[1].Value;
var data = match.Groups[4].Value; var author = match.Groups[2].Value;
if (current != null && current.CommitSHA == commit) {
current.Content = current.Content + "\n" + data;
} else {
var timestamp = int.Parse(match.Groups[3].Value); var timestamp = int.Parse(match.Groups[3].Value);
var data = match.Groups[4].Value;
var when = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(timestamp).ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"); var when = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(timestamp).ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss");
current = new Blame.Block() { var blameLine = new Blame.Line() {
CommitSHA = commit, CommitSHA = commit,
Author = match.Groups[2].Value, Author = author,
Time = when, Time = when,
Content = data, Content = data,
}; };
if (current.Author == null) current.Author = ""; blame.Lines.Add(blameLine);
blame.Blocks.Add(current);
}
blame.LineCount++;
}); });
if (errs != null) App.RaiseError(errs); if (errs != null) App.RaiseError(errs);

View file

@ -107,80 +107,60 @@
</Border> </Border>
<!-- Content --> <!-- Content -->
<Border Grid.Row="2" BorderThickness="1" BorderBrush="{StaticResource Brush.Border2}" ClipToBounds="True"> <Border Grid.Row="2" x:Name="area" BorderThickness="1" BorderBrush="{StaticResource Brush.Border2}" ClipToBounds="True" SizeChanged="ContentSizeChanged">
<Grid> <Grid>
<Grid.ColumnDefinitions> <Grid.Resources>
<ColumnDefinition Width="4"/> <Style x:Key="Style.DataGridText.LineNumber" TargetType="{x:Type TextBlock}">
<ColumnDefinition Width="Auto"/> <Setter Property="FontFamily" Value="Consolas"/>
<ColumnDefinition Width="4"/> <Setter Property="Foreground" Value="{StaticResource Brush.FG}"/>
<ColumnDefinition Width="1"/> <Setter Property="HorizontalAlignment" Value="Right"/>
<ColumnDefinition Width="*"/> <Setter Property="VerticalAlignment" Value="Center"/>
</Grid.ColumnDefinitions> <Setter Property="Padding" Value="8,0"/>
</Style>
<ItemsControl <Style x:Key="Style.DataGridText.Block" TargetType="{x:Type TextBlock}">
Grid.Column="1" <Setter Property="FontFamily" Value="Consolas"/>
x:Name="lineNumber" <Setter Property="Foreground" Value="{StaticResource Brush.FG}"/>
Padding="0" <Setter Property="VerticalAlignment" Value="Center"/>
Margin="0" <Setter Property="Padding" Value="0"/>
BorderThickness="0" </Style>
Background="Transparent" </Grid.Resources>
VirtualizingPanel.ScrollUnit="Item"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="Vertical"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.Template> <!-- Blame -->
<ControlTemplate TargetType="{x:Type ItemsControl}"> <DataGrid
<ScrollViewer CanContentScroll="True" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Disabled"> Grid.Column="0"
<ItemsPresenter/> x:Name="blame"
</ScrollViewer> GridLinesVisibility="Vertical"
</ControlTemplate> VerticalGridLinesBrush="{StaticResource Brush.Border2}"
</ItemsControl.Template> FrozenColumnCount="1"
SelectionUnit="FullRow"
SelectionMode="Single">
<ItemsControl.ItemTemplate> <DataGrid.RowStyle>
<Style TargetType="{x:Type DataGridRow}" BasedOn="{StaticResource Style.DataGridRow}">
<EventSetter Event="RequestBringIntoView" Handler="OnBlameRequestBringIntoView"/>
<EventSetter Event="ContextMenuOpening" Handler="OnBlameContextMenuOpening"/>
</Style>
</DataGrid.RowStyle>
<DataGrid.Columns>
<DataGridTextColumn Width="Auto" IsReadOnly="True" Binding="{Binding LineNumber}" ElementStyle="{StaticResource Style.DataGridText.LineNumber}"/>
<DataGridTemplateColumn Width="1" MinWidth="1" IsReadOnly="True">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate> <DataTemplate>
<TextBlock Text="{Binding .}" Padding="0" Margin="0" FontFamily="Consolas" FontSize="13" HorizontalAlignment="Right" VerticalAlignment="Center" Foreground="{StaticResource Brush.FG}"/> <Grid>
<Border Background="{Binding BG}" BorderThickness="0"/>
<TextBlock Text="{Binding Line.Content}" Background="Transparent" Foreground="{StaticResource Brush.FG}" FontFamily="Consolas" FontSize="12" Margin="0" Padding="0"/>
</Grid>
</DataTemplate> </DataTemplate>
</ItemsControl.ItemTemplate> </DataGridTemplateColumn.CellTemplate>
</ItemsControl> </DataGridTemplateColumn>
</DataGrid.Columns>
<Rectangle Grid.Column="3" Width="1" Fill="{StaticResource Brush.Border2}"/> </DataGrid>
<RichTextBox
x:Name="content"
Grid.Column="4"
AcceptsReturn="True"
AcceptsTab="True"
IsReadOnly="True"
BorderThickness="0"
Background="Transparent"
Foreground="{StaticResource Brush.FG}"
Height="Auto"
FontSize="13"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"
RenderOptions.ClearTypeHint="Enabled"
ScrollViewer.ScrollChanged="SyncScrollChanged"
PreviewMouseWheel="MouseWheelOnContent"
SizeChanged="ContentSizeChanged"
SelectionChanged="ContentSelectionChanged"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
FontFamily="Consolas">
<RichTextBox.ContextMenu>
<ContextMenu>
<MenuItem Command="ApplicationCommands.Copy"/>
</ContextMenu>
</RichTextBox.ContextMenu>
<FlowDocument PageWidth="0"/>
</RichTextBox>
<!-- Loading tip --> <!-- Loading tip -->
<Path x:Name="loading" Grid.ColumnSpan="5" Data="{StaticResource Icon.Loading}" RenderTransformOrigin=".5,.5"> <Path x:Name="loading" Data="{StaticResource Icon.Loading}" RenderTransformOrigin=".5,.5">
<Path.RenderTransform> <Path.RenderTransform>
<RotateTransform Angle="0"/> <RotateTransform Angle="0"/>
</Path.RenderTransform> </Path.RenderTransform>
@ -197,7 +177,7 @@
</Path> </Path>
<!-- Popup to show commit info --> <!-- Popup to show commit info -->
<Popup x:Name="popup" Grid.ColumnSpan="5" Placement="MousePoint" IsOpen="False" StaysOpen="False" Focusable="True"> <Popup x:Name="popup" Placement="MousePoint" IsOpen="False" StaysOpen="False" Focusable="True">
<Border BorderBrush="{StaticResource Brush.Accent1}" BorderThickness="1" Background="{StaticResource Brush.BG1}"> <Border BorderBrush="{StaticResource Brush.Accent1}" BorderThickness="1" Background="{StaticResource Brush.BG1}">
<Grid Margin="4"> <Grid Margin="4">
<Grid.RowDefinitions> <Grid.RowDefinitions>

View file

@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
@ -15,6 +14,9 @@ namespace SourceGit.UI {
/// Viewer to show git-blame /// Viewer to show git-blame
/// </summary> /// </summary>
public partial class Blame : Window { public partial class Blame : Window {
private Git.Repository repo = null;
private string lastSHA = null;
private int lastBG = 1;
/// <summary> /// <summary>
/// Background color for blocks. /// Background color for blocks.
@ -24,17 +26,24 @@ namespace SourceGit.UI {
new SolidColorBrush(Color.FromArgb(128, 0, 0, 0)) new SolidColorBrush(Color.FromArgb(128, 0, 0, 0))
}; };
/// <summary>
/// Record
/// </summary>
public class Record {
public Git.Blame.Line Line { get; set; }
public Brush BG { get; set; }
public int LineNumber { get; set; }
}
/// <summary> /// <summary>
/// Constructor /// Constructor
/// </summary> /// </summary>
/// <param name="repo"></param> /// <param name="open"></param>
/// <param name="file"></param> /// <param name="file"></param>
/// <param name="revision"></param> /// <param name="revision"></param>
public Blame(Git.Repository repo, string file, string revision) { public Blame(Git.Repository open, string file, string revision) {
InitializeComponent(); InitializeComponent();
double minWidth = content.ActualWidth;
// Move to center. // Move to center.
var parent = App.Current.MainWindow; var parent = App.Current.MainWindow;
Left = parent.Left + (parent.Width - Width) * 0.5; Left = parent.Left + (parent.Width - Width) * 0.5;
@ -48,97 +57,57 @@ namespace SourceGit.UI {
// Layout content // Layout content
blameFile.Content = $"{file}@{revision.Substring(0, 8)}"; blameFile.Content = $"{file}@{revision.Substring(0, 8)}";
repo = open;
Task.Run(() => { Task.Run(() => {
var blame = repo.BlameFile(file, revision); var result = repo.BlameFile(file, revision);
var records = new List<Record>();
if (result.IsBinary) {
var error = new Record();
error.Line = new Git.Blame.Line() { Content = "BINARY FILE BLAME NOT SUPPORTED!!!", CommitSHA = null };
error.BG = Brushes.Red;
error.LineNumber = 0;
records.Add(error);
} else {
int count = 1;
foreach (var line in result.Lines) {
var r = new Record();
r.Line = line;
r.BG = GetBG(line);
r.LineNumber = count;
records.Add(r);
count++;
}
}
Dispatcher.Invoke(() => { Dispatcher.Invoke(() => {
content.Document.Blocks.Clear();
if (blame.IsBinary) {
lineNumber.ItemsSource = null;
Paragraph p = new Paragraph(new Run("BINARY FILE BLAME NOT SUPPORTED!!!"));
p.Margin = new Thickness(0);
p.Padding = new Thickness(0);
p.LineHeight = 1;
p.Background = Brushes.Transparent;
p.Foreground = FindResource("Brush.FG") as SolidColorBrush;
p.FontStyle = FontStyles.Normal;
content.Document.Blocks.Add(p);
} else {
List<string> numbers = new List<string>();
for (int i = 0; i < blame.LineCount; i++) numbers.Add(i.ToString());
lineNumber.ItemsSource = numbers;
var fg = FindResource("Brush.FG") as SolidColorBrush;
var tf = new Typeface(content.FontFamily, FontStyles.Normal, FontWeights.Normal, FontStretches.Normal);
var ns = new NumberSubstitution();
var mp = new Thickness(0);
for (int i = 0; i < blame.Blocks.Count; i++) {
var frag = blame.Blocks[i];
var idx = i;
Paragraph p = new Paragraph(new Run(frag.Content));
p.DataContext = frag;
p.Margin = mp;
p.Padding = mp;
p.LineHeight = 1;
p.Background = BG[i % 2];
p.Foreground = fg;
p.FontStyle = FontStyles.Normal;
p.ContextMenuOpening += (sender, ev) => {
if (!content.Selection.IsEmpty) return;
Hyperlink link = new Hyperlink(new Run(frag.CommitSHA));
link.ToolTip = "CLICK TO GO";
link.Click += (o, e) => {
repo.OnNavigateCommit?.Invoke(frag.CommitSHA);
e.Handled = true;
};
foreach (var block in content.Document.Blocks) {
var paragraph = block as Paragraph;
if ((paragraph.DataContext as Git.Blame.Block).CommitSHA == frag.CommitSHA) {
paragraph.Background = Brushes.Green;
} else {
paragraph.Background = BG[i % 2];
}
}
commitID.Content = link;
authorName.Content = frag.Author;
authorTime.Content = frag.Time;
popup.IsOpen = true;
ev.Handled = true;
};
var formatter = new FormattedText(
frag.Content,
CultureInfo.CurrentUICulture,
FlowDirection.LeftToRight,
tf,
content.FontSize,
Brushes.Black,
ns,
TextFormattingMode.Ideal);
if (minWidth < formatter.Width) {
content.Document.PageWidth = formatter.Width + 16;
minWidth = formatter.Width;
}
content.Document.Blocks.Add(p);
}
}
// Hide loading.
loading.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, null); loading.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, null);
loading.Visibility = Visibility.Collapsed; loading.Visibility = Visibility.Collapsed;
blame.ItemsSource = records;
blame.UpdateLayout();
ContentSizeChanged(null, null);
}); });
}); });
} }
/// <summary>
/// Get background brush.
/// </summary>
/// <param name="line"></param>
/// <returns></returns>
private Brush GetBG(Git.Blame.Line line) {
if (lastSHA != line.CommitSHA) {
lastSHA = line.CommitSHA;
lastBG = 1 - lastBG;
}
return BG[lastBG];
}
/// <summary> /// <summary>
/// Click logo /// Click logo
/// </summary> /// </summary>
@ -177,68 +146,83 @@ namespace SourceGit.UI {
Close(); Close();
} }
/// <summary>
/// Sync scroll
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void SyncScrollChanged(object sender, ScrollChangedEventArgs e) {
if (e.VerticalChange != 0) {
var margin = new Thickness(4, -e.VerticalOffset, 4, 0);
lineNumber.Margin = margin;
}
}
/// <summary>
/// Mouse wheel
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void MouseWheelOnContent(object sender, MouseWheelEventArgs e) {
if (e.Delta > 0) {
content.LineUp();
} else {
content.LineDown();
}
e.Handled = true;
}
/// <summary> /// <summary>
/// Content size changed. /// Content size changed.
/// </summary> /// </summary>
/// <param name="sender"></param> /// <param name="sender"></param>
/// <param name="e"></param> /// <param name="e"></param>
private void ContentSizeChanged(object sender, SizeChangedEventArgs e) { private void ContentSizeChanged(object sender, SizeChangedEventArgs e) {
if (content.Document.PageWidth < content.ActualWidth) { var total = area.ActualWidth;
content.Document.PageWidth = content.ActualWidth; var offset = blame.NonFrozenColumnsViewportHorizontalOffset;
} var minWidth = total - offset - 2;
var scroller = GetVisualChild<ScrollViewer>(blame);
if (scroller.ComputedVerticalScrollBarVisibility == Visibility.Visible) minWidth -= 8;
blame.Columns[1].MinWidth = minWidth;
blame.Columns[1].Width = DataGridLength.SizeToCells;
blame.UpdateLayout();
} }
/// <summary> /// <summary>
/// Auto scroll when selection changed. /// Context menu opening.
/// </summary> /// </summary>
/// <param name="sender"></param> /// <param name="sender"></param>
/// <param name="e"></param> /// <param name="e"></param>
private void ContentSelectionChanged(object sender, RoutedEventArgs e) { private void OnBlameContextMenuOpening(object sender, ContextMenuEventArgs ev) {
var doc = sender as RichTextBox; var item = sender as DataGridRow;
if (doc == null || doc.IsFocused == false) return; if (item == null) return;
if (Mouse.LeftButton == MouseButtonState.Pressed && !doc.Selection.IsEmpty) { var record = item.DataContext as Record;
var p = Mouse.GetPosition(doc); if (record == null || record.Line.CommitSHA == null) return;
if (p.X <= 8) { Hyperlink link = new Hyperlink(new Run(record.Line.CommitSHA));
doc.LineLeft(); link.ToolTip = "CLICK TO GO";
} else if (p.X >= doc.ActualWidth - 8) { link.Click += (o, e) => {
doc.LineRight(); repo.OnNavigateCommit?.Invoke(record.Line.CommitSHA);
e.Handled = true;
};
commitID.Content = link;
authorName.Content = record.Line.Author;
authorTime.Content = record.Line.Time;
popup.IsOpen = true;
ev.Handled = true;
} }
if (p.Y <= 8) { /// <summary>
doc.LineUp(); /// Prevent auto scroll.
} else if (p.Y >= doc.ActualHeight - 8) { /// </summary>
doc.LineDown(); /// <param name="sender"></param>
/// <param name="e"></param>
private void OnBlameRequestBringIntoView(object sender, RequestBringIntoViewEventArgs e) {
e.Handled = true;
}
/// <summary>
/// Find child element of type.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="parent"></param>
/// <returns></returns>
private T GetVisualChild<T>(DependencyObject parent) where T : Visual {
T child = null;
int count = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < count; i++) {
Visual v = (Visual)VisualTreeHelper.GetChild(parent, i);
child = v as T;
if (child == null) {
child = GetVisualChild<T>(v);
}
if (child != null) {
break;
} }
} }
return child;
} }
} }
} }

View file

@ -299,6 +299,7 @@
<Style TargetType="{x:Type TreeViewItem}" BasedOn="{StaticResource Style.TreeView.ItemContainerStyle}"> <Style TargetType="{x:Type TreeViewItem}" BasedOn="{StaticResource Style.TreeView.ItemContainerStyle}">
<Setter Property="IsExpanded" Value="{Binding IsNodeExpanded, Mode=TwoWay}"/> <Setter Property="IsExpanded" Value="{Binding IsNodeExpanded, Mode=TwoWay}"/>
<EventSetter Event="ContextMenuOpening" Handler="TreeContextMenuOpening"/> <EventSetter Event="ContextMenuOpening" Handler="TreeContextMenuOpening"/>
<EventSetter Event="RequestBringIntoView" Handler="TreeRequestBringIntoView"/>
</Style> </Style>
</TreeView.ItemContainerStyle> </TreeView.ItemContainerStyle>
<TreeView.ItemTemplate> <TreeView.ItemTemplate>
@ -388,6 +389,7 @@
<Style TargetType="{x:Type TreeViewItem}" BasedOn="{StaticResource Style.TreeView.ItemContainerStyle}"> <Style TargetType="{x:Type TreeViewItem}" BasedOn="{StaticResource Style.TreeView.ItemContainerStyle}">
<Setter Property="IsExpanded" Value="{Binding IsNodeExpanded, Mode=TwoWay}"/> <Setter Property="IsExpanded" Value="{Binding IsNodeExpanded, Mode=TwoWay}"/>
<EventSetter Event="ContextMenuOpening" Handler="TreeContextMenuOpening"/> <EventSetter Event="ContextMenuOpening" Handler="TreeContextMenuOpening"/>
<EventSetter Event="RequestBringIntoView" Handler="TreeRequestBringIntoView"/>
</Style> </Style>
</TreeView.ItemContainerStyle> </TreeView.ItemContainerStyle>
<TreeView.ItemTemplate> <TreeView.ItemTemplate>

View file

@ -551,7 +551,10 @@ namespace SourceGit.UI {
menu.IsOpen = true; menu.IsOpen = true;
e.Handled = true; e.Handled = true;
} }
#endregion
private void TreeRequestBringIntoView(object sender, RequestBringIntoViewEventArgs e) {
e.Handled = true;
}
#endregion
} }
} }

View file

@ -7,12 +7,29 @@
mc:Ignorable="d" mc:Ignorable="d"
FontFamily="Consolas"> FontFamily="Consolas">
<Border BorderThickness="1" BorderBrush="{StaticResource Brush.Border2}"> <Border BorderThickness="1" BorderBrush="{StaticResource Brush.Border2}">
<Grid> <Grid x:Name="area" SizeChanged="OnSizeChanged">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="26"/> <RowDefinition Height="26"/>
<RowDefinition Height="*"/> <RowDefinition Height="*"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid.Resources>
<Style x:Key="Style.DataGridText.LineNumber" TargetType="{x:Type TextBlock}">
<Setter Property="FontFamily" Value="Consolas"/>
<Setter Property="Foreground" Value="{StaticResource Brush.FG}"/>
<Setter Property="HorizontalAlignment" Value="Right"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Padding" Value="8,0"/>
</Style>
<Style x:Key="Style.DataGridText.Block" TargetType="{x:Type TextBlock}">
<Setter Property="FontFamily" Value="Consolas"/>
<Setter Property="Foreground" Value="{StaticResource Brush.FG}"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Padding" Value="0"/>
</Style>
</Grid.Resources>
<Border Grid.Row="0" BorderBrush="{StaticResource Brush.Border2}" BorderThickness="0,0,0,1"> <Border Grid.Row="0" BorderBrush="{StaticResource Brush.Border2}" BorderThickness="0,0,0,1">
<Grid Margin="8,4"> <Grid Margin="8,4">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
@ -52,139 +69,105 @@
</Grid> </Grid>
</Border> </Border>
<Grid x:Name="textChange" Grid.Row="1" ClipToBounds="True"> <Grid Grid.Row="1" x:Name="textChangeTwoSides">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="4"/> <ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/>
<ColumnDefinition Width="4"/>
<ColumnDefinition Width="1"/>
<ColumnDefinition x:Name="twoSideLeft"/>
<ColumnDefinition x:Name="twoSideSplittter" Width="1"/>
<ColumnDefinition Width="4"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="4"/>
<ColumnDefinition Width="1"/>
<ColumnDefinition Width="*" MinWidth="100"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<ItemsControl <DataGrid
Grid.Column="1" Grid.Column="0"
x:Name="leftLineNumber" x:Name="textChangeOldSide"
Padding="0" GridLinesVisibility="Vertical"
Margin="0" VerticalGridLinesBrush="{StaticResource Brush.Border2}"
BorderThickness="0" FrozenColumnCount="1"
Background="Transparent" ScrollViewer.ScrollChanged="OnTwoSidesScroll"
VirtualizingPanel.ScrollUnit="Item" ContextMenuOpening="OnTextChangeContextMenuOpening">
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="Vertical"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.Template> <DataGrid.RowStyle>
<ControlTemplate TargetType="{x:Type ItemsControl}"> <Style TargetType="{x:Type DataGridRow}" BasedOn="{StaticResource Style.DataGridRow}">
<ScrollViewer CanContentScroll="True" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Disabled"> <EventSetter Event="RequestBringIntoView" Handler="OnLineRequestBringIntoView"/>
<ItemsPresenter/> </Style>
</ScrollViewer> </DataGrid.RowStyle>
</ControlTemplate>
</ItemsControl.Template>
<ItemsControl.ItemTemplate> <DataGrid.Columns>
<DataGridTextColumn Width="Auto" IsReadOnly="True" Binding="{Binding OldLine}" ElementStyle="{StaticResource Style.DataGridText.LineNumber}"/>
<DataGridTemplateColumn Width="Auto" IsReadOnly="True">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate> <DataTemplate>
<TextBlock Text="{Binding .}" Padding="0" Margin="0" FontFamily="Consolas" FontSize="13" HorizontalAlignment="Right" VerticalAlignment="Center" Foreground="{StaticResource Brush.FG}"/> <Grid>
</DataTemplate> <Border Background="{Binding BG}" BorderThickness="0"/>
</ItemsControl.ItemTemplate> <TextBlock Text="{Binding Content}" Background="Transparent" Foreground="{Binding FG}" FontFamily="Consolas" FontSize="12" FontStyle="{Binding Style}" Margin="0" Padding="0"/>
</ItemsControl>
<Rectangle Grid.Column="3" Width="1" Fill="{StaticResource Brush.Border2}"/>
<RichTextBox
x:Name="leftText"
Grid.Column="4"
AcceptsReturn="True"
AcceptsTab="True"
IsReadOnly="True"
BorderThickness="0"
Background="Transparent"
Foreground="{StaticResource Brush.FG}"
Height="Auto"
FontSize="13"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"
RenderOptions.ClearTypeHint="Enabled"
ScrollViewer.ScrollChanged="OnViewerScroll"
PreviewMouseWheel="OnViewerMouseWheel"
SizeChanged="OnSizeChanged"
SelectionChanged="OnViewerSelectionChanged"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<RichTextBox.Document>
<FlowDocument PageWidth="0"/>
</RichTextBox.Document>
</RichTextBox>
<Rectangle Grid.Column="5" Width="1" Fill="{StaticResource Brush.Border2}"/>
<ItemsControl
Grid.Column="7"
x:Name="rightLineNumber"
Padding="0"
Margin="0"
BorderThickness="0"
Background="Transparent"
VirtualizingPanel.ScrollUnit="Item"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="Vertical"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.Template>
<ControlTemplate TargetType="{x:Type ItemsControl}">
<ScrollViewer CanContentScroll="True" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Disabled">
<ItemsPresenter/>
</ScrollViewer>
</ControlTemplate>
</ItemsControl.Template>
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding .}" Padding="0" Margin="0" FontFamily="Consolas" FontSize="13" HorizontalAlignment="Right" VerticalAlignment="Center" Foreground="{StaticResource Brush.FG}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Rectangle Grid.Column="9" Width="1" Fill="{StaticResource Brush.Border2}"/>
<RichTextBox
x:Name="rightText"
Grid.Column="10"
AcceptsReturn="True"
AcceptsTab="True"
IsReadOnly="True"
BorderThickness="0"
Background="Transparent"
Foreground="{StaticResource Brush.FG}"
Height="Auto"
FontSize="13"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"
RenderOptions.ClearTypeHint="Enabled"
ScrollViewer.ScrollChanged="OnViewerScroll"
PreviewMouseWheel="OnViewerMouseWheel"
SizeChanged="OnSizeChanged"
SelectionChanged="OnViewerSelectionChanged"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<RichTextBox.Document>
<FlowDocument PageWidth="0"/>
</RichTextBox.Document>
</RichTextBox>
</Grid> </Grid>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<DataGrid
Grid.Column="1"
x:Name="textChangeNewSide"
GridLinesVisibility="Vertical"
VerticalGridLinesBrush="{StaticResource Brush.Border2}"
FrozenColumnCount="1"
ScrollViewer.ScrollChanged="OnTwoSidesScroll"
ContextMenuOpening="OnTextChangeContextMenuOpening">
<DataGrid.RowStyle>
<Style TargetType="{x:Type DataGridRow}" BasedOn="{StaticResource Style.DataGridRow}">
<EventSetter Event="RequestBringIntoView" Handler="OnLineRequestBringIntoView"/>
</Style>
</DataGrid.RowStyle>
<DataGrid.Columns>
<DataGridTextColumn Width="Auto" IsReadOnly="True" Binding="{Binding NewLine}" ElementStyle="{StaticResource Style.DataGridText.LineNumber}"/>
<DataGridTemplateColumn Width="Auto" IsReadOnly="True">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Grid>
<Border Background="{Binding BG}" BorderThickness="0"/>
<TextBlock Text="{Binding Content}" Background="Transparent" Foreground="{Binding FG}" FontFamily="Consolas" FontSize="12" FontStyle="{Binding Style}" Margin="0" Padding="0"/>
</Grid>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
<DataGrid
Grid.Row="1"
x:Name="textChangeOneSide"
GridLinesVisibility="Vertical"
VerticalGridLinesBrush="{StaticResource Brush.Border2}"
FrozenColumnCount="2"
ContextMenuOpening="OnTextChangeContextMenuOpening">
<DataGrid.RowStyle>
<Style TargetType="{x:Type DataGridRow}" BasedOn="{StaticResource Style.DataGridRow}">
<EventSetter Event="RequestBringIntoView" Handler="OnLineRequestBringIntoView"/>
</Style>
</DataGrid.RowStyle>
<DataGrid.Columns>
<DataGridTextColumn Width="Auto" IsReadOnly="True" Binding="{Binding OldLine}" ElementStyle="{StaticResource Style.DataGridText.LineNumber}"/>
<DataGridTextColumn Width="Auto" IsReadOnly="True" Binding="{Binding NewLine}" ElementStyle="{StaticResource Style.DataGridText.LineNumber}"/>
<DataGridTemplateColumn Width="Auto" IsReadOnly="True">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Grid>
<Border Background="{Binding BG}" BorderThickness="0"/>
<TextBlock Text="{Binding Content}" Background="Transparent" Foreground="{Binding FG}" FontFamily="Consolas" FontSize="12" FontStyle="{Binding Style}" Margin="0" Padding="0"/>
</Grid>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<Border x:Name="sizeChange" Grid.Row="1" ClipToBounds="True" Background="{StaticResource Brush.BG3}" Visibility="Collapsed"> <Border x:Name="sizeChange" Grid.Row="1" ClipToBounds="True" Background="{StaticResource Brush.BG3}" Visibility="Collapsed">
<StackPanel Orientation="Vertical" VerticalAlignment="Center"> <StackPanel Orientation="Vertical" VerticalAlignment="Center">

View file

@ -1,12 +1,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Threading;
namespace SourceGit.UI { namespace SourceGit.UI {
@ -14,8 +13,11 @@ namespace SourceGit.UI {
/// Viewer for git diff /// Viewer for git diff
/// </summary> /// </summary>
public partial class DiffViewer : UserControl { public partial class DiffViewer : UserControl {
private double minWidth = 0; private List<Git.Diff.LineChange> lineChanges = null;
private Git.Diff.TextChange textChangeData = null; private Brush bgEmpty = new SolidColorBrush(Color.FromArgb(40, 0, 0, 0));
private Brush bgAdded = new SolidColorBrush(Color.FromArgb(60, 0, 255, 0));
private Brush bgDeleted = new SolidColorBrush(Color.FromArgb(60, 255, 0, 0));
private Brush bgNormal = Brushes.Transparent;
/// <summary> /// <summary>
/// Diff options. /// Diff options.
@ -27,6 +29,19 @@ namespace SourceGit.UI {
public string ExtraArgs = ""; public string ExtraArgs = "";
} }
/// <summary>
/// Change block.
/// </summary>
public class ChangeBlock {
public string Content { get; set; }
public Git.Diff.LineMode Mode { get; set; }
public Brush BG { get; set; }
public Brush FG { get; set; }
public FontStyle Style { get; set; }
public string OldLine { get; set; }
public string NewLine { get; set; }
}
/// <summary> /// <summary>
/// Constructor /// Constructor
/// </summary> /// </summary>
@ -50,11 +65,12 @@ namespace SourceGit.UI {
public void Diff(Git.Repository repo, Option opts) { public void Diff(Git.Repository repo, Option opts) {
SetTitle(opts.Path, opts.OrgPath); SetTitle(opts.Path, opts.OrgPath);
textChangeData = null; lineChanges = null;
loading.Visibility = Visibility.Visible; loading.Visibility = Visibility.Visible;
mask.Visibility = Visibility.Collapsed; mask.Visibility = Visibility.Collapsed;
textChange.Visibility = Visibility.Collapsed; textChangeOneSide.Visibility = Visibility.Collapsed;
textChangeTwoSides.Visibility = Visibility.Collapsed;
sizeChange.Visibility = Visibility.Collapsed; sizeChange.Visibility = Visibility.Collapsed;
noChange.Visibility = Visibility.Collapsed; noChange.Visibility = Visibility.Collapsed;
@ -82,8 +98,8 @@ namespace SourceGit.UI {
var rs = Git.Diff.GetTextChange(repo, args); var rs = Git.Diff.GetTextChange(repo, args);
if (rs.IsBinary) { if (rs.IsBinary) {
SetBinaryChange(Git.Diff.GetSizeChange(repo, opts.RevisionRange, opts.Path, opts.OrgPath)); SetBinaryChange(Git.Diff.GetSizeChange(repo, opts.RevisionRange, opts.Path, opts.OrgPath));
} else if (rs.Blocks.Count > 0) { } else if (rs.Lines.Count > 0) {
textChangeData = rs; lineChanges = rs.Lines;
SetTextChange(); SetTextChange();
} else { } else {
SetSame(); SetSame();
@ -112,43 +128,101 @@ namespace SourceGit.UI {
/// </summary> /// </summary>
/// <param name="rs"></param> /// <param name="rs"></param>
private void SetTextChange() { private void SetTextChange() {
if (textChangeData == null) return; if (lineChanges == null) return;
var fgCommon = FindResource("Brush.FG") as Brush;
var fgIndicator = FindResource("Brush.FG2") as Brush;
if (App.Preference.UIUseOneSideDiff) {
var blocks = new List<ChangeBlock>();
foreach (var line in lineChanges) {
var block = new ChangeBlock();
block.Content = line.Content;
block.Mode = line.Mode;
block.BG = GetLineBackground(line);
block.FG = line.Mode == Git.Diff.LineMode.Indicator ? fgIndicator : fgCommon;
block.Style = line.Mode == Git.Diff.LineMode.Indicator ? FontStyles.Italic : FontStyles.Normal;
block.OldLine = line.OldLine;
block.NewLine = line.NewLine;
blocks.Add(block);
}
Dispatcher.Invoke(() => { Dispatcher.Invoke(() => {
loading.Visibility = Visibility.Collapsed; loading.Visibility = Visibility.Collapsed;
textChange.Visibility = Visibility.Visible;
textChangeOptions.Visibility = Visibility.Visible; textChangeOptions.Visibility = Visibility.Visible;
textChangeOneSide.Visibility = Visibility.Visible;
textChangeTwoSides.Visibility = Visibility.Collapsed;
if (App.Preference.UIUseOneSideDiff) { ResetDataGrid(textChangeOneSide);
twoSideLeft.Width = new GridLength(0); textChangeOneSide.ItemsSource = blocks;
twoSideLeft.MinWidth = 0; OnSizeChanged(null, null);
twoSideSplittter.Width = new GridLength(0); });
} else { } else {
twoSideLeft.Width = new GridLength(1, GridUnitType.Star); var oldSideBlocks = new List<ChangeBlock>();
twoSideLeft.MinWidth = 100; var newSideBlocks = new List<ChangeBlock>();
twoSideSplittter.Width = new GridLength(2);
foreach (var line in lineChanges) {
var block = new ChangeBlock();
block.Content = line.Content;
block.Mode = line.Mode;
block.BG = GetLineBackground(line);
block.FG = line.Mode == Git.Diff.LineMode.Indicator ? fgIndicator : fgCommon;
block.Style = line.Mode == Git.Diff.LineMode.Indicator ? FontStyles.Italic : FontStyles.Normal;
block.OldLine = line.OldLine;
block.NewLine = line.NewLine;
switch (line.Mode) {
case Git.Diff.LineMode.Added:
newSideBlocks.Add(block);
var oldEmpty = new ChangeBlock();
oldEmpty.Content = "";
oldEmpty.Mode = Git.Diff.LineMode.None;
oldEmpty.BG = bgEmpty;
oldEmpty.FG = fgCommon;
oldEmpty.Style = FontStyles.Normal;
oldEmpty.OldLine = block.OldLine;
oldEmpty.NewLine = block.NewLine;
oldSideBlocks.Add(oldEmpty);
break;
case Git.Diff.LineMode.Deleted:
oldSideBlocks.Add(block);
var newEmpty = new ChangeBlock();
newEmpty.Content = "";
newEmpty.Mode = Git.Diff.LineMode.None;
newEmpty.BG = bgEmpty;
newEmpty.FG = fgCommon;
newEmpty.Style = FontStyles.Normal;
newEmpty.OldLine = block.OldLine;
newEmpty.NewLine = block.NewLine;
newSideBlocks.Add(newEmpty);
break;
default:
oldSideBlocks.Add(block);
newSideBlocks.Add(block);
break;
}
} }
minWidth = Math.Max(leftText.ActualWidth, rightText.ActualWidth) - 16; Dispatcher.Invoke(() => {
loading.Visibility = Visibility.Collapsed;
textChangeOptions.Visibility = Visibility.Visible;
textChangeOneSide.Visibility = Visibility.Collapsed;
textChangeTwoSides.Visibility = Visibility.Visible;
leftLineNumber.ItemsSource = null; ResetDataGrid(textChangeOldSide);
rightLineNumber.ItemsSource = null; ResetDataGrid(textChangeNewSide);
leftText.Document.Blocks.Clear(); textChangeOldSide.ItemsSource = oldSideBlocks;
rightText.Document.Blocks.Clear(); textChangeNewSide.ItemsSource = newSideBlocks;
var lLineNumbers = new List<string>(); OnSizeChanged(null, null);
var rLineNumbers = new List<string>();
foreach (var b in textChangeData.Blocks) ShowBlock(b, lLineNumbers, rLineNumbers);
if (!App.Preference.UIUseOneSideDiff) leftText.Document.PageWidth = minWidth + 16;
rightText.Document.PageWidth = minWidth + 16;
leftLineNumber.ItemsSource = lLineNumbers;
rightLineNumber.ItemsSource = rLineNumbers;
leftText.ScrollToHome();
}); });
} }
}
/// <summary> /// <summary>
/// Show size changes. /// Show size changes.
@ -195,277 +269,145 @@ namespace SourceGit.UI {
} }
/// <summary> /// <summary>
/// Make paragraph for two-sides diff /// Get background color of line.
/// </summary> /// </summary>
/// <param name="b"></param> /// <param name="b"></param>
/// <param name="leftNumber"></param> /// <returns></returns>
/// <param name="rightNumber"></param> private Brush GetLineBackground(Git.Diff.LineChange line) {
private void ShowBlock(Git.Diff.Block b, List<string> leftNumber, List<string> rightNumber) { switch (line.Mode) {
bool useOneSide = App.Preference.UIUseOneSideDiff; case Git.Diff.LineMode.Added:
if (useOneSide && b.Mode == Git.Diff.LineMode.Empty) return; return bgAdded;
case Git.Diff.LineMode.Deleted:
var content = b.Builder.ToString(); return bgDeleted;
// Make paragraph element
Paragraph p = new Paragraph(new Run(content));
p.Margin = new Thickness(0);
p.Padding = new Thickness(0);
p.LineHeight = 1;
p.Background = GetBlockBackground(b);
p.Foreground = b.Mode == Git.Diff.LineMode.Indicator ? Brushes.Gray : FindResource("Brush.FG") as SolidColorBrush;
p.FontStyle = b.Mode == Git.Diff.LineMode.Indicator ? FontStyles.Italic : FontStyles.Normal;
p.DataContext = b;
p.ContextMenuOpening += OnParagraphContextMenuOpening;
// Calculate with
var formatter = new FormattedText(
content,
CultureInfo.CurrentUICulture,
FlowDirection.LeftToRight,
new Typeface(leftText.FontFamily, p.FontStyle, p.FontWeight, p.FontStretch),
leftText.FontSize,
Brushes.Black,
new NumberSubstitution(),
TextFormattingMode.Ideal);
if (minWidth < formatter.Width) minWidth = formatter.Width;
// Line numbers
switch (b.Side) {
case Git.Diff.Side.Left:
for (int i = 0; i < b.Count; i++) {
if (b.CanShowNumber) leftNumber.Add($"{i + b.LeftStart}");
else leftNumber.Add("");
if (useOneSide) rightNumber.Add("");
}
break;
case Git.Diff.Side.Right:
for (int i = 0; i < b.Count; i++) {
if (b.CanShowNumber) rightNumber.Add($"{i + b.RightStart}");
else rightNumber.Add("");
if (useOneSide) leftNumber.Add("");
}
break;
default: default:
for (int i = 0; i < b.Count; i++) { return bgNormal;
if (b.CanShowNumber) {
leftNumber.Add($"{i + b.LeftStart}");
rightNumber.Add($"{i + b.RightStart}");
} else {
leftNumber.Add("");
rightNumber.Add("");
}
}
break;
}
// Add this paragraph to document.
if (App.Preference.UIUseOneSideDiff) {
rightText.Document.Blocks.Add(p);
} else {
switch (b.Side) {
case Git.Diff.Side.Left:
leftText.Document.Blocks.Add(p);
break;
case Git.Diff.Side.Right:
rightText.Document.Blocks.Add(p);
break;
default:
leftText.Document.Blocks.Add(p);
var cp = new Paragraph(new Run(content));
cp.Margin = new Thickness(0);
cp.Padding = new Thickness();
cp.LineHeight = 1;
cp.Background = p.Background;
cp.Foreground = p.Foreground;
cp.FontStyle = p.FontStyle;
cp.DataContext = b;
cp.ContextMenuOpening += OnParagraphContextMenuOpening;
rightText.Document.Blocks.Add(cp);
break;
}
} }
} }
/// <summary> /// <summary>
/// Get background color of block. /// Find child element of type.
/// </summary> /// </summary>
/// <param name="b"></param> /// <typeparam name="T"></typeparam>
/// <param name="parent"></param>
/// <returns></returns> /// <returns></returns>
private Brush GetBlockBackground(Git.Diff.Block b) { private T GetVisualChild<T>(DependencyObject parent) where T : Visual {
Border border = new Border(); T child = null;
border.BorderThickness = new Thickness(0);
border.BorderBrush = Brushes.LightBlue;
border.Height = b.Count * 16 - 1;
border.Width = minWidth - 1;
switch (b.Mode) { int count = VisualTreeHelper.GetChildrenCount(parent);
case Git.Diff.LineMode.Empty: for (int i = 0; i < count; i++) {
border.Background = new SolidColorBrush(Color.FromArgb(40, 0, 0, 0)); Visual v = (Visual)VisualTreeHelper.GetChild(parent, i);
break; child = v as T;
case Git.Diff.LineMode.Added:
border.Background = new SolidColorBrush(Color.FromArgb(60, 0, 255, 0)); if (child == null) {
break; child = GetVisualChild<T>(v);
case Git.Diff.LineMode.Deleted:
border.Background = new SolidColorBrush(Color.FromArgb(60, 255, 0, 0));
break;
default:
border.Background = Brushes.Transparent;
break;
} }
VisualBrush highlight = new VisualBrush(); if (child != null) {
highlight.TileMode = TileMode.None; break;
highlight.Stretch = Stretch.Fill; }
highlight.Visual = border; }
return highlight;
return child;
}
private void ResetDataGrid(DataGrid dg) {
dg.ItemsSource = null;
dg.Items.Clear();
foreach (var col in dg.Columns) {
col.MinWidth = 0;
col.Width = 0;
}
} }
#endregion #endregion
#region EVENTS #region EVENTS
/// <summary>
/// Context menu for text-change paragraph
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnParagraphContextMenuOpening(object sender, ContextMenuEventArgs ev) {
var paragraph = sender as Paragraph;
var doc = (paragraph.Parent as FlowDocument);
if (doc != null) {
var textBox = doc.Parent as RichTextBox;
if (textBox != null && !textBox.Selection.IsEmpty) {
var copyItem = new MenuItem();
copyItem.Header = "Copy";
copyItem.Click += (o, e) => {
Clipboard.SetText(textBox.Selection.Text);
e.Handled = true;
};
var copyMenu = new ContextMenu();
copyMenu.Items.Add(copyItem);
copyMenu.IsOpen = true;
ev.Handled = true;
return;
}
}
var block = paragraph.DataContext as Git.Diff.Block;
if (block.Mode == Git.Diff.LineMode.Empty || block.Mode == Git.Diff.LineMode.Indicator) {
ev.Handled = true;
return;
}
var highlight = paragraph.Background as VisualBrush;
if (highlight != null) {
(highlight.Visual as Border).BorderThickness = new Thickness(.5);
}
paragraph.ContextMenu = new ContextMenu();
paragraph.ContextMenu.Closed += (o, e) => {
if (paragraph.ContextMenu == (o as ContextMenu)) {
if (highlight != null) {
(highlight.Visual as Border).BorderThickness = new Thickness(0);
}
paragraph.ContextMenu = null;
}
};
var copy = new MenuItem();
copy.Header = "Copy";
copy.Click += (o, e) => {
Clipboard.SetText(block.Builder.ToString());
e.Handled = true;
};
paragraph.ContextMenu.Items.Add(copy);
paragraph.ContextMenu.IsOpen = true;
ev.Handled = true;
}
/// <summary> /// <summary>
/// Fix document size. /// Auto fit text change diff size.
/// </summary> /// </summary>
/// <param name="sender"></param> /// <param name="sender"></param>
/// <param name="e"></param> /// <param name="e"></param>
private void OnSizeChanged(object sender, SizeChangedEventArgs e) { private void OnSizeChanged(object sender, SizeChangedEventArgs e) {
var text = sender as RichTextBox; var total = area.ActualWidth;
if (text.Document.PageWidth < text.ActualWidth) {
text.Document.PageWidth = text.ActualWidth; if (App.Preference.UIUseOneSideDiff) {
textChangeOneSide.Columns[0].Width = DataGridLength.Auto;
textChangeOneSide.Columns[1].Width = DataGridLength.Auto;
textChangeOneSide.Columns[2].MinWidth = 1;
textChangeOneSide.Columns[2].Width = 1;
textChangeOneSide.UpdateLayout();
var offset = textChangeOneSide.NonFrozenColumnsViewportHorizontalOffset;
var minWidth = total - offset;
var scroller = GetVisualChild<ScrollViewer>(textChangeOneSide);
if (scroller.ComputedVerticalScrollBarVisibility == Visibility.Visible) minWidth -= 8;
textChangeOneSide.Columns[2].MinWidth = minWidth;
textChangeOneSide.Columns[2].Width = DataGridLength.Auto;
textChangeOneSide.UpdateLayout();
} else {
textChangeOldSide.Columns[0].Width = DataGridLength.Auto;
textChangeOldSide.Columns[1].MinWidth = 1;
textChangeOldSide.Columns[1].Width = 1;
textChangeOldSide.UpdateLayout();
textChangeNewSide.Columns[0].Width = DataGridLength.Auto;
textChangeNewSide.Columns[1].MinWidth = 1;
textChangeNewSide.Columns[1].Width = 1;
textChangeNewSide.UpdateLayout();
var oldOffset = textChangeOldSide.NonFrozenColumnsViewportHorizontalOffset;
var newOffset = textChangeNewSide.NonFrozenColumnsViewportHorizontalOffset;
var minWidth = total - Math.Min(oldOffset, newOffset);
var scroller = GetVisualChild<ScrollViewer>(textChangeNewSide);
if (scroller.ComputedVerticalScrollBarVisibility == Visibility.Visible) minWidth -= 8;
textChangeOldSide.Columns[1].MinWidth = minWidth;
textChangeOldSide.Columns[1].Width = DataGridLength.Auto;
textChangeOldSide.UpdateLayout();
textChangeNewSide.Columns[1].MinWidth = minWidth;
textChangeNewSide.Columns[1].Width = DataGridLength.Auto;
textChangeNewSide.UpdateLayout();
} }
} }
/// <summary> /// <summary>
/// Scroll using mouse wheel. /// Prevent default auto-scrolling when click row in DataGrid.
/// </summary> /// </summary>
/// <param name="sender"></param> /// <param name="sender"></param>
/// <param name="e"></param> /// <param name="e"></param>
private void OnViewerMouseWheel(object sender, MouseWheelEventArgs e) { private void OnLineRequestBringIntoView(object sender, RequestBringIntoViewEventArgs e) {
var text = sender as RichTextBox;
if (text == null) return;
if (e.Delta > 0) {
text.LineUp();
} else {
text.LineDown();
}
e.Handled = true; e.Handled = true;
} }
/// <summary> /// <summary>
/// Sync scroll both sides. /// Sync scroll on two sides diff.
/// </summary> /// </summary>
/// <param name="sender"></param> /// <param name="sender"></param>
/// <param name="e"></param> /// <param name="e"></param>
private void OnViewerScroll(object sender, ScrollChangedEventArgs e) { private void OnTwoSidesScroll(object sender, ScrollChangedEventArgs e) {
var oldSideScroller = GetVisualChild<ScrollViewer>(textChangeOldSide);
var newSideScroller = GetVisualChild<ScrollViewer>(textChangeNewSide);
if (e.VerticalChange != 0) { if (e.VerticalChange != 0) {
if (leftText.VerticalOffset != e.VerticalOffset) { if (oldSideScroller.VerticalOffset != e.VerticalOffset) {
leftText.ScrollToVerticalOffset(e.VerticalOffset); oldSideScroller.ScrollToVerticalOffset(e.VerticalOffset);
} }
if (rightText.VerticalOffset != e.VerticalOffset) { if (newSideScroller.VerticalOffset != e.VerticalOffset) {
rightText.ScrollToVerticalOffset(e.VerticalOffset); newSideScroller.ScrollToVerticalOffset(e.VerticalOffset);
} }
leftLineNumber.Margin = new Thickness(0, -e.VerticalOffset, 0, 0);
rightLineNumber.Margin = new Thickness(0, -e.VerticalOffset, 0, 0);
} else { } else {
if (leftText.HorizontalOffset != e.HorizontalOffset) { if (oldSideScroller.HorizontalOffset != e.HorizontalOffset) {
leftText.ScrollToHorizontalOffset(e.HorizontalOffset); oldSideScroller.ScrollToHorizontalOffset(e.HorizontalOffset);
} }
if (rightText.HorizontalOffset != e.HorizontalOffset) { if (newSideScroller.HorizontalOffset != e.HorizontalOffset) {
rightText.ScrollToHorizontalOffset(e.HorizontalOffset); newSideScroller.ScrollToHorizontalOffset(e.HorizontalOffset);
}
}
}
/// <summary>
/// Auto scroll when selection changed.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnViewerSelectionChanged(object sender, RoutedEventArgs e) {
var doc = sender as RichTextBox;
if (doc == null || doc.IsFocused == false) return;
if (Mouse.LeftButton == MouseButtonState.Pressed && !doc.Selection.IsEmpty) {
var p = Mouse.GetPosition(doc);
if (p.X <= 8) {
doc.LineLeft();
} else if (p.X >= doc.ActualWidth - 8) {
doc.LineRight();
}
if (p.Y <= 8) {
doc.LineUp();
} else if (p.Y >= doc.ActualHeight - 8) {
doc.LineDown();
} }
} }
} }
@ -476,47 +418,24 @@ namespace SourceGit.UI {
/// <param name="sender"></param> /// <param name="sender"></param>
/// <param name="e"></param> /// <param name="e"></param>
private void Go2Next(object sender, RoutedEventArgs e) { private void Go2Next(object sender, RoutedEventArgs e) {
double minTop = 0; var grid = textChangeOneSide;
if (!App.Preference.UIUseOneSideDiff) grid = textChangeNewSide;
if (App.Preference.UIUseOneSideDiff) { var scroller = GetVisualChild<ScrollViewer>(grid);
foreach (var p in rightText.Document.Blocks) { var firstVisible = (int)scroller.VerticalOffset;
var rect = p.ContentStart.GetCharacterRect(LogicalDirection.Forward); var firstModeEnded = false;
var block = p.DataContext as Git.Diff.Block; var first = grid.Items[firstVisible] as ChangeBlock;
if (rect.Top > 17 && (block.IsLeftDelete || block.IsRightAdded)) { for (int i = firstVisible + 1; i < grid.Items.Count; i++) {
minTop = rect.Top; var next = grid.Items[i] as ChangeBlock;
if (next.Mode != Git.Diff.LineMode.Normal && next.Mode != Git.Diff.LineMode.Indicator) {
if (firstModeEnded || next.Mode != first.Mode) {
scroller.ScrollToVerticalOffset(i);
break; break;
} }
}
} else { } else {
Paragraph next = null; firstModeEnded = true;
foreach (var p in leftText.Document.Blocks) {
var rect = p.ContentStart.GetCharacterRect(LogicalDirection.Forward);
var block = p.DataContext as Git.Diff.Block;
if (rect.Top > 17 && block.IsLeftDelete) {
next = p as Paragraph;
minTop = rect.Top;
break;
} }
} }
foreach (var p in rightText.Document.Blocks) {
var rect = p.ContentStart.GetCharacterRect(LogicalDirection.Forward);
var block = p.DataContext as Git.Diff.Block;
if (rect.Top > 17 && block.IsRightAdded) {
if (next == null || minTop > rect.Top) {
next = p as Paragraph;
minTop = rect.Top;
}
break;
}
}
}
if (minTop > 0) {
rightText.ScrollToVerticalOffset(rightText.VerticalOffset + minTop - 16);
}
} }
/// <summary> /// <summary>
@ -525,51 +444,23 @@ namespace SourceGit.UI {
/// <param name="sender"></param> /// <param name="sender"></param>
/// <param name="e"></param> /// <param name="e"></param>
private void Go2Prev(object sender, RoutedEventArgs e) { private void Go2Prev(object sender, RoutedEventArgs e) {
double maxTop = double.MaxValue; var grid = textChangeOneSide;
if (!App.Preference.UIUseOneSideDiff) grid = textChangeNewSide;
if (App.Preference.UIUseOneSideDiff) { var scroller = GetVisualChild<ScrollViewer>(grid);
var p = rightText.Document.Blocks.LastBlock as Paragraph; var firstVisible = (int)scroller.VerticalOffset;
do { var firstModeEnded = false;
var rect = p.ContentStart.GetCharacterRect(LogicalDirection.Forward); var first = grid.Items[firstVisible] as ChangeBlock;
var block = p.DataContext as Git.Diff.Block; for (int i = firstVisible - 1; i >= 0; i--) {
if (rect.Top < 15 && (block.IsLeftDelete || block.IsRightAdded)) { var next = grid.Items[i] as ChangeBlock;
maxTop = rect.Top; if (next.Mode != Git.Diff.LineMode.Normal && next.Mode != Git.Diff.LineMode.Indicator) {
if (firstModeEnded || next.Mode != first.Mode) {
scroller.ScrollToVerticalOffset(i);
break; break;
} }
p = p.PreviousBlock as Paragraph;
} while (p != null);
} else { } else {
Paragraph next = null; firstModeEnded = true;
var p = leftText.Document.Blocks.LastBlock as Paragraph;
do {
var rect = p.ContentStart.GetCharacterRect(LogicalDirection.Forward);
var block = p.DataContext as Git.Diff.Block;
if (rect.Top < 15 && block.IsLeftDelete) {
next = p;
maxTop = rect.Top;
break;
} }
p = p.PreviousBlock as Paragraph;
} while (p != null);
p = rightText.Document.Blocks.LastBlock as Paragraph;
do {
var rect = p.ContentStart.GetCharacterRect(LogicalDirection.Forward);
var block = p.DataContext as Git.Diff.Block;
if (rect.Top < 15 && block.IsRightAdded) {
if (next == null || maxTop < rect.Top) maxTop = rect.Top;
break;
}
p = p.PreviousBlock as Paragraph;
} while (p != null);
}
if (maxTop != double.MaxValue) {
rightText.ScrollToVerticalOffset(rightText.VerticalOffset + maxTop - 16);
} }
} }
@ -581,6 +472,39 @@ namespace SourceGit.UI {
private void ChangeDiffMode(object sender, RoutedEventArgs e) { private void ChangeDiffMode(object sender, RoutedEventArgs e) {
SetTextChange(); SetTextChange();
} }
/// <summary>
/// Text change context menu opening.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnTextChangeContextMenuOpening(object sender, ContextMenuEventArgs e) {
var grid = sender as DataGrid;
if (grid == null) return;
var menu = new ContextMenu();
var copy = new MenuItem();
copy.Header = "Copy Selected Lines";
copy.Click += (o, ev) => {
var items = grid.SelectedItems;
if (items.Count == 0) return;
var builder = new StringBuilder();
foreach (var item in items) {
var block = item as ChangeBlock;
if (block == null) continue;
if (block.Mode == Git.Diff.LineMode.None || block.Mode == Git.Diff.LineMode.Indicator) continue;
builder.Append(block.Content);
builder.AppendLine();
}
Clipboard.SetText(builder.ToString());
};
menu.Items.Add(copy);
menu.IsOpen = true;
e.Handled = true;
}
#endregion #endregion
} }
} }