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 { /// /// 提交信息面板中的文件列表分页 /// public partial class RevisionFiles : UserControl { private string repo = null; private string sha = null; private bool isLFSEnabled = false; private List cached = new List(); private string filter = null; /// /// 文件列表树节点 /// 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 IsExpanded { get; set; } = false; public bool IsFolder => Type == Models.ObjectType.None; public List Children { get; set; } = new List(); } public RevisionFiles() { InitializeComponent(); } public void SetData(string repo, string sha, Commands.Context cancelToken) { this.repo = repo; this.sha = sha; this.isLFSEnabled = new Commands.LFS(repo).IsEnabled(); var cmd = new Commands.RevisionObjects(repo, sha) { Ctx = cancelToken }; Task.Run(() => { var objects = cmd.Result(); if (cmd.Ctx.IsCancelRequested) return; cached = objects; ShowVisibles(); }); } public void Cleanup() { treeFiles.ItemsSource = new List(); cached = new List(); } private void ShowVisibles() { var nodes = new List(); var folders = new Dictionary(); var visibles = new List(); if (string.IsNullOrEmpty(filter)) { visibles.AddRange(cached); } else { foreach (var obj in cached) { if (obj.Path.ToUpper().Contains(filter)) visibles.Add(obj); } } var expanded = visibles.Count <= 50; foreach (var obj in visibles) { 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, IsExpanded = expanded, }; nodes.Add(lastFolder); folders.Add(folder, lastFolder); } else { var cur = new FileNode() { Type = Models.ObjectType.None, Path = folder, SHA = null, IsExpanded = expanded, }; 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, }); } } folders.Clear(); visibles.Clear(); SortFileNodes(nodes); Dispatcher.Invoke(() => { treeFiles.ItemsSource = nodes; GC.Collect(); }); } private void SortFileNodes(List 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 lines) { var maxLineNumber = $"{lines.Count + 1}"; var formatted = new FormattedText( maxLineNumber, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, new Typeface(txtPreviewData.FontFamily, 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() { PlacementTarget = grid }; 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.SetDataObject(builder.ToString(), true); }; 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 (isLFSEnabled && new Commands.LFS(repo).IsFiltered(node.Path)) { 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 = sender as Controls.TreeItem; 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 FileHistories(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) => { var dialog = new Controls.FolderDialog(); if (dialog.ShowDialog() == true) { var full = Path.Combine(dialog.SelectedPath, 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.SetDataObject(node.Path, true); ev.Handled = true; }; var menu = new ContextMenu() { PlacementTarget = item }; 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; } private void OnSearchFilterChanged(object sender, TextChangedEventArgs e) { var edit = sender as Controls.TextEdit; filter = edit.Text.ToUpper(); Task.Run(() => ShowVisibles()); e.Handled = true; } #endregion } }