mirror of
https://github.com/sourcegit-scm/sourcegit.git
synced 2024-12-24 20:57:19 -08:00
refactor<CommitDetail>: move revision files to anthor view
This commit is contained in:
parent
0b6ca9ab8c
commit
bf7cc8eed4
6 changed files with 464 additions and 410 deletions
|
@ -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();
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
115
src/Views/Widgets/RevisionFiles.xaml
Normal file
115
src/Views/Widgets/RevisionFiles.xaml
Normal 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>
|
322
src/Views/Widgets/RevisionFiles.xaml.cs
Normal file
322
src/Views/Widgets/RevisionFiles.xaml.cs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue