refactor<CommitDetail>: move revision files to anthor view

This commit is contained in:
leo 2021-04-30 15:33:26 +08:00
parent 0b6ca9ab8c
commit bf7cc8eed4
6 changed files with 464 additions and 410 deletions

View file

@ -7,9 +7,9 @@ using System.Text.RegularExpressions;
namespace SourceGit.Commands { namespace SourceGit.Commands {
/// <summary> /// <summary>
/// 取消命令执行的对象 /// 用于取消命令执行的上下文对象
/// </summary> /// </summary>
public class Cancellable { public class Context {
public bool IsCancelRequested { get; set; } = false; public bool IsCancelRequested { get; set; } = false;
} }
@ -27,6 +27,11 @@ namespace SourceGit.Commands {
public string Error { get; set; } public string Error { get; set; }
} }
/// <summary>
/// 上下文
/// </summary>
public Context Ctx { get; set; } = null;
/// <summary> /// <summary>
/// 运行路径 /// 运行路径
/// </summary> /// </summary>
@ -42,11 +47,6 @@ namespace SourceGit.Commands {
/// </summary> /// </summary>
public bool TraitErrorAsOutput { get; set; } = false; public bool TraitErrorAsOutput { get; set; } = false;
/// <summary>
/// 用于取消命令指行的Token
/// </summary>
public Cancellable Token { get; set; } = null;
/// <summary> /// <summary>
/// 运行 /// 运行
/// </summary> /// </summary>
@ -69,7 +69,7 @@ namespace SourceGit.Commands {
var isCancelled = false; var isCancelled = false;
proc.OutputDataReceived += (o, e) => { proc.OutputDataReceived += (o, e) => {
if (Token != null && Token.IsCancelRequested) { if (Ctx != null && Ctx.IsCancelRequested) {
isCancelled = true; isCancelled = true;
proc.CancelErrorRead(); proc.CancelErrorRead();
proc.CancelOutputRead(); proc.CancelOutputRead();
@ -85,7 +85,7 @@ namespace SourceGit.Commands {
OnReadline(e.Data); OnReadline(e.Data);
}; };
proc.ErrorDataReceived += (o, e) => { proc.ErrorDataReceived += (o, e) => {
if (Token != null && Token.IsCancelRequested) { if (Ctx != null && Ctx.IsCancelRequested) {
isCancelled = true; isCancelled = true;
proc.CancelErrorRead(); proc.CancelErrorRead();
proc.CancelOutputRead(); proc.CancelOutputRead();

View file

@ -211,21 +211,9 @@ namespace SourceGit.Views.Widgets {
ev.Handled = true; ev.Handled = true;
}; };
var saveAs = new MenuItem();
saveAs.Header = App.Text("SaveAs");
saveAs.Visibility = range.Count == 1 ? Visibility.Visible : Visibility.Collapsed;
saveAs.Click += (obj, ev) => {
FolderBrowser.Open(null, App.Text("SaveFileTo"), saveTo => {
var full = Path.Combine(saveTo, Path.GetFileName(path));
new Commands.SaveRevisionFile(repo, path, range[0].SHA, full).Exec();
});
ev.Handled = true;
};
menu.Items.Add(history); menu.Items.Add(history);
menu.Items.Add(blame); menu.Items.Add(blame);
menu.Items.Add(explore); menu.Items.Add(explore);
menu.Items.Add(saveAs);
} }
var copyPath = new MenuItem(); var copyPath = new MenuItem();

View file

@ -4,18 +4,10 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:controls="clr-namespace:SourceGit.Views.Controls" xmlns:controls="clr-namespace:SourceGit.Views.Controls"
xmlns:converters="clr-namespace:SourceGit.Views.Converters"
xmlns:models="clr-namespace:SourceGit.Models" xmlns:models="clr-namespace:SourceGit.Models"
xmlns:widgets="clr-namespace:SourceGit.Views.Widgets" xmlns:widgets="clr-namespace:SourceGit.Views.Widgets"
mc:Ignorable="d" mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"> d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<Style x:Key="Style.DataGridRow.TextPreview" TargetType="{x:Type DataGridRow}" BasedOn="{StaticResource Style.DataGridRow}">
<EventSetter Event="RequestBringIntoView" Handler="OnRequestBringIntoView"/>
</Style>
<converters:PureFileName x:Key="PureFileName"/>
</UserControl.Resources>
<TabControl> <TabControl>
<TabItem Header="{StaticResource Text.CommitViewer.Info}"> <TabItem Header="{StaticResource Text.CommitViewer.Info}">
<Grid> <Grid>
@ -209,7 +201,7 @@
RowHeight="24" RowHeight="24"
Margin="11,0,0,2"> Margin="11,0,0,2">
<DataGrid.RowStyle> <DataGrid.RowStyle>
<Style TargetType="{x:Type DataGridRow}" BasedOn="{StaticResource Style.DataGridRow.TextPreview}"> <Style TargetType="{x:Type DataGridRow}" BasedOn="{StaticResource Style.DataGridRow}">
<EventSetter Event="ContextMenuOpening" Handler="OnChangeListContextMenuOpening"/> <EventSetter Event="ContextMenuOpening" Handler="OnChangeListContextMenuOpening"/>
</Style> </Style>
</DataGrid.RowStyle> </DataGrid.RowStyle>
@ -241,104 +233,7 @@
<!-- Revision Files --> <!-- Revision Files -->
<TabItem Header="{StaticResource Text.CommitViewer.Files}"> <TabItem Header="{StaticResource Text.CommitViewer.Files}">
<Grid> <widgets:RevisionFiles x:Name="revisionFiles"/>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200" MinWidth="200" MaxWidth="400"/>
<ColumnDefinition Width="1"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Background="{StaticResource Brush.Contents}" BorderBrush="{StaticResource Brush.Border2}" BorderThickness="1">
<controls:Tree
x:Name="treeFiles"
FontFamily="Consolas"
SelectionChanged="OnFilesSelectionChanged"
ContextMenuOpening="OnFilesContextMenuOpening">
<controls:Tree.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal" Height="24">
<Path x:Name="Icon" Width="14" Height="14" Data="{StaticResource Icon.File}"/>
<TextBlock Margin="6,0,0,0" FontSize="11" Text="{Binding Path, Converter={StaticResource PureFileName}}"/>
</StackPanel>
<HierarchicalDataTemplate.Triggers>
<DataTrigger Binding="{Binding IsFolder}" Value="True">
<Setter TargetName="Icon" Property="Fill" Value="Goldenrod"/>
<Setter TargetName="Icon" Property="Opacity" Value="1"/>
</DataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsFolder}" Value="True"/>
<Condition Binding="{Binding RelativeSource={RelativeSource AncestorType={x:Type controls:TreeItem}}, Path=IsExpanded}" Value="False"/>
</MultiDataTrigger.Conditions>
<Setter TargetName="Icon" Property="Data" Value="{StaticResource Icon.Folder.Fill}"/>
</MultiDataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsFolder}" Value="True"/>
<Condition Binding="{Binding RelativeSource={RelativeSource AncestorType={x:Type controls:TreeItem}}, Path=IsExpanded}" Value="True"/>
</MultiDataTrigger.Conditions>
<Setter TargetName="Icon" Property="Data" Value="{StaticResource Icon.Folder.Open}"/>
</MultiDataTrigger>
</HierarchicalDataTemplate.Triggers>
</HierarchicalDataTemplate>
</controls:Tree.ItemTemplate>
</controls:Tree>
</Border>
<GridSplitter Grid.Column="1" Width="1" HorizontalAlignment="Center" VerticalAlignment="Stretch" Background="Transparent"/>
<Border Grid.Column="2" BorderBrush="{StaticResource Brush.Border2}" BorderThickness="1" Margin="2,0">
<Grid>
<Grid x:Name="layerTextPreview" Visibility="Collapsed" SizeChanged="OnTextPreviewSizeChanged">
<DataGrid
x:Name="txtPreviewData"
FontFamily="Consolas"
RowHeight="16"
RowStyle="{StaticResource Style.DataGridRow.TextPreview}"
FrozenColumnCount="1"
ContextMenuOpening="OnTextPreviewContextMenuOpening"
SelectionMode="Extended"
SelectionUnit="FullRow">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Number}" ElementStyle="{StaticResource Style.TextBlock.LineNumber}"/>
<DataGridTextColumn Binding="{Binding Data}" ElementStyle="{StaticResource Style.TextBlock.LineContent}"/>
</DataGrid.Columns>
</DataGrid>
<Rectangle x:Name="txtPreviewSplitter" Width="1" Fill="{StaticResource Brush.Border2}" HorizontalAlignment="Left"/>
</Grid>
<ScrollViewer
x:Name="layerImagePreview"
HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"
Visibility="Collapsed">
<Image
x:Name="imgPreviewData"
Width="Auto" Height="Auto"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</ScrollViewer>
<StackPanel
x:Name="layerRevisionPreview"
Orientation="Vertical"
VerticalAlignment="Center" HorizontalAlignment="Center"
Visibility="Collapsed">
<Path x:Name="iconRevisionPreview" Width="64" Height="64" Data="{StaticResource Icon.Submodule}" Fill="{StaticResource Brush.FG2}"/>
<TextBlock x:Name="txtRevisionPreview" Margin="0,16,0,0" FontFamily="Consolas" FontSize="18" FontWeight="UltraBold" HorizontalAlignment="Center" Foreground="{StaticResource Brush.FG2}"/>
</StackPanel>
<StackPanel
x:Name="layerBinaryPreview"
Orientation="Vertical"
VerticalAlignment="Center" HorizontalAlignment="Center"
Visibility="Collapsed">
<Path Width="64" Height="64" Data="{StaticResource Icon.Error}" Fill="{StaticResource Brush.FG2}"/>
<TextBlock Margin="0,16,0,0" Text="{StaticResource Text.BinaryNotSupported}" FontFamily="Consolas" FontSize="18" FontWeight="UltraBold" HorizontalAlignment="Center" Foreground="{StaticResource Brush.FG2}"/>
</StackPanel>
</Grid>
</Border>
</Grid>
</TabItem> </TabItem>
</TabControl> </TabControl>
</UserControl> </UserControl>

View file

@ -1,14 +1,9 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization;
using System.IO; using System.IO;
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.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation; using System.Windows.Navigation;
namespace SourceGit.Views.Widgets { namespace SourceGit.Views.Widgets {
@ -19,18 +14,7 @@ namespace SourceGit.Views.Widgets {
public partial class CommitDetail : UserControl { public partial class CommitDetail : UserControl {
private string repo = null; private string repo = null;
private Models.Commit commit = null; private Models.Commit commit = null;
private Commands.Cancellable cancelToken = new Commands.Cancellable(); private Commands.Context cancelToken = new Commands.Context();
/// <summary>
/// 文件列表树节点
/// </summary>
public class FileNode {
public Models.ObjectType Type { get; set; } = Models.ObjectType.None;
public string Path { get; set; } = "";
public string SHA { get; set; } = null;
public bool IsFolder => Type == Models.ObjectType.None;
public List<FileNode> Children { get; set; } = new List<FileNode>();
}
public CommitDetail() { public CommitDetail() {
InitializeComponent(); InitializeComponent();
@ -38,14 +22,14 @@ namespace SourceGit.Views.Widgets {
public void SetData(string repo, Models.Commit commit) { public void SetData(string repo, Models.Commit commit) {
cancelToken.IsCancelRequested = true; cancelToken.IsCancelRequested = true;
cancelToken = new Commands.Cancellable(); cancelToken = new Commands.Context();
this.repo = repo; this.repo = repo;
this.commit = commit; this.commit = commit;
revisionFiles.SetData(repo, commit.SHA, cancelToken);
UpdateInformation(commit); UpdateInformation(commit);
UpdateChanges(); UpdateChanges();
UpdateRevisionFiles();
} }
#region DATA #region DATA
@ -91,10 +75,10 @@ namespace SourceGit.Views.Widgets {
} }
private void UpdateChanges() { private void UpdateChanges() {
var cmd = new Commands.CommitChanges(repo, commit.SHA) { Token = cancelToken }; var cmd = new Commands.CommitChanges(repo, commit.SHA) { Ctx = cancelToken };
Task.Run(() => { Task.Run(() => {
var changes = cmd.Result(); var changes = cmd.Result();
if (cmd.Token.IsCancelRequested) return; if (cmd.Ctx.IsCancelRequested) return;
Dispatcher.Invoke(() => { Dispatcher.Invoke(() => {
changeList.ItemsSource = changes; changeList.ItemsSource = changes;
@ -102,90 +86,6 @@ namespace SourceGit.Views.Widgets {
}); });
}); });
} }
private void SortFileNodes(List<FileNode> nodes) {
nodes.Sort((l, r) => {
if (l.IsFolder == r.IsFolder) {
return l.Path.CompareTo(r.Path);
} else {
return l.IsFolder ? -1 : 1;
}
});
foreach (var node in nodes) {
if (node.Children.Count > 1) SortFileNodes(node.Children);
}
}
private void UpdateRevisionFiles() {
var cmd = new Commands.RevisionObjects(repo, commit.SHA) { Token = cancelToken };
Task.Run(() => {
var objects = cmd.Result();
if (cmd.Token.IsCancelRequested) return;
var nodes = new List<FileNode>();
var folders = new Dictionary<string, FileNode>();
foreach (var obj in objects) {
var sepIdx = obj.Path.IndexOf('/');
if (sepIdx == -1) {
nodes.Add(new FileNode() {
Type = obj.Type,
Path = obj.Path,
SHA = obj.SHA,
});
} else {
FileNode lastFolder = null;
var start = 0;
while (sepIdx != -1) {
var folder = obj.Path.Substring(0, sepIdx);
if (folders.ContainsKey(folder)) {
lastFolder = folders[folder];
} else if (lastFolder == null) {
lastFolder = new FileNode() {
Type = Models.ObjectType.None,
Path = folder,
SHA = null,
};
nodes.Add(lastFolder);
folders.Add(folder, lastFolder);
} else {
var cur = new FileNode() {
Type = Models.ObjectType.None,
Path = folder,
SHA = null,
};
folders.Add(folder, cur);
lastFolder.Children.Add(cur);
lastFolder = cur;
}
start = sepIdx + 1;
sepIdx = obj.Path.IndexOf('/', start);
}
lastFolder.Children.Add(new FileNode() {
Type = obj.Type,
Path = obj.Path,
SHA = obj.SHA,
});
}
obj.Path = null;
}
folders.Clear();
objects.Clear();
SortFileNodes(nodes);
Dispatcher.Invoke(() => {
treeFiles.ItemsSource = nodes;
GC.Collect();
});
});
}
#endregion #endregion
#region INFORMATION #region INFORMATION
@ -201,214 +101,48 @@ namespace SourceGit.Views.Widgets {
if (change == null) return; if (change == null) return;
var menu = new ContextMenu(); var menu = new ContextMenu();
FillContextMenu(menu, change.Path, change.Index == Models.Change.Status.Deleted, true); if (change.Index != Models.Change.Status.Deleted) {
menu.IsOpen = true;
e.Handled = true;
}
#endregion
#region REVISION_FILES
private bool IsImageFile(string path) {
return path.EndsWith(".png") ||
path.EndsWith(".jpg") ||
path.EndsWith(".jpeg") ||
path.EndsWith(".ico") ||
path.EndsWith(".bmp") ||
path.EndsWith(".tiff") ||
path.EndsWith(".gif");
}
private void LayoutTextPreview(List<Models.TextLine> lines) {
var font = new FontFamily("Consolas");
var maxLineNumber = $"{lines.Count + 1}";
var formatted = new FormattedText(
maxLineNumber,
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
new Typeface(font, FontStyles.Normal, FontWeights.Normal, FontStretches.Normal),
12.0,
Brushes.Black,
VisualTreeHelper.GetDpi(this).PixelsPerDip);
var offset = formatted.Width + 16;
if (lines.Count * 16 > layerTextPreview.ActualHeight) offset += 8;
txtPreviewData.ItemsSource = lines;
txtPreviewData.Columns[0].Width = new DataGridLength(formatted.Width + 16, DataGridLengthUnitType.Pixel);
txtPreviewData.Columns[1].Width = DataGridLength.Auto;
txtPreviewData.Columns[1].Width = DataGridLength.SizeToCells;
txtPreviewData.Columns[1].MinWidth = layerTextPreview.ActualWidth - offset;
txtPreviewSplitter.Margin = new Thickness(formatted.Width + 15, 0, 0, 0);
}
private void OnTextPreviewSizeChanged(object sender, SizeChangedEventArgs e) {
if (txtPreviewData == null) return;
var offset = txtPreviewData.NonFrozenColumnsViewportHorizontalOffset;
if (txtPreviewData.Items.Count * 16 > layerTextPreview.ActualHeight) offset += 8;
txtPreviewData.Columns[1].Width = DataGridLength.Auto;
txtPreviewData.Columns[1].Width = DataGridLength.SizeToCells;
txtPreviewData.Columns[1].MinWidth = layerTextPreview.ActualWidth - offset;
txtPreviewData.UpdateLayout();
}
private void OnTextPreviewContextMenuOpening(object sender, ContextMenuEventArgs e) {
var grid = sender as DataGrid;
if (grid == null) return;
var menu = new ContextMenu();
var copyIcon = new System.Windows.Shapes.Path();
copyIcon.Data = FindResource("Icon.Copy") as Geometry;
copyIcon.Width = 10;
var copy = new MenuItem();
copy.Header = "Copy";
copy.Icon = copyIcon;
copy.Click += (o, ev) => {
var items = grid.SelectedItems;
if (items.Count == 0) return;
var builder = new StringBuilder();
foreach (var item in items) {
var line = item as Models.TextLine;
if (line == null) continue;
builder.Append(line.Data);
builder.AppendLine();
}
Clipboard.SetText(builder.ToString());
};
menu.Items.Add(copy);
menu.IsOpen = true;
e.Handled = true;
}
private void OnFilesSelectionChanged(object sender, RoutedEventArgs e) {
layerTextPreview.Visibility = Visibility.Collapsed;
layerImagePreview.Visibility = Visibility.Collapsed;
layerRevisionPreview.Visibility = Visibility.Collapsed;
layerBinaryPreview.Visibility = Visibility.Collapsed;
txtPreviewData.ItemsSource = null;
if (treeFiles.Selected.Count == 0) return;
var node = treeFiles.Selected[0] as FileNode;
switch (node.Type) {
case Models.ObjectType.Blob:
if (IsImageFile(node.Path)) {
var tmp = Path.GetTempFileName();
new Commands.SaveRevisionFile(repo, node.Path, commit.SHA, tmp).Exec();
layerImagePreview.Visibility = Visibility.Visible;
imgPreviewData.Source = new BitmapImage(new Uri(tmp, UriKind.Absolute));
} else if (new Commands.IsLFSFiltered(repo, node.Path).Result()) {
var lfs = new Commands.QueryLFSObject(repo, commit.SHA, node.Path).Result();
layerRevisionPreview.Visibility = Visibility.Visible;
iconRevisionPreview.Data = FindResource("Icon.LFS") as Geometry;
txtRevisionPreview.Text = "LFS SIZE: " + App.Text("Bytes", lfs.Size);
} else if (new Commands.IsBinaryFile(repo, commit.SHA, node.Path).Result()) {
layerBinaryPreview.Visibility = Visibility.Visible;
} else {
layerTextPreview.Visibility = Visibility.Visible;
Task.Run(() => {
var lines = new Commands.QueryFileContent(repo, commit.SHA, node.Path).Result();
Dispatcher.Invoke(() => LayoutTextPreview(lines));
});
}
break;
case Models.ObjectType.Tag:
layerRevisionPreview.Visibility = Visibility.Visible;
iconRevisionPreview.Data = FindResource("Icon.Tag") as Geometry;
txtRevisionPreview.Text = "TAG: " + node.SHA;
break;
case Models.ObjectType.Commit:
layerRevisionPreview.Visibility = Visibility.Visible;
iconRevisionPreview.Data = FindResource("Icon.Submodule") as Geometry;
txtRevisionPreview.Text = "SUBMODULE: " + node.SHA;
break;
case Models.ObjectType.Tree:
layerRevisionPreview.Visibility = Visibility.Visible;
iconRevisionPreview.Data = FindResource("Icon.Tree") as Geometry;
txtRevisionPreview.Text = "TREE: " + node.SHA;
break;
default:
return;
}
}
private void OnFilesContextMenuOpening(object sender, ContextMenuEventArgs e) {
var item = treeFiles.FindItem(e.OriginalSource as DependencyObject);
if (item == null) return;
var node = item.DataContext as FileNode;
if (node == null || node.IsFolder) return;
var menu = new ContextMenu();
FillContextMenu(menu, node.Path, false, node.Type == Models.ObjectType.Blob);
menu.IsOpen = true;
e.Handled = true;
}
#endregion
#region COMMON
private void FillContextMenu(ContextMenu menu, string path, bool isDeleted, bool canSave) {
if (!isDeleted) {
var history = new MenuItem(); var history = new MenuItem();
history.Header = App.Text("FileHistory"); history.Header = App.Text("FileHistory");
history.IsEnabled = change.Index != Models.Change.Status.Deleted;
history.Click += (o, ev) => { history.Click += (o, ev) => {
var viewer = new Views.Histories(repo, path); var viewer = new Views.Histories(repo, change.Path);
viewer.Show(); viewer.Show();
ev.Handled = true; ev.Handled = true;
}; };
var blame = new MenuItem(); var blame = new MenuItem();
blame.Header = App.Text("Blame"); blame.Header = App.Text("Blame");
blame.IsEnabled = change.Index != Models.Change.Status.Deleted;
blame.Click += (obj, ev) => { blame.Click += (obj, ev) => {
var viewer = new Blame(repo, path, commit.SHA); var viewer = new Blame(repo, change.Path, commit.SHA);
viewer.Show(); viewer.Show();
ev.Handled = true; ev.Handled = true;
}; };
var explore = new MenuItem(); var explore = new MenuItem();
explore.Header = App.Text("RevealFile"); explore.Header = App.Text("RevealFile");
explore.IsEnabled = change.Index != Models.Change.Status.Deleted;
explore.Click += (o, ev) => { explore.Click += (o, ev) => {
var full = Path.GetFullPath(repo + "\\" + path); var full = Path.GetFullPath(repo + "\\" + change.Path);
Process.Start("explorer", $"/select,{full}"); Process.Start("explorer", $"/select,{full}");
ev.Handled = true; ev.Handled = true;
}; };
var saveAs = new MenuItem();
saveAs.Header = App.Text("SaveAs");
saveAs.IsEnabled = canSave;
saveAs.Click += (obj, ev) => {
FolderBrowser.Open(null, App.Text("SaveFileTo"), saveTo => {
var full = Path.Combine(saveTo, Path.GetFileName(path));
new Commands.SaveRevisionFile(repo, path, commit.SHA, full).Exec();
});
ev.Handled = true;
};
menu.Items.Add(history); menu.Items.Add(history);
menu.Items.Add(blame); menu.Items.Add(blame);
menu.Items.Add(explore); menu.Items.Add(explore);
menu.Items.Add(saveAs);
} }
var copyPath = new MenuItem(); var copyPath = new MenuItem();
copyPath.Header = App.Text("CopyPath"); copyPath.Header = App.Text("CopyPath");
copyPath.Click += (obj, ev) => { copyPath.Click += (obj, ev) => {
Clipboard.SetText(path); Clipboard.SetText(change.Path);
ev.Handled = true;
}; };
menu.Items.Add(copyPath); menu.Items.Add(copyPath);
}
private void OnRequestBringIntoView(object sender, RequestBringIntoViewEventArgs e) { menu.IsOpen = true;
e.Handled = true; e.Handled = true;
} }
#endregion #endregion

View file

@ -0,0 +1,115 @@
<UserControl x:Class="SourceGit.Views.Widgets.RevisionFiles"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:controls="clr-namespace:SourceGit.Views.Controls"
xmlns:converters="clr-namespace:SourceGit.Views.Converters"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<Style x:Key="Style.DataGridRow.TextPreview" TargetType="{x:Type DataGridRow}" BasedOn="{StaticResource Style.DataGridRow}">
<EventSetter Event="RequestBringIntoView" Handler="OnRequestBringIntoView"/>
</Style>
<converters:PureFileName x:Key="PureFileName"/>
</UserControl.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200" MinWidth="200" MaxWidth="400"/>
<ColumnDefinition Width="1"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Background="{StaticResource Brush.Contents}" BorderBrush="{StaticResource Brush.Border2}" BorderThickness="1">
<controls:Tree
x:Name="treeFiles"
FontFamily="Consolas"
SelectionChanged="OnFilesSelectionChanged"
ContextMenuOpening="OnFilesContextMenuOpening">
<controls:Tree.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal" Height="24">
<Path x:Name="Icon" Width="14" Height="14" Data="{StaticResource Icon.File}"/>
<TextBlock Margin="6,0,0,0" FontSize="11" Text="{Binding Path, Converter={StaticResource PureFileName}}"/>
</StackPanel>
<HierarchicalDataTemplate.Triggers>
<DataTrigger Binding="{Binding IsFolder}" Value="True">
<Setter TargetName="Icon" Property="Fill" Value="Goldenrod"/>
<Setter TargetName="Icon" Property="Opacity" Value="1"/>
</DataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsFolder}" Value="True"/>
<Condition Binding="{Binding RelativeSource={RelativeSource AncestorType={x:Type controls:TreeItem}}, Path=IsExpanded}" Value="False"/>
</MultiDataTrigger.Conditions>
<Setter TargetName="Icon" Property="Data" Value="{StaticResource Icon.Folder.Fill}"/>
</MultiDataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsFolder}" Value="True"/>
<Condition Binding="{Binding RelativeSource={RelativeSource AncestorType={x:Type controls:TreeItem}}, Path=IsExpanded}" Value="True"/>
</MultiDataTrigger.Conditions>
<Setter TargetName="Icon" Property="Data" Value="{StaticResource Icon.Folder.Open}"/>
</MultiDataTrigger>
</HierarchicalDataTemplate.Triggers>
</HierarchicalDataTemplate>
</controls:Tree.ItemTemplate>
</controls:Tree>
</Border>
<GridSplitter Grid.Column="1" Width="1" HorizontalAlignment="Center" VerticalAlignment="Stretch" Background="Transparent"/>
<Border Grid.Column="2" BorderBrush="{StaticResource Brush.Border2}" BorderThickness="1" Margin="2,0">
<Grid>
<Grid x:Name="layerTextPreview" Visibility="Collapsed" SizeChanged="OnTextPreviewSizeChanged">
<DataGrid
x:Name="txtPreviewData"
FontFamily="Consolas"
RowHeight="16"
RowStyle="{StaticResource Style.DataGridRow.TextPreview}"
FrozenColumnCount="1"
ContextMenuOpening="OnTextPreviewContextMenuOpening"
SelectionMode="Extended"
SelectionUnit="FullRow">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Number}" ElementStyle="{StaticResource Style.TextBlock.LineNumber}"/>
<DataGridTextColumn Binding="{Binding Data}" ElementStyle="{StaticResource Style.TextBlock.LineContent}"/>
</DataGrid.Columns>
</DataGrid>
<Rectangle x:Name="txtPreviewSplitter" Width="1" Fill="{StaticResource Brush.Border2}" HorizontalAlignment="Left"/>
</Grid>
<ScrollViewer
x:Name="layerImagePreview"
HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"
Visibility="Collapsed">
<Image
x:Name="imgPreviewData"
Width="Auto" Height="Auto"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</ScrollViewer>
<StackPanel
x:Name="layerRevisionPreview"
Orientation="Vertical"
VerticalAlignment="Center" HorizontalAlignment="Center"
Visibility="Collapsed">
<Path x:Name="iconRevisionPreview" Width="64" Height="64" Data="{StaticResource Icon.Submodule}" Fill="{StaticResource Brush.FG2}"/>
<TextBlock x:Name="txtRevisionPreview" Margin="0,16,0,0" FontFamily="Consolas" FontSize="18" FontWeight="UltraBold" HorizontalAlignment="Center" Foreground="{StaticResource Brush.FG2}"/>
</StackPanel>
<StackPanel
x:Name="layerBinaryPreview"
Orientation="Vertical"
VerticalAlignment="Center" HorizontalAlignment="Center"
Visibility="Collapsed">
<Path Width="64" Height="64" Data="{StaticResource Icon.Error}" Fill="{StaticResource Brush.FG2}"/>
<TextBlock Margin="0,16,0,0" Text="{StaticResource Text.BinaryNotSupported}" FontFamily="Consolas" FontSize="18" FontWeight="UltraBold" HorizontalAlignment="Center" Foreground="{StaticResource Brush.FG2}"/>
</StackPanel>
</Grid>
</Border>
</Grid>
</UserControl>

View file

@ -0,0 +1,322 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace SourceGit.Views.Widgets {
/// <summary>
/// 提交信息面板中的文件列表分页
/// </summary>
public partial class RevisionFiles : UserControl {
private string repo = null;
private string sha = null;
/// <summary>
/// 文件列表树节点
/// </summary>
public class FileNode {
public Models.ObjectType Type { get; set; } = Models.ObjectType.None;
public string Path { get; set; } = "";
public string SHA { get; set; } = null;
public bool IsFolder => Type == Models.ObjectType.None;
public List<FileNode> Children { get; set; } = new List<FileNode>();
}
public RevisionFiles() {
InitializeComponent();
}
public void SetData(string repo, string sha, Commands.Context cancelToken) {
this.repo = repo;
this.sha = sha;
var cmd = new Commands.RevisionObjects(repo, sha) { Ctx = cancelToken };
Task.Run(() => {
var objects = cmd.Result();
if (cmd.Ctx.IsCancelRequested) return;
var nodes = new List<FileNode>();
var folders = new Dictionary<string, FileNode>();
foreach (var obj in objects) {
var sepIdx = obj.Path.IndexOf('/');
if (sepIdx == -1) {
nodes.Add(new FileNode() {
Type = obj.Type,
Path = obj.Path,
SHA = obj.SHA,
});
} else {
FileNode lastFolder = null;
var start = 0;
while (sepIdx != -1) {
var folder = obj.Path.Substring(0, sepIdx);
if (folders.ContainsKey(folder)) {
lastFolder = folders[folder];
} else if (lastFolder == null) {
lastFolder = new FileNode() {
Type = Models.ObjectType.None,
Path = folder,
SHA = null,
};
nodes.Add(lastFolder);
folders.Add(folder, lastFolder);
} else {
var cur = new FileNode() {
Type = Models.ObjectType.None,
Path = folder,
SHA = null,
};
folders.Add(folder, cur);
lastFolder.Children.Add(cur);
lastFolder = cur;
}
start = sepIdx + 1;
sepIdx = obj.Path.IndexOf('/', start);
}
lastFolder.Children.Add(new FileNode() {
Type = obj.Type,
Path = obj.Path,
SHA = obj.SHA,
});
}
obj.Path = null;
}
folders.Clear();
objects.Clear();
SortFileNodes(nodes);
Dispatcher.Invoke(() => {
treeFiles.ItemsSource = nodes;
GC.Collect();
});
});
}
private void SortFileNodes(List<FileNode> nodes) {
nodes.Sort((l, r) => {
if (l.IsFolder == r.IsFolder) {
return l.Path.CompareTo(r.Path);
} else {
return l.IsFolder ? -1 : 1;
}
});
foreach (var node in nodes) {
if (node.Children.Count > 1) SortFileNodes(node.Children);
}
}
private bool IsImageFile(string path) {
return path.EndsWith(".png") ||
path.EndsWith(".jpg") ||
path.EndsWith(".jpeg") ||
path.EndsWith(".ico") ||
path.EndsWith(".bmp") ||
path.EndsWith(".tiff") ||
path.EndsWith(".gif");
}
#region EVENTS
private void LayoutTextPreview(List<Models.TextLine> lines) {
var font = new FontFamily("Consolas");
var maxLineNumber = $"{lines.Count + 1}";
var formatted = new FormattedText(
maxLineNumber,
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
new Typeface(font, FontStyles.Normal, FontWeights.Normal, FontStretches.Normal),
12.0,
Brushes.Black,
VisualTreeHelper.GetDpi(this).PixelsPerDip);
var offset = formatted.Width + 16;
if (lines.Count * 16 > layerTextPreview.ActualHeight) offset += 8;
txtPreviewData.ItemsSource = lines;
txtPreviewData.Columns[0].Width = new DataGridLength(formatted.Width + 16, DataGridLengthUnitType.Pixel);
txtPreviewData.Columns[1].Width = DataGridLength.Auto;
txtPreviewData.Columns[1].Width = DataGridLength.SizeToCells;
txtPreviewData.Columns[1].MinWidth = layerTextPreview.ActualWidth - offset;
txtPreviewSplitter.Margin = new Thickness(formatted.Width + 15, 0, 0, 0);
}
private void OnTextPreviewSizeChanged(object sender, SizeChangedEventArgs e) {
if (txtPreviewData == null) return;
var offset = txtPreviewData.NonFrozenColumnsViewportHorizontalOffset;
if (txtPreviewData.Items.Count * 16 > layerTextPreview.ActualHeight) offset += 8;
txtPreviewData.Columns[1].Width = DataGridLength.Auto;
txtPreviewData.Columns[1].Width = DataGridLength.SizeToCells;
txtPreviewData.Columns[1].MinWidth = layerTextPreview.ActualWidth - offset;
txtPreviewData.UpdateLayout();
}
private void OnTextPreviewContextMenuOpening(object sender, ContextMenuEventArgs e) {
var grid = sender as DataGrid;
if (grid == null) return;
var menu = new ContextMenu();
var copyIcon = new System.Windows.Shapes.Path();
copyIcon.Data = FindResource("Icon.Copy") as Geometry;
copyIcon.Width = 10;
var copy = new MenuItem();
copy.Header = "Copy";
copy.Icon = copyIcon;
copy.Click += (o, ev) => {
var items = grid.SelectedItems;
if (items.Count == 0) return;
var builder = new StringBuilder();
foreach (var item in items) {
var line = item as Models.TextLine;
if (line == null) continue;
builder.Append(line.Data);
builder.AppendLine();
}
Clipboard.SetText(builder.ToString());
};
menu.Items.Add(copy);
menu.IsOpen = true;
e.Handled = true;
}
private void OnFilesSelectionChanged(object sender, RoutedEventArgs e) {
layerTextPreview.Visibility = Visibility.Collapsed;
layerImagePreview.Visibility = Visibility.Collapsed;
layerRevisionPreview.Visibility = Visibility.Collapsed;
layerBinaryPreview.Visibility = Visibility.Collapsed;
txtPreviewData.ItemsSource = null;
if (treeFiles.Selected.Count == 0) return;
var node = treeFiles.Selected[0] as FileNode;
switch (node.Type) {
case Models.ObjectType.Blob:
if (IsImageFile(node.Path)) {
var tmp = Path.GetTempFileName();
new Commands.SaveRevisionFile(repo, node.Path, sha, tmp).Exec();
layerImagePreview.Visibility = Visibility.Visible;
imgPreviewData.Source = new BitmapImage(new Uri(tmp, UriKind.Absolute));
} else if (new Commands.IsLFSFiltered(repo, node.Path).Result()) {
var lfs = new Commands.QueryLFSObject(repo, sha, node.Path).Result();
layerRevisionPreview.Visibility = Visibility.Visible;
iconRevisionPreview.Data = FindResource("Icon.LFS") as Geometry;
txtRevisionPreview.Text = "LFS SIZE: " + App.Text("Bytes", lfs.Size);
} else if (new Commands.IsBinaryFile(repo, sha, node.Path).Result()) {
layerBinaryPreview.Visibility = Visibility.Visible;
} else {
layerTextPreview.Visibility = Visibility.Visible;
Task.Run(() => {
var lines = new Commands.QueryFileContent(repo, sha, node.Path).Result();
Dispatcher.Invoke(() => LayoutTextPreview(lines));
});
}
break;
case Models.ObjectType.Tag:
layerRevisionPreview.Visibility = Visibility.Visible;
iconRevisionPreview.Data = FindResource("Icon.Tag") as Geometry;
txtRevisionPreview.Text = "TAG: " + node.SHA;
break;
case Models.ObjectType.Commit:
layerRevisionPreview.Visibility = Visibility.Visible;
iconRevisionPreview.Data = FindResource("Icon.Submodule") as Geometry;
txtRevisionPreview.Text = "SUBMODULE: " + node.SHA;
break;
case Models.ObjectType.Tree:
layerRevisionPreview.Visibility = Visibility.Visible;
iconRevisionPreview.Data = FindResource("Icon.Tree") as Geometry;
txtRevisionPreview.Text = "TREE: " + node.SHA;
break;
default:
return;
}
}
private void OnFilesContextMenuOpening(object sender, ContextMenuEventArgs e) {
var item = treeFiles.FindItem(e.OriginalSource as DependencyObject);
if (item == null) return;
var node = item.DataContext as FileNode;
if (node == null || node.IsFolder) return;
var history = new MenuItem();
history.Header = App.Text("FileHistory");
history.Click += (o, ev) => {
var viewer = new Views.Histories(repo, node.Path);
viewer.Show();
ev.Handled = true;
};
var blame = new MenuItem();
blame.Header = App.Text("Blame");
blame.Click += (obj, ev) => {
var viewer = new Blame(repo, node.Path, sha);
viewer.Show();
ev.Handled = true;
};
var explore = new MenuItem();
explore.Header = App.Text("RevealFile");
explore.Click += (o, ev) => {
var full = Path.GetFullPath(repo + "\\" + node.Path);
Process.Start("explorer", $"/select,{full}");
ev.Handled = true;
};
var saveAs = new MenuItem();
saveAs.Header = App.Text("SaveAs");
saveAs.IsEnabled = node.Type == Models.ObjectType.Blob;
saveAs.Click += (obj, ev) => {
FolderBrowser.Open(null, App.Text("SaveFileTo"), saveTo => {
var full = Path.Combine(saveTo, Path.GetFileName(node.Path));
new Commands.SaveRevisionFile(repo, node.Path, sha, full).Exec();
});
ev.Handled = true;
};
var copyPath = new MenuItem();
copyPath.Header = App.Text("CopyPath");
copyPath.Click += (obj, ev) => {
Clipboard.SetText(node.Path);
ev.Handled = true;
};
var menu = new ContextMenu();
menu.Items.Add(history);
menu.Items.Add(blame);
menu.Items.Add(explore);
menu.Items.Add(saveAs);
menu.Items.Add(copyPath);
menu.IsOpen = true;
e.Handled = true;
}
private void OnRequestBringIntoView(object sender, RequestBringIntoViewEventArgs e) {
e.Handled = true;
}
#endregion
}
}