From b192a1c4235f0080f1e3b444d265661a567d75cd Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 28 May 2024 21:19:53 +0800 Subject: [PATCH] refactor: use TreeDataGrid instead of TreeView/DataGrid to improve performance (#148) --- src/App.axaml | 1 + src/Models/TreeDataGridSelectionModel.cs | 421 +++++++++++++++++ src/Resources/Locales/en_US.axaml | 2 +- src/Resources/Locales/zh_CN.axaml | 2 +- src/Resources/Styles.axaml | 54 +++ src/SourceGit.csproj | 1 + src/ViewModels/CommitDetail.cs | 170 +++---- src/ViewModels/DiffContext.cs | 10 +- src/ViewModels/FileTreeNode.cs | 6 +- src/ViewModels/RevisionCompare.cs | 68 +-- src/ViewModels/WorkingCopy.cs | 556 +++++++++++------------ src/Views/ChangeCollectionView.axaml | 46 ++ src/Views/ChangeCollectionView.axaml.cs | 271 +++++++++++ src/Views/ChangeViewModeSwitcher.axaml | 6 +- src/Views/CommitChanges.axaml | 112 +---- src/Views/CommitChanges.axaml.cs | 37 +- src/Views/CommitDetail.axaml | 48 +- src/Views/CommitDetail.axaml.cs | 30 +- src/Views/RevisionCompare.axaml | 112 +---- src/Views/RevisionCompare.axaml.cs | 41 +- src/Views/RevisionFiles.axaml | 31 +- src/Views/RevisionFiles.axaml.cs | 14 +- src/Views/WorkingCopy.axaml | 258 ++--------- src/Views/WorkingCopy.axaml.cs | 366 +++------------ 24 files changed, 1333 insertions(+), 1330 deletions(-) create mode 100644 src/Models/TreeDataGridSelectionModel.cs create mode 100644 src/Views/ChangeCollectionView.axaml create mode 100644 src/Views/ChangeCollectionView.axaml.cs diff --git a/src/App.axaml b/src/App.axaml index 7129fd7e..fff2ca0d 100644 --- a/src/App.axaml +++ b/src/App.axaml @@ -19,6 +19,7 @@ + diff --git a/src/Models/TreeDataGridSelectionModel.cs b/src/Models/TreeDataGridSelectionModel.cs new file mode 100644 index 00000000..071d3414 --- /dev/null +++ b/src/Models/TreeDataGridSelectionModel.cs @@ -0,0 +1,421 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Models.TreeDataGrid; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Selection; +using Avalonia.Input; + +namespace SourceGit.Models +{ + public class TreeDataGridSelectionModel : TreeSelectionModelBase, + ITreeDataGridRowSelectionModel, + ITreeDataGridSelectionInteraction + where TModel : class + { + private static readonly Point s_InvalidPoint = new(double.NegativeInfinity, double.NegativeInfinity); + + private readonly ITreeDataGridSource _source; + private EventHandler _viewSelectionChanged; + private EventHandler _rowDoubleTapped; + private Point _pressedPoint = s_InvalidPoint; + private bool _raiseViewSelectionChanged; + private Func> _childrenGetter; + + public TreeDataGridSelectionModel(ITreeDataGridSource source, Func> childrenGetter) + : base(source.Items) + { + _source = source; + _childrenGetter = childrenGetter; + + SelectionChanged += (s, e) => + { + if (!IsSourceCollectionChanging) + _viewSelectionChanged?.Invoke(this, e); + else + _raiseViewSelectionChanged = true; + }; + } + + public void Select(IEnumerable items) + { + var sets = new HashSet(); + foreach (var item in items) + sets.Add(item); + + using (BatchUpdate()) + { + Clear(); + + int num = _source.Rows.Count; + for (int i = 0; i < num; ++i) + { + var m = _source.Rows[i].Model as TModel; + if (m != null && sets.Contains(m)) + { + var idx = _source.Rows.RowIndexToModelIndex(i); + Select(idx); + } + } + } + } + + event EventHandler ITreeDataGridSelectionInteraction.SelectionChanged + { + add => _viewSelectionChanged += value; + remove => _viewSelectionChanged -= value; + } + + public event EventHandler RowDoubleTapped + { + add => _rowDoubleTapped += value; + remove => _rowDoubleTapped -= value; + } + + IEnumerable ITreeDataGridSelection.Source + { + get => Source; + set => Source = value; + } + + bool ITreeDataGridSelectionInteraction.IsRowSelected(IRow rowModel) + { + if (rowModel is IModelIndexableRow indexable) + return IsSelected(indexable.ModelIndexPath); + return false; + } + + bool ITreeDataGridSelectionInteraction.IsRowSelected(int rowIndex) + { + if (rowIndex >= 0 && rowIndex < _source.Rows.Count) + { + if (_source.Rows[rowIndex] is IModelIndexableRow indexable) + return IsSelected(indexable.ModelIndexPath); + } + + return false; + } + + void ITreeDataGridSelectionInteraction.OnKeyDown(TreeDataGrid sender, KeyEventArgs e) + { + if (sender.RowsPresenter is null) + return; + + if (!e.Handled) + { + var ctrl = e.KeyModifiers.HasFlag(KeyModifiers.Control); + if (e.Key == Key.A && ctrl && !SingleSelect) + { + using (BatchUpdate()) + { + Clear(); + + int num = _source.Rows.Count; + for (int i = 0; i < num; ++i) + { + var m = _source.Rows.RowIndexToModelIndex(i); + Select(m); + } + } + e.Handled = true; + } + + var direction = e.Key.ToNavigationDirection(); + var shift = e.KeyModifiers.HasFlag(KeyModifiers.Shift); + if (direction.HasValue) + { + var anchorRowIndex = _source.Rows.ModelIndexToRowIndex(AnchorIndex); + sender.RowsPresenter.BringIntoView(anchorRowIndex); + + var anchor = sender.TryGetRow(anchorRowIndex); + if (anchor is not null && !ctrl) + { + e.Handled = TryKeyExpandCollapse(sender, direction.Value, anchor); + } + + if (!e.Handled && (!ctrl || shift)) + { + e.Handled = MoveSelection(sender, direction.Value, shift, anchor); + } + + if (!e.Handled && direction == NavigationDirection.Left + && anchor?.Rows is HierarchicalRows hierarchicalRows && anchorRowIndex > 0) + { + var newIndex = hierarchicalRows.GetParentRowIndex(AnchorIndex); + UpdateSelection(sender, newIndex, true); + FocusRow(sender, sender.RowsPresenter.BringIntoView(newIndex)); + } + + if (!e.Handled && direction == NavigationDirection.Right + && anchor?.Rows is HierarchicalRows hierarchicalRows2 && hierarchicalRows2[anchorRowIndex].IsExpanded) + { + var newIndex = anchorRowIndex + 1; + UpdateSelection(sender, newIndex, true); + sender.RowsPresenter.BringIntoView(newIndex); + } + } + } + } + + void ITreeDataGridSelectionInteraction.OnPointerPressed(TreeDataGrid sender, PointerPressedEventArgs e) + { + if (!e.Handled && + e.Pointer.Type == PointerType.Mouse && + e.Source is Control source && + sender.TryGetRow(source, out var row) && + _source.Rows.RowIndexToModelIndex(row.RowIndex) is { } modelIndex) + { + if (!IsSelected(modelIndex)) + { + PointerSelect(sender, row, e); + } + else + { + var point = e.GetCurrentPoint(sender); + if (point.Properties.IsRightButtonPressed) + return; + + if (e.KeyModifiers == KeyModifiers.Control) + { + Deselect(modelIndex); + } + else if (e.ClickCount == 2) + { + _rowDoubleTapped?.Invoke(this, e); + } + else + { + using (BatchUpdate()) + { + Clear(); + Select(modelIndex); + } + } + } + + _pressedPoint = s_InvalidPoint; + } + else + { + if (!sender.TryGetRow(e.Source as Control, out var test)) + Clear(); + + _pressedPoint = e.GetPosition(sender); + } + } + + void ITreeDataGridSelectionInteraction.OnPointerReleased(TreeDataGrid sender, PointerReleasedEventArgs e) + { + if (!e.Handled && + _pressedPoint != s_InvalidPoint && + e.Source is Control source && + sender.TryGetRow(source, out var row) && + _source.Rows.RowIndexToModelIndex(row.RowIndex) is { } modelIndex) + { + if (!IsSelected(modelIndex)) + { + var p = e.GetPosition(sender); + if (Math.Abs(p.X - _pressedPoint.X) <= 3 || Math.Abs(p.Y - _pressedPoint.Y) <= 3) + PointerSelect(sender, row, e); + } + } + } + + protected override void OnSourceCollectionChangeFinished() + { + if (_raiseViewSelectionChanged) + { + _viewSelectionChanged?.Invoke(this, EventArgs.Empty); + _raiseViewSelectionChanged = false; + } + } + + private void PointerSelect(TreeDataGrid sender, TreeDataGridRow row, PointerEventArgs e) + { + var point = e.GetCurrentPoint(sender); + + var commandModifiers = TopLevel.GetTopLevel(sender)?.PlatformSettings?.HotkeyConfiguration.CommandModifiers; + var toggleModifier = commandModifiers is not null && e.KeyModifiers.HasFlag(commandModifiers); + var isRightButton = point.Properties.PointerUpdateKind is PointerUpdateKind.RightButtonPressed or + PointerUpdateKind.RightButtonReleased; + + UpdateSelection( + sender, + row.RowIndex, + select: true, + rangeModifier: e.KeyModifiers.HasFlag(KeyModifiers.Shift), + toggleModifier: toggleModifier, + rightButton: isRightButton); + e.Handled = true; + } + + private void UpdateSelection(TreeDataGrid treeDataGrid, int rowIndex, bool select = true, bool rangeModifier = false, bool toggleModifier = false, bool rightButton = false) + { + var modelIndex = _source.Rows.RowIndexToModelIndex(rowIndex); + if (modelIndex == default) + return; + + var mode = SingleSelect ? SelectionMode.Single : SelectionMode.Multiple; + var multi = (mode & SelectionMode.Multiple) != 0; + var toggle = (toggleModifier || (mode & SelectionMode.Toggle) != 0); + var range = multi && rangeModifier; + + if (!select) + { + if (IsSelected(modelIndex) && !treeDataGrid.QueryCancelSelection()) + Deselect(modelIndex); + } + else if (rightButton) + { + if (IsSelected(modelIndex) == false && !treeDataGrid.QueryCancelSelection()) + SelectedIndex = modelIndex; + } + else if (range) + { + if (!treeDataGrid.QueryCancelSelection()) + { + var anchor = RangeAnchorIndex; + var i = Math.Max(_source.Rows.ModelIndexToRowIndex(anchor), 0); + var step = i < rowIndex ? 1 : -1; + + using (BatchUpdate()) + { + Clear(); + + while (true) + { + var m = _source.Rows.RowIndexToModelIndex(i); + Select(m); + anchor = m; + if (i == rowIndex) + break; + i += step; + } + } + } + } + else if (multi && toggle) + { + if (!treeDataGrid.QueryCancelSelection()) + { + if (IsSelected(modelIndex) == true) + Deselect(modelIndex); + else + Select(modelIndex); + } + } + else if (toggle) + { + if (!treeDataGrid.QueryCancelSelection()) + SelectedIndex = (SelectedIndex == modelIndex) ? -1 : modelIndex; + } + else if (SelectedIndex != modelIndex || Count > 1) + { + if (!treeDataGrid.QueryCancelSelection()) + SelectedIndex = modelIndex; + } + } + + private bool TryKeyExpandCollapse(TreeDataGrid treeDataGrid, NavigationDirection direction, TreeDataGridRow focused) + { + if (treeDataGrid.RowsPresenter is null || focused.RowIndex < 0) + return false; + + var row = _source.Rows[focused.RowIndex]; + + if (row is IExpander expander) + { + if (direction == NavigationDirection.Right && !expander.IsExpanded) + { + expander.IsExpanded = true; + return true; + } + else if (direction == NavigationDirection.Left && expander.IsExpanded) + { + expander.IsExpanded = false; + return true; + } + } + + return false; + } + + private bool MoveSelection(TreeDataGrid treeDataGrid, NavigationDirection direction, bool rangeModifier, TreeDataGridRow focused) + { + if (treeDataGrid.RowsPresenter is null || _source.Columns.Count == 0 || _source.Rows.Count == 0) + return false; + + var currentRowIndex = focused?.RowIndex ?? _source.Rows.ModelIndexToRowIndex(SelectedIndex); + int newRowIndex; + + if (direction == NavigationDirection.First || direction == NavigationDirection.Last) + { + newRowIndex = direction == NavigationDirection.First ? 0 : _source.Rows.Count - 1; + } + else + { + (var x, var y) = direction switch + { + NavigationDirection.Up => (0, -1), + NavigationDirection.Down => (0, 1), + NavigationDirection.Left => (-1, 0), + NavigationDirection.Right => (1, 0), + _ => (0, 0) + }; + + newRowIndex = Math.Max(0, Math.Min(currentRowIndex + y, _source.Rows.Count - 1)); + } + + if (newRowIndex != currentRowIndex) + UpdateSelection(treeDataGrid, newRowIndex, true, rangeModifier); + + if (newRowIndex != currentRowIndex) + { + treeDataGrid.RowsPresenter?.BringIntoView(newRowIndex); + FocusRow(treeDataGrid, treeDataGrid.TryGetRow(newRowIndex)); + return true; + } + else + { + return false; + } + } + + private static void FocusRow(TreeDataGrid owner, Control control) + { + if (!owner.TryGetRow(control, out var row) || row.CellsPresenter is null) + return; + + // Get the column index of the currently focused cell if possible: we'll try to focus the + // same column in the new row. + if (TopLevel.GetTopLevel(owner)?.FocusManager is { } focusManager && + focusManager.GetFocusedElement() is Control currentFocus && + owner.TryGetCell(currentFocus, out var currentCell) && + row.TryGetCell(currentCell.ColumnIndex) is { } newCell && + newCell.Focusable) + { + newCell.Focus(); + } + else + { + // Otherwise, just focus the first focusable cell in the row. + foreach (var cell in row.CellsPresenter.GetRealizedElements()) + { + if (cell.Focusable) + { + cell.Focus(); + break; + } + } + } + } + + protected override IEnumerable GetChildren(TModel node) + { + return _childrenGetter?.Invoke(node); + } + } +} diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index bc2bc9c0..ea98a86b 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -330,7 +330,7 @@ Remote : Push Changes To Remote Remote Branch : - Tracking remote branch(--set-upstream) + Tracking remote branch Push all tags Push Tag To Remote Push to all remotes diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 3dffd574..adb90a98 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -330,7 +330,7 @@ 远程仓库 : 推送到远程仓库 远程分支 : - 跟踪远程分支(--set-upstream) + 跟踪远程分支 同时推送标签 推送标签到远程仓库 推送到所有远程仓库 diff --git a/src/Resources/Styles.axaml b/src/Resources/Styles.axaml index 376f9424..f7eb7054 100644 --- a/src/Resources/Styles.axaml +++ b/src/Resources/Styles.axaml @@ -1075,4 +1075,58 @@ + + + + + + + + diff --git a/src/SourceGit.csproj b/src/SourceGit.csproj index 657b49f3..a3d38391 100644 --- a/src/SourceGit.csproj +++ b/src/SourceGit.csproj @@ -33,6 +33,7 @@ + diff --git a/src/ViewModels/CommitDetail.cs b/src/ViewModels/CommitDetail.cs index 6fdcd93c..9fd320fd 100644 --- a/src/ViewModels/CommitDetail.cs +++ b/src/ViewModels/CommitDetail.cs @@ -4,6 +4,8 @@ using System.IO; using System.Threading.Tasks; using Avalonia.Controls; +using Avalonia.Controls.Models.TreeDataGrid; +using Avalonia.Interactivity; using Avalonia.Media.Imaging; using Avalonia.Platform.Storage; using Avalonia.Threading; @@ -48,48 +50,17 @@ namespace SourceGit.ViewModels set => SetProperty(ref _visibleChanges, value); } - public List ChangeTree + public List SelectedChanges { - get => _changeTree; - set => SetProperty(ref _changeTree, value); - } - - public Models.Change SelectedChange - { - get => _selectedChange; + get => _selectedChanges; set { - if (SetProperty(ref _selectedChange, value)) + if (SetProperty(ref _selectedChanges, value)) { - if (value == null) - { - SelectedChangeNode = null; + if (value == null || value.Count != 1) DiffContext = null; - } else - { - SelectedChangeNode = FileTreeNode.SelectByPath(_changeTree, value.Path); - DiffContext = new DiffContext(_repo, new Models.DiffOption(_commit, value), _diffContext); - } - } - } - } - - public FileTreeNode SelectedChangeNode - { - get => _selectedChangeNode; - set - { - if (SetProperty(ref _selectedChangeNode, value)) - { - if (value == null) - { - SelectedChange = null; - } - else - { - SelectedChange = value.Backend as Models.Change; - } + DiffContext = new DiffContext(_repo, new Models.DiffOption(_commit, value[0]), _diffContext); } } } @@ -106,26 +77,10 @@ namespace SourceGit.ViewModels } } - public List RevisionFilesTree + public HierarchicalTreeDataGridSource RevisionFiles { - get => _revisionFilesTree; - set => SetProperty(ref _revisionFilesTree, value); - } - - public FileTreeNode SelectedRevisionFileNode - { - get => _selectedRevisionFileNode; - set - { - if (SetProperty(ref _selectedRevisionFileNode, value) && value != null && !value.IsFolder) - { - RefreshViewRevisionFile(value.Backend as Models.Object); - } - else - { - ViewRevisionFileContent = null; - } - } + get => _revisionFiles; + private set => SetProperty(ref _revisionFiles, value); } public string SearchFileFilter @@ -159,17 +114,14 @@ namespace SourceGit.ViewModels _changes.Clear(); if (_visibleChanges != null) _visibleChanges.Clear(); - if (_changeTree != null) - _changeTree.Clear(); - _selectedChange = null; - _selectedChangeNode = null; + if (_selectedChanges != null) + _selectedChanges.Clear(); _searchChangeFilter = null; _diffContext = null; + if (_revisionFilesBackup != null) + _revisionFilesBackup.Clear(); if (_revisionFiles != null) - _revisionFiles.Clear(); - if (_revisionFilesTree != null) - _revisionFilesTree.Clear(); - _selectedRevisionFileNode = null; + _revisionFiles.Dispose(); _searchFileFilter = null; _viewRevisionFileContent = null; _cancelToken = null; @@ -346,9 +298,14 @@ namespace SourceGit.ViewModels { _changes = null; VisibleChanges = null; - SelectedChange = null; - RevisionFilesTree = null; - SelectedRevisionFileNode = null; + SelectedChanges = null; + + if (_revisionFiles != null) + { + _revisionFiles.Dispose(); + _revisionFiles = null; + } + if (_commit == null) return; if (_cancelToken != null) @@ -379,40 +336,34 @@ namespace SourceGit.ViewModels } } - var tree = FileTreeNode.Build(visible); + var tree = FileTreeNode.Build(visible, true); Dispatcher.UIThread.Invoke(() => { Changes = changes; VisibleChanges = visible; - ChangeTree = tree; }); }); Task.Run(() => { - var files = cmdRevisionFiles.Result(); + _revisionFilesBackup = cmdRevisionFiles.Result(); if (cmdRevisionFiles.Cancel.Requested) return; - var visible = files; - if (!string.IsNullOrWhiteSpace(_searchFileFilter)) + var visible = _revisionFilesBackup; + var isSearching = !string.IsNullOrWhiteSpace(_searchFileFilter); + if (isSearching) { visible = new List(); - foreach (var f in files) + foreach (var f in _revisionFilesBackup) { if (f.Path.Contains(_searchFileFilter, StringComparison.OrdinalIgnoreCase)) - { visible.Add(f); - } } } - var tree = FileTreeNode.Build(visible); - Dispatcher.UIThread.Invoke(() => - { - _revisionFiles = files; - RevisionFilesTree = tree; - }); + var tree = FileTreeNode.Build(visible, isSearching || visible.Count <= 100); + Dispatcher.UIThread.Invoke(() => BuildRevisionFilesSource(tree)); }); } @@ -431,15 +382,11 @@ namespace SourceGit.ViewModels foreach (var c in _changes) { if (c.Path.Contains(_searchChangeFilter, StringComparison.OrdinalIgnoreCase)) - { visible.Add(c); - } } VisibleChanges = visible; } - - ChangeTree = FileTreeNode.Build(_visibleChanges); } private void RefreshVisibleFiles() @@ -447,24 +394,29 @@ namespace SourceGit.ViewModels if (_revisionFiles == null) return; - var visible = _revisionFiles; - if (!string.IsNullOrWhiteSpace(_searchFileFilter)) + var visible = _revisionFilesBackup; + var isSearching = !string.IsNullOrWhiteSpace(_searchFileFilter); + if (isSearching) { visible = new List(); - foreach (var f in _revisionFiles) + foreach (var f in _revisionFilesBackup) { if (f.Path.Contains(_searchFileFilter, StringComparison.OrdinalIgnoreCase)) - { visible.Add(f); - } } } - RevisionFilesTree = FileTreeNode.Build(visible); + BuildRevisionFilesSource(FileTreeNode.Build(visible, isSearching || visible.Count < 100)); } private void RefreshViewRevisionFile(Models.Object file) { + if (file == null) + { + ViewRevisionFileContent = null; + return; + } + switch (file.Type) { case Models.ObjectType.Blob: @@ -541,6 +493,35 @@ namespace SourceGit.ViewModels } } + private void BuildRevisionFilesSource(List tree) + { + var source = new HierarchicalTreeDataGridSource(tree) + { + Columns = + { + new HierarchicalExpanderColumn( + new TemplateColumn("Icon", "FileTreeNodeExpanderTemplate", null, GridLength.Auto), + x => x.Children, + x => x.Children.Count > 0, + x => x.IsExpanded), + new TextColumn( + null, + x => string.Empty, + GridLength.Star) + } + }; + + source.Selection = new Models.TreeDataGridSelectionModel(source, x => x.Children); + source.RowSelection.SingleSelect = true; + source.RowSelection.SelectionChanged += (s, _) => + { + if (s is Models.TreeDataGridSelectionModel selection) + RefreshViewRevisionFile(selection.SelectedItem?.Backend as Models.Object); + }; + + RevisionFiles = source; + } + private static readonly HashSet IMG_EXTS = new HashSet() { ".ico", ".bmp", ".jpg", ".png", ".jpeg" @@ -551,14 +532,11 @@ namespace SourceGit.ViewModels private Models.Commit _commit = null; private List _changes = null; private List _visibleChanges = null; - private List _changeTree = null; - private Models.Change _selectedChange = null; - private FileTreeNode _selectedChangeNode = null; + private List _selectedChanges = null; private string _searchChangeFilter = string.Empty; private DiffContext _diffContext = null; - private List _revisionFiles = null; - private List _revisionFilesTree = null; - private FileTreeNode _selectedRevisionFileNode = null; + private List _revisionFilesBackup = null; + private HierarchicalTreeDataGridSource _revisionFiles = null; private string _searchFileFilter = string.Empty; private object _viewRevisionFileContent = null; private Commands.Command.CancelToken _cancelToken = null; diff --git a/src/ViewModels/DiffContext.cs b/src/ViewModels/DiffContext.cs index 3aad22ea..d05f0c57 100644 --- a/src/ViewModels/DiffContext.cs +++ b/src/ViewModels/DiffContext.cs @@ -85,6 +85,11 @@ namespace SourceGit.ViewModels _content = previous._content; } + if (string.IsNullOrEmpty(_option.OrgPath) || _option.OrgPath == "/dev/null") + _title = _option.Path; + else + _title = $"{_option.OrgPath} → {_option.Path}"; + LoadDiffContent(); } @@ -175,11 +180,6 @@ namespace SourceGit.ViewModels Dispatcher.UIThread.Post(() => { - if (string.IsNullOrEmpty(_option.OrgPath) || _option.OrgPath == "/dev/null") - Title = _option.Path; - else - Title = $"{_option.OrgPath} → {_option.Path}"; - FileModeChange = latest.FileModeChange; Content = rs; IsTextDiff = latest.TextDiff != null; diff --git a/src/ViewModels/FileTreeNode.cs b/src/ViewModels/FileTreeNode.cs index ca6d850f..8e3ba076 100644 --- a/src/ViewModels/FileTreeNode.cs +++ b/src/ViewModels/FileTreeNode.cs @@ -18,11 +18,10 @@ namespace SourceGit.ViewModels set => SetProperty(ref _isExpanded, value); } - public static List Build(List changes) + public static List Build(List changes, bool expanded) { var nodes = new List(); var folders = new Dictionary(); - var expanded = changes.Count <= 50; foreach (var c in changes) { @@ -94,11 +93,10 @@ namespace SourceGit.ViewModels return nodes; } - public static List Build(List files) + public static List Build(List files, bool expanded) { var nodes = new List(); var folders = new Dictionary(); - var expanded = files.Count <= 50; foreach (var f in files) { diff --git a/src/ViewModels/RevisionCompare.cs b/src/ViewModels/RevisionCompare.cs index a2fd25ca..536d71bc 100644 --- a/src/ViewModels/RevisionCompare.cs +++ b/src/ViewModels/RevisionCompare.cs @@ -35,48 +35,17 @@ namespace SourceGit.ViewModels private set => SetProperty(ref _visibleChanges, value); } - public List ChangeTree + public List SelectedChanges { - get => _changeTree; - private set => SetProperty(ref _changeTree, value); - } - - public Models.Change SelectedChange - { - get => _selectedChange; + get => _selectedChanges; set { - if (SetProperty(ref _selectedChange, value)) + if (SetProperty(ref _selectedChanges, value)) { - if (value == null) - { - SelectedNode = null; + if (value != null && value.Count == 1) + DiffContext = new DiffContext(_repo, new Models.DiffOption(StartPoint.SHA, _endPoint, value[0]), _diffContext); + else DiffContext = null; - } - else - { - SelectedNode = FileTreeNode.SelectByPath(_changeTree, value.Path); - DiffContext = new DiffContext(_repo, new Models.DiffOption(StartPoint.SHA, _endPoint, value), _diffContext); - } - } - } - } - - public FileTreeNode SelectedNode - { - get => _selectedNode; - set - { - if (SetProperty(ref _selectedNode, value)) - { - if (value == null) - { - SelectedChange = null; - } - else - { - SelectedChange = value.Backend as Models.Change; - } } } } @@ -126,17 +95,14 @@ namespace SourceGit.ViewModels foreach (var c in _changes) { if (c.Path.Contains(_searchFilter, StringComparison.OrdinalIgnoreCase)) - { visible.Add(c); - } } } - var tree = FileTreeNode.Build(visible); + var tree = FileTreeNode.Build(visible, true); Dispatcher.UIThread.Invoke(() => { VisibleChanges = visible; - ChangeTree = tree; }); }); } @@ -148,10 +114,8 @@ namespace SourceGit.ViewModels _changes.Clear(); if (_visibleChanges != null) _visibleChanges.Clear(); - if (_changeTree != null) - _changeTree.Clear(); - _selectedChange = null; - _selectedNode = null; + if (_selectedChanges != null) + _selectedChanges.Clear(); _searchFilter = null; _diffContext = null; } @@ -168,8 +132,12 @@ namespace SourceGit.ViewModels SearchFilter = string.Empty; } - public ContextMenu CreateChangeContextMenu(Models.Change change) + public ContextMenu CreateChangeContextMenu() { + if (_selectedChanges == null || _selectedChanges.Count != 1) + return null; + + var change = _selectedChanges[0]; var menu = new ContextMenu(); var diffWithMerger = new MenuItem(); @@ -237,24 +205,18 @@ namespace SourceGit.ViewModels foreach (var c in _changes) { if (c.Path.Contains(_searchFilter, StringComparison.OrdinalIgnoreCase)) - { visible.Add(c); - } } VisibleChanges = visible; } - - ChangeTree = FileTreeNode.Build(_visibleChanges); } private string _repo = string.Empty; private string _endPoint = string.Empty; private List _changes = null; private List _visibleChanges = null; - private List _changeTree = null; - private Models.Change _selectedChange = null; - private FileTreeNode _selectedNode = null; + private List _selectedChanges = null; private string _searchFilter = string.Empty; private DiffContext _diffContext = null; } diff --git a/src/ViewModels/WorkingCopy.cs b/src/ViewModels/WorkingCopy.cs index 76c3be50..e8897d48 100644 --- a/src/ViewModels/WorkingCopy.cs +++ b/src/ViewModels/WorkingCopy.cs @@ -58,7 +58,23 @@ namespace SourceGit.ViewModels public bool UseAmend { get => _useAmend; - set => SetProperty(ref _useAmend, value); + set + { + if (SetProperty(ref _useAmend, value) && value) + { + var commits = new Commands.QueryCommits(_repo.FullPath, "-n 1", false).Result(); + if (commits.Count == 0) + { + App.RaiseException(_repo.FullPath, "No commits to amend!!!"); + _useAmend = false; + OnPropertyChanged(); + } + else + { + CommitMessage = commits[0].FullMessage; + } + } + } } public List Unstaged @@ -73,103 +89,58 @@ namespace SourceGit.ViewModels private set => SetProperty(ref _staged, value); } - public int Count + public List SelectedUnstaged { - get => _count; - } - - public Models.Change SelectedUnstagedChange - { - get => _selectedUnstagedChange; + get => _selectedUnstaged; set { - if (SetProperty(ref _selectedUnstagedChange, value) && value != null) + if (SetProperty(ref _selectedUnstaged, value)) { - SelectedStagedChange = null; - SelectedStagedTreeNode = null; - SetDetail(value, true); - } - } - } - - public Models.Change SelectedStagedChange - { - get => _selectedStagedChange; - set - { - if (SetProperty(ref _selectedStagedChange, value) && value != null) - { - SelectedUnstagedChange = null; - SelectedUnstagedTreeNode = null; - SetDetail(value, false); - } - } - } - - public List UnstagedTree - { - get => _unstagedTree; - private set => SetProperty(ref _unstagedTree, value); - } - - public List StagedTree - { - get => _stagedTree; - private set => SetProperty(ref _stagedTree, value); - } - - public FileTreeNode SelectedUnstagedTreeNode - { - get => _selectedUnstagedTreeNode; - set - { - if (SetProperty(ref _selectedUnstagedTreeNode, value)) - { - if (value == null) + if (value == null || value.Count == 0) { - SelectedUnstagedChange = null; + if (_selectedStaged == null || _selectedStaged.Count == 0) + SetDetail(null); } else { - SelectedUnstagedChange = value.Backend as Models.Change; - SelectedStagedTreeNode = null; - SelectedStagedChange = null; + SelectedStaged = null; - if (value.IsFolder) - { - SetDetail(null, true); - } + if (value.Count == 1) + SetDetail(value[0]); + else + SetDetail(null); } } } } - public FileTreeNode SelectedStagedTreeNode + public List SelectedStaged { - get => _selectedStagedTreeNode; + get => _selectedStaged; set { - if (SetProperty(ref _selectedStagedTreeNode, value)) + if (SetProperty(ref _selectedStaged, value)) { - if (value == null) + if (value == null || value.Count == 0) { - SelectedStagedChange = null; + if (_selectedUnstaged == null || _selectedUnstaged.Count == 0) + SetDetail(null); } else { - SelectedStagedChange = value.Backend as Models.Change; - SelectedUnstagedTreeNode = null; - SelectedUnstagedChange = null; + SelectedUnstaged = null; - if (value.IsFolder) - { - SetDetail(null, false); - } + if (value.Count == 1) + SetDetail(value[0]); + else + SetDetail(null); } } } } + public int Count => _count; + public object DetailContext { get => _detailContext; @@ -194,14 +165,10 @@ namespace SourceGit.ViewModels _unstaged.Clear(); if (_staged != null) _staged.Clear(); - if (_unstagedTree != null) - _unstagedTree.Clear(); - if (_stagedTree != null) - _stagedTree.Clear(); - _selectedUnstagedChange = null; - _selectedStagedChange = null; - _selectedUnstagedTreeNode = null; - _selectedStagedTreeNode = null; + if (_selectedUnstaged != null) + _selectedUnstaged.Clear(); + if (_selectedStaged != null) + _selectedStaged.Clear(); _detailContext = null; _commitMessage = string.Empty; } @@ -210,20 +177,22 @@ namespace SourceGit.ViewModels { var unstaged = new List(); var staged = new List(); + var selectedUnstaged = new List(); + var selectedStaged = new List(); - var viewFile = string.Empty; - var lastSelectedIsUnstaged = false; - if (_selectedUnstagedChange != null) + var lastSelectedUnstaged = new HashSet(); + var lastSelectedStaged = new HashSet(); + if (_selectedUnstaged != null) { - viewFile = _selectedUnstagedChange.Path; - lastSelectedIsUnstaged = true; + foreach (var c in _selectedUnstaged) + lastSelectedUnstaged.Add(c.Path); } - else if (_selectedStagedChange != null) + else if (_selectedStaged != null) { - viewFile = _selectedStagedChange.Path; + foreach (var c in _selectedStaged) + lastSelectedStaged.Add(c.Path); } - var viewChange = null as Models.Change; var hasConflict = false; foreach (var c in changes) { @@ -233,65 +202,43 @@ namespace SourceGit.ViewModels || c.Index == Models.ChangeState.Renamed) { staged.Add(c); - if (!lastSelectedIsUnstaged && c.Path == viewFile) - { - viewChange = c; - } + + if (lastSelectedStaged.Contains(c.Path)) + selectedStaged.Add(c); } if (c.WorkTree != Models.ChangeState.None) { unstaged.Add(c); hasConflict |= c.IsConflit; - if (lastSelectedIsUnstaged && c.Path == viewFile) - { - viewChange = c; - } + + if (lastSelectedUnstaged.Contains(c.Path)) + selectedUnstaged.Add(c); } } _count = changes.Count; - var unstagedTree = FileTreeNode.Build(unstaged); - var stagedTree = FileTreeNode.Build(staged); Dispatcher.UIThread.Invoke(() => { _isLoadingData = true; Unstaged = unstaged; Staged = staged; - UnstagedTree = unstagedTree; - StagedTree = stagedTree; _isLoadingData = false; - // Restore last selection states. - if (viewChange != null) - { - var scrollOffset = Vector.Zero; - if (_detailContext is DiffContext old) - scrollOffset = old.SyncScrollOffset; + var scrollOffset = Vector.Zero; + if (_detailContext is DiffContext old) + scrollOffset = old.SyncScrollOffset; - if (lastSelectedIsUnstaged) - { - SelectedUnstagedChange = viewChange; - SelectedUnstagedTreeNode = FileTreeNode.SelectByPath(_unstagedTree, viewFile); - } - else - { - SelectedStagedChange = viewChange; - SelectedStagedTreeNode = FileTreeNode.SelectByPath(_stagedTree, viewFile); - } - - if (_detailContext is DiffContext cur) - cur.SyncScrollOffset = scrollOffset; - } + if (selectedUnstaged.Count > 0) + SelectedUnstaged = selectedUnstaged; + else if (selectedStaged.Count > 0) + SelectedStaged = selectedStaged; else - { - SelectedUnstagedChange = null; - SelectedUnstagedTreeNode = null; - SelectedStagedChange = null; - SelectedStagedTreeNode = null; - SetDetail(null, false); - } + SetDetail(null); + + if (_detailContext is DiffContext cur) + cur.SyncScrollOffset = scrollOffset; // Try to load merge message from MERGE_MSG if (string.IsNullOrEmpty(_commitMessage)) @@ -305,30 +252,24 @@ namespace SourceGit.ViewModels return hasConflict; } - public void SetDetail(Models.Change change, bool isUnstaged) + public void OpenAssumeUnchanged() { - if (_isLoadingData) - return; + var dialog = new Views.AssumeUnchangedManager() + { + DataContext = new AssumeUnchangedManager(_repo.FullPath) + }; - if (change == null) - { - DetailContext = null; - } - else if (change.IsConflit && isUnstaged) - { - DetailContext = new ConflictContext(_repo.FullPath, change); - } - else - { - if (_detailContext is DiffContext previous) - { - DetailContext = new DiffContext(_repo.FullPath, new Models.DiffOption(change, isUnstaged), previous); - } - else - { - DetailContext = new DiffContext(_repo.FullPath, new Models.DiffOption(change, isUnstaged)); - } - } + dialog.ShowDialog(App.GetTopLevel() as Window); + } + + public void StageSelected() + { + StageChanges(_selectedUnstaged); + } + + public void StageAll() + { + StageChanges(_unstaged); } public async void StageChanges(List changes) @@ -336,7 +277,7 @@ namespace SourceGit.ViewModels if (_unstaged.Count == 0 || changes.Count == 0) return; - SetDetail(null, true); + SetDetail(null); IsStaging = true; _repo.SetWatcherEnabled(false); if (changes.Count == _unstaged.Count) @@ -357,12 +298,22 @@ namespace SourceGit.ViewModels IsStaging = false; } + public void UnstageSelected() + { + UnstageChanges(_selectedStaged); + } + + public void UnstageAll() + { + UnstageChanges(_staged); + } + public async void UnstageChanges(List changes) { if (_staged.Count == 0 || changes.Count == 0) return; - SetDetail(null, false); + SetDetail(null); IsUnstaging = true; _repo.SetWatcherEnabled(false); if (changes.Count == _staged.Count) @@ -412,113 +363,25 @@ namespace SourceGit.ViewModels } } - public async void UseTheirs(List changes) + public void Commit() { - var files = new List(); - foreach (var change in changes) - { - if (change.IsConflit) - files.Add(change.Path); - } - - _repo.SetWatcherEnabled(false); - var succ = await Task.Run(() => new Commands.Checkout(_repo.FullPath).UseTheirs(files)); - if (succ) - { - await Task.Run(() => new Commands.Add(_repo.FullPath, changes).Exec()); - } - _repo.MarkWorkingCopyDirtyManually(); - _repo.SetWatcherEnabled(true); + DoCommit(false); } - public async void UseMine(List changes) + public void CommitWithPush() { - var files = new List(); - foreach (var change in changes) - { - if (change.IsConflit) - files.Add(change.Path); - } - - _repo.SetWatcherEnabled(false); - var succ = await Task.Run(() => new Commands.Checkout(_repo.FullPath).UseMine(files)); - if (succ) - { - await Task.Run(() => new Commands.Add(_repo.FullPath, changes).Exec()); - } - _repo.MarkWorkingCopyDirtyManually(); - _repo.SetWatcherEnabled(true); + DoCommit(true); } - public async void UseExternalMergeTool(Models.Change change) + public ContextMenu CreateContextMenuForUnstagedChanges() { - var type = Preference.Instance.ExternalMergeToolType; - var exec = Preference.Instance.ExternalMergeToolPath; - - var tool = Models.ExternalMerger.Supported.Find(x => x.Type == type); - if (tool == null) - { - App.RaiseException(_repo.FullPath, "Invalid merge tool in preference setting!"); - return; - } - - var args = tool.Type != 0 ? tool.Cmd : Preference.Instance.ExternalMergeToolCmd; - - _repo.SetWatcherEnabled(false); - await Task.Run(() => Commands.MergeTool.OpenForMerge(_repo.FullPath, exec, args, change.Path)); - _repo.SetWatcherEnabled(true); - } - - public async void DoCommit(bool autoPush) - { - if (!PopupHost.CanCreatePopup()) - { - App.RaiseException(_repo.FullPath, "Repository has unfinished job! Please wait!"); - return; - } - - if (_staged.Count == 0) - { - App.RaiseException(_repo.FullPath, "No files added to commit!"); - return; - } - - if (string.IsNullOrWhiteSpace(_commitMessage)) - { - App.RaiseException(_repo.FullPath, "Commit without message is NOT allowed!"); - return; - } - - PushCommitMessage(); - - SetDetail(null, false); - IsCommitting = true; - _repo.SetWatcherEnabled(false); - var succ = await Task.Run(() => new Commands.Commit(_repo.FullPath, _commitMessage, _useAmend).Exec()); - if (succ) - { - CommitMessage = string.Empty; - UseAmend = false; - - if (autoPush) - { - PopupHost.ShowAndStartPopup(new Push(_repo, null)); - } - } - _repo.MarkWorkingCopyDirtyManually(); - _repo.SetWatcherEnabled(true); - IsCommitting = false; - } - - public ContextMenu CreateContextMenuForUnstagedChanges(List changes) - { - if (changes.Count == 0) + if (_selectedUnstaged.Count == 0) return null; var menu = new ContextMenu(); - if (changes.Count == 1) + if (_selectedUnstaged.Count == 1) { - var change = changes[0]; + var change = _selectedUnstaged[0]; var path = Path.GetFullPath(Path.Combine(_repo.FullPath, change.Path)); var explore = new MenuItem(); @@ -551,7 +414,7 @@ namespace SourceGit.ViewModels useTheirs.Header = App.Text("FileCM.UseTheirs"); useTheirs.Click += (_, e) => { - UseTheirs(changes); + UseTheirs(_selectedUnstaged); e.Handled = true; }; @@ -560,7 +423,7 @@ namespace SourceGit.ViewModels useMine.Header = App.Text("FileCM.UseMine"); useMine.Click += (_, e) => { - UseMine(changes); + UseMine(_selectedUnstaged); e.Handled = true; }; @@ -585,7 +448,7 @@ namespace SourceGit.ViewModels stage.Icon = App.CreateMenuIcon("Icons.File.Add"); stage.Click += (_, e) => { - StageChanges(changes); + StageChanges(_selectedUnstaged); e.Handled = true; }; @@ -594,7 +457,7 @@ namespace SourceGit.ViewModels discard.Icon = App.CreateMenuIcon("Icons.Undo"); discard.Click += (_, e) => { - Discard(changes, true); + Discard(_selectedUnstaged, true); e.Handled = true; }; @@ -605,7 +468,7 @@ namespace SourceGit.ViewModels { if (PopupHost.CanCreatePopup()) { - PopupHost.ShowPopup(new StashChanges(_repo, changes, false)); + PopupHost.ShowPopup(new StashChanges(_repo, _selectedUnstaged, false)); } e.Handled = true; }; @@ -627,7 +490,7 @@ namespace SourceGit.ViewModels var storageFile = await topLevel.StorageProvider.SaveFilePickerAsync(options); if (storageFile != null) { - var succ = await Task.Run(() => Commands.SaveChangesAsPatch.Exec(_repo.FullPath, changes, true, storageFile.Path.LocalPath)); + var succ = await Task.Run(() => Commands.SaveChangesAsPatch.Exec(_repo.FullPath, _selectedUnstaged, true, storageFile.Path.LocalPath)); if (succ) App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess")); } @@ -679,7 +542,7 @@ namespace SourceGit.ViewModels { var hasConflicts = false; var hasNoneConflicts = false; - foreach (var change in changes) + foreach (var change in _selectedUnstaged) { if (change.IsConflit) { @@ -704,7 +567,7 @@ namespace SourceGit.ViewModels useTheirs.Header = App.Text("FileCM.UseTheirs"); useTheirs.Click += (_, e) => { - UseTheirs(changes); + UseTheirs(_selectedUnstaged); e.Handled = true; }; @@ -713,7 +576,7 @@ namespace SourceGit.ViewModels useMine.Header = App.Text("FileCM.UseMine"); useMine.Click += (_, e) => { - UseMine(changes); + UseMine(_selectedUnstaged); e.Handled = true; }; @@ -723,31 +586,31 @@ namespace SourceGit.ViewModels } var stage = new MenuItem(); - stage.Header = App.Text("FileCM.StageMulti", changes.Count); + stage.Header = App.Text("FileCM.StageMulti", _selectedUnstaged.Count); stage.Icon = App.CreateMenuIcon("Icons.File.Add"); stage.Click += (_, e) => { - StageChanges(changes); + StageChanges(_selectedUnstaged); e.Handled = true; }; var discard = new MenuItem(); - discard.Header = App.Text("FileCM.DiscardMulti", changes.Count); + discard.Header = App.Text("FileCM.DiscardMulti", _selectedUnstaged.Count); discard.Icon = App.CreateMenuIcon("Icons.Undo"); discard.Click += (_, e) => { - Discard(changes, true); + Discard(_selectedUnstaged, true); e.Handled = true; }; var stash = new MenuItem(); - stash.Header = App.Text("FileCM.StashMulti", changes.Count); + stash.Header = App.Text("FileCM.StashMulti", _selectedUnstaged.Count); stash.Icon = App.CreateMenuIcon("Icons.Stashes"); stash.Click += (_, e) => { if (PopupHost.CanCreatePopup()) { - PopupHost.ShowPopup(new StashChanges(_repo, changes, false)); + PopupHost.ShowPopup(new StashChanges(_repo, _selectedUnstaged, false)); } e.Handled = true; }; @@ -769,7 +632,7 @@ namespace SourceGit.ViewModels var storageFile = await topLevel.StorageProvider.SaveFilePickerAsync(options); if (storageFile != null) { - var succ = await Task.Run(() => Commands.SaveChangesAsPatch.Exec(_repo.FullPath, changes, true, storageFile.Path.LocalPath)); + var succ = await Task.Run(() => Commands.SaveChangesAsPatch.Exec(_repo.FullPath, _selectedUnstaged, true, storageFile.Path.LocalPath)); if (succ) App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess")); } @@ -786,15 +649,15 @@ namespace SourceGit.ViewModels return menu; } - public ContextMenu CreateContextMenuForStagedChanges(List changes) + public ContextMenu CreateContextMenuForStagedChanges() { - if (changes.Count == 0) + if (_selectedStaged.Count == 0) return null; var menu = new ContextMenu(); - if (changes.Count == 1) + if (_selectedStaged.Count == 1) { - var change = changes[0]; + var change = _selectedStaged[0]; var path = Path.GetFullPath(Path.Combine(_repo.FullPath, change.Path)); var explore = new MenuItem(); @@ -822,7 +685,7 @@ namespace SourceGit.ViewModels unstage.Icon = App.CreateMenuIcon("Icons.File.Remove"); unstage.Click += (o, e) => { - UnstageChanges(changes); + UnstageChanges(_selectedStaged); e.Handled = true; }; @@ -831,7 +694,7 @@ namespace SourceGit.ViewModels discard.Icon = App.CreateMenuIcon("Icons.Undo"); discard.Click += (_, e) => { - Discard(changes, false); + Discard(_selectedStaged, false); e.Handled = true; }; @@ -842,7 +705,7 @@ namespace SourceGit.ViewModels { if (PopupHost.CanCreatePopup()) { - PopupHost.ShowPopup(new StashChanges(_repo, changes, false)); + PopupHost.ShowPopup(new StashChanges(_repo, _selectedStaged, false)); } e.Handled = true; }; @@ -864,7 +727,7 @@ namespace SourceGit.ViewModels var storageFile = await topLevel.StorageProvider.SaveFilePickerAsync(options); if (storageFile != null) { - var succ = await Task.Run(() => Commands.SaveChangesAsPatch.Exec(_repo.FullPath, changes, false, storageFile.Path.LocalPath)); + var succ = await Task.Run(() => Commands.SaveChangesAsPatch.Exec(_repo.FullPath, _selectedStaged, false, storageFile.Path.LocalPath)); if (succ) App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess")); } @@ -894,31 +757,31 @@ namespace SourceGit.ViewModels else { var unstage = new MenuItem(); - unstage.Header = App.Text("FileCM.UnstageMulti", changes.Count); + unstage.Header = App.Text("FileCM.UnstageMulti", _selectedStaged.Count); unstage.Icon = App.CreateMenuIcon("Icons.File.Remove"); unstage.Click += (o, e) => { - UnstageChanges(changes); + UnstageChanges(_selectedStaged); e.Handled = true; }; var discard = new MenuItem(); - discard.Header = App.Text("FileCM.DiscardMulti", changes.Count); + discard.Header = App.Text("FileCM.DiscardMulti", _selectedStaged.Count); discard.Icon = App.CreateMenuIcon("Icons.Undo"); discard.Click += (_, e) => { - Discard(changes, false); + Discard(_selectedStaged, false); e.Handled = true; }; var stash = new MenuItem(); - stash.Header = App.Text("FileCM.StashMulti", changes.Count); + stash.Header = App.Text("FileCM.StashMulti", _selectedStaged.Count); stash.Icon = App.CreateMenuIcon("Icons.Stashes"); stash.Click += (_, e) => { if (PopupHost.CanCreatePopup()) { - PopupHost.ShowPopup(new StashChanges(_repo, changes, false)); + PopupHost.ShowPopup(new StashChanges(_repo, _selectedStaged, false)); } e.Handled = true; }; @@ -940,7 +803,7 @@ namespace SourceGit.ViewModels var storageFile = await topLevel.StorageProvider.SaveFilePickerAsync(options); if (storageFile != null) { - var succ = await Task.Run(() => Commands.SaveChangesAsPatch.Exec(_repo.FullPath, changes, false, storageFile.Path.LocalPath)); + var succ = await Task.Run(() => Commands.SaveChangesAsPatch.Exec(_repo.FullPath, _selectedStaged, false, storageFile.Path.LocalPath)); if (succ) App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess")); } @@ -993,6 +856,139 @@ namespace SourceGit.ViewModels return menu; } + private void SetDetail(Models.Change change) + { + if (_isLoadingData) + return; + + var isUnstaged = _selectedUnstaged != null && _selectedUnstaged.Count > 0; + if (change == null) + { + DetailContext = null; + } + else if (change.IsConflit && isUnstaged) + { + DetailContext = new ConflictContext(_repo.FullPath, change); + } + else + { + if (_detailContext is DiffContext previous) + { + DetailContext = new DiffContext(_repo.FullPath, new Models.DiffOption(change, isUnstaged), previous); + } + else + { + DetailContext = new DiffContext(_repo.FullPath, new Models.DiffOption(change, isUnstaged)); + } + } + } + + private async void UseTheirs(List changes) + { + var files = new List(); + foreach (var change in changes) + { + if (change.IsConflit) + files.Add(change.Path); + } + + _repo.SetWatcherEnabled(false); + var succ = await Task.Run(() => new Commands.Checkout(_repo.FullPath).UseTheirs(files)); + if (succ) + { + await Task.Run(() => new Commands.Add(_repo.FullPath, changes).Exec()); + } + _repo.MarkWorkingCopyDirtyManually(); + _repo.SetWatcherEnabled(true); + } + + private async void UseMine(List changes) + { + var files = new List(); + foreach (var change in changes) + { + if (change.IsConflit) + files.Add(change.Path); + } + + _repo.SetWatcherEnabled(false); + var succ = await Task.Run(() => new Commands.Checkout(_repo.FullPath).UseMine(files)); + if (succ) + { + await Task.Run(() => new Commands.Add(_repo.FullPath, changes).Exec()); + } + _repo.MarkWorkingCopyDirtyManually(); + _repo.SetWatcherEnabled(true); + } + + private async void UseExternalMergeTool(Models.Change change) + { + var type = Preference.Instance.ExternalMergeToolType; + var exec = Preference.Instance.ExternalMergeToolPath; + + var tool = Models.ExternalMerger.Supported.Find(x => x.Type == type); + if (tool == null) + { + App.RaiseException(_repo.FullPath, "Invalid merge tool in preference setting!"); + return; + } + + var args = tool.Type != 0 ? tool.Cmd : Preference.Instance.ExternalMergeToolCmd; + + _repo.SetWatcherEnabled(false); + await Task.Run(() => Commands.MergeTool.OpenForMerge(_repo.FullPath, exec, args, change.Path)); + _repo.SetWatcherEnabled(true); + } + + private void DoCommit(bool autoPush) + { + if (!PopupHost.CanCreatePopup()) + { + App.RaiseException(_repo.FullPath, "Repository has unfinished job! Please wait!"); + return; + } + + if (_staged.Count == 0) + { + App.RaiseException(_repo.FullPath, "No files added to commit!"); + return; + } + + if (string.IsNullOrWhiteSpace(_commitMessage)) + { + App.RaiseException(_repo.FullPath, "Commit without message is NOT allowed!"); + return; + } + + PushCommitMessage(); + + SetDetail(null); + IsCommitting = true; + _repo.SetWatcherEnabled(false); + + Task.Run(() => + { + var succ = new Commands.Commit(_repo.FullPath, _commitMessage, _useAmend).Exec(); + Dispatcher.UIThread.Post(() => + { + if (succ) + { + CommitMessage = string.Empty; + UseAmend = false; + + if (autoPush) + { + PopupHost.ShowAndStartPopup(new Push(_repo, null)); + } + } + _repo.MarkWorkingCopyDirtyManually(); + _repo.SetWatcherEnabled(true); + + IsCommitting = false; + }); + }); + } + private void PushCommitMessage() { var existIdx = _repo.CommitMessages.IndexOf(CommitMessage); @@ -1022,13 +1018,9 @@ namespace SourceGit.ViewModels private bool _useAmend = false; private List _unstaged = null; private List _staged = null; - private Models.Change _selectedUnstagedChange = null; - private Models.Change _selectedStagedChange = null; + private List _selectedUnstaged = null; + private List _selectedStaged = null; private int _count = 0; - private List _unstagedTree = null; - private List _stagedTree = null; - private FileTreeNode _selectedUnstagedTreeNode = null; - private FileTreeNode _selectedStagedTreeNode = null; private object _detailContext = null; private string _commitMessage = string.Empty; } diff --git a/src/Views/ChangeCollectionView.axaml b/src/Views/ChangeCollectionView.axaml new file mode 100644 index 00000000..9b37cb17 --- /dev/null +++ b/src/Views/ChangeCollectionView.axaml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/ChangeCollectionView.axaml.cs b/src/Views/ChangeCollectionView.axaml.cs new file mode 100644 index 00000000..9b9ad904 --- /dev/null +++ b/src/Views/ChangeCollectionView.axaml.cs @@ -0,0 +1,271 @@ +using System; +using System.Collections.Generic; +using System.IO; + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Models.TreeDataGrid; +using Avalonia.Interactivity; + +namespace SourceGit.Views +{ + public partial class ChangeCollectionView : UserControl + { + public static readonly StyledProperty IsWorkingCopyChangeProperty = + AvaloniaProperty.Register(nameof(IsWorkingCopy), false); + + public bool IsWorkingCopy + { + get => GetValue(IsWorkingCopyChangeProperty); + set => SetValue(IsWorkingCopyChangeProperty, value); + } + + public static readonly StyledProperty SingleSelectProperty = + AvaloniaProperty.Register(nameof(SingleSelect), true); + + public bool SingleSelect + { + get => GetValue(SingleSelectProperty); + set => SetValue(SingleSelectProperty, value); + } + + public static readonly StyledProperty ViewModeProperty = + AvaloniaProperty.Register(nameof(ViewMode), Models.ChangeViewMode.Tree); + + public Models.ChangeViewMode ViewMode + { + get => GetValue(ViewModeProperty); + set => SetValue(ViewModeProperty, value); + } + + public static readonly StyledProperty> ChangesProperty = + AvaloniaProperty.Register>(nameof(Changes), null); + + public List Changes + { + get => GetValue(ChangesProperty); + set => SetValue(ChangesProperty, value); + } + + public static readonly StyledProperty> SelectedChangesProperty = + AvaloniaProperty.Register>(nameof(SelectedChanges), null); + + public List SelectedChanges + { + get => GetValue(SelectedChangesProperty); + set => SetValue(SelectedChangesProperty, value); + } + + public static readonly RoutedEvent ChangeDoubleTappedEvent = + RoutedEvent.Register(nameof(ChangeDoubleTapped), RoutingStrategies.Tunnel | RoutingStrategies.Bubble); + + public event EventHandler ChangeDoubleTapped + { + add { AddHandler(ChangeDoubleTappedEvent, value); } + remove { RemoveHandler(ChangeDoubleTappedEvent, value); } + } + + static ChangeCollectionView() + { + ViewModeProperty.Changed.AddClassHandler((c, e) => c.UpdateSource()); + ChangesProperty.Changed.AddClassHandler((c, e) => c.UpdateSource()); + SelectedChangesProperty.Changed.AddClassHandler((c, e) => c.UpdateSelected()); + } + + public ChangeCollectionView() + { + InitializeComponent(); + } + + private void UpdateSource() + { + if (tree.Source is IDisposable disposable) + { + disposable.Dispose(); + tree.Source = null; + } + + var changes = Changes; + if (changes == null) + return; + + var viewMode = ViewMode; + if (viewMode == Models.ChangeViewMode.Tree) + { + var filetree = ViewModels.FileTreeNode.Build(changes, true); + var source = new HierarchicalTreeDataGridSource(filetree) + { + Columns = + { + new HierarchicalExpanderColumn( + new TemplateColumn(null, "TreeModeTemplate", null, GridLength.Auto), + x => x.Children, + x => x.Children.Count > 0, + x => x.IsExpanded), + new TextColumn( + null, + x => string.Empty, + GridLength.Star) + } + }; + + var selection = new Models.TreeDataGridSelectionModel(source, x => x.Children); + selection.RowDoubleTapped += (_, e) => RaiseEvent(new RoutedEventArgs(ChangeDoubleTappedEvent)); + + source.Selection = selection; + source.RowSelection.SingleSelect = SingleSelect; + source.RowSelection.SelectionChanged += (s, _) => + { + if (!_isSelecting && s is Models.TreeDataGridSelectionModel model) + { + var selection = new List(); + foreach (var c in model.SelectedItems) + CollectChangesInNode(selection, c); + + _isSelecting = true; + SetCurrentValue(SelectedChangesProperty, selection); + _isSelecting = false; + } + }; + + tree.Source = source; + } + else if (viewMode == Models.ChangeViewMode.List) + { + var source = new FlatTreeDataGridSource(changes) + { + Columns = { new TemplateColumn(null, "ListModeTemplate", null, GridLength.Auto) } + }; + + var selection = new Models.TreeDataGridSelectionModel(source, null); + selection.RowDoubleTapped += (_, e) => RaiseEvent(new RoutedEventArgs(ChangeDoubleTappedEvent)); + + source.Selection = selection; + source.RowSelection.SingleSelect = SingleSelect; + source.RowSelection.SelectionChanged += (s, _) => + { + if (!_isSelecting && s is Models.TreeDataGridSelectionModel model) + { + var selection = new List(); + foreach (var c in model.SelectedItems) + selection.Add(c); + + _isSelecting = true; + SetCurrentValue(SelectedChangesProperty, selection); + _isSelecting = false; + } + }; + + tree.Source = source; + } + else + { + var source = new FlatTreeDataGridSource(changes) + { + Columns = + { + new TemplateColumn(null, "GridModeFileTemplate", null, GridLength.Auto), + new TemplateColumn(null, "GridModeDirTemplate", null, GridLength.Auto) + }, + }; + + var selection = new Models.TreeDataGridSelectionModel(source, null); + selection.RowDoubleTapped += (_, e) => RaiseEvent(new RoutedEventArgs(ChangeDoubleTappedEvent)); + + source.Selection = selection; + source.RowSelection.SingleSelect = SingleSelect; + source.RowSelection.SelectionChanged += (s, _) => + { + if (!_isSelecting && s is Models.TreeDataGridSelectionModel model) + { + var selection = new List(); + foreach (var c in model.SelectedItems) + selection.Add(c); + + _isSelecting = true; + SetCurrentValue(SelectedChangesProperty, selection); + _isSelecting = false; + } + }; + + tree.Source = source; + } + } + + private void UpdateSelected() + { + if (_isSelecting || tree.Source == null) + return; + + _isSelecting = true; + var selected = SelectedChanges; + if (tree.Source.Selection is Models.TreeDataGridSelectionModel changeSelection) + { + if (selected == null || selected.Count == 0) + changeSelection.Clear(); + else + changeSelection.Select(selected); + } + else if (tree.Source.Selection is Models.TreeDataGridSelectionModel treeSelection) + { + if (selected == null || selected.Count == 0) + { + treeSelection.Clear(); + _isSelecting = false; + return; + } + + var set = new HashSet(); + foreach (var c in selected) + set.Add(c); + + var nodes = new List(); + foreach (var node in tree.Source.Items) + CollectSelectedNodeByChange(nodes, node as ViewModels.FileTreeNode, set); + + if (nodes.Count == 0) + { + treeSelection.Clear(); + } + else + { + treeSelection.Select(nodes); + } + } + _isSelecting = false; + } + + private void CollectChangesInNode(List outs, ViewModels.FileTreeNode node) + { + if (node.IsFolder) + { + foreach (var child in node.Children) + CollectChangesInNode(outs, child); + } + else + { + var change = node.Backend as Models.Change; + if (change != null && !outs.Contains(change)) + outs.Add(change); + } + } + + private void CollectSelectedNodeByChange(List outs, ViewModels.FileTreeNode node, HashSet selected) + { + if (node == null) + return; + + if (node.IsFolder) + { + foreach (var child in node.Children) + CollectSelectedNodeByChange(outs, child, selected); + } + else if (node.Backend != null && selected.Contains(node.Backend)) + { + outs.Add(node); + } + } + + private bool _isSelecting = false; + } +} diff --git a/src/Views/ChangeViewModeSwitcher.axaml b/src/Views/ChangeViewModeSwitcher.axaml index 32d4f1e9..bae68079 100644 --- a/src/Views/ChangeViewModeSwitcher.axaml +++ b/src/Views/ChangeViewModeSwitcher.axaml @@ -14,17 +14,17 @@ - + - + - + diff --git a/src/Views/CommitChanges.axaml b/src/Views/CommitChanges.axaml index a82f06e2..eccd0952 100644 --- a/src/Views/CommitChanges.axaml +++ b/src/Views/CommitChanges.axaml @@ -17,7 +17,7 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/src/Views/CommitChanges.axaml.cs b/src/Views/CommitChanges.axaml.cs index f3566a2c..d9ac6c2e 100644 --- a/src/Views/CommitChanges.axaml.cs +++ b/src/Views/CommitChanges.axaml.cs @@ -1,3 +1,4 @@ +using Avalonia; using Avalonia.Controls; namespace SourceGit.Views @@ -9,38 +10,16 @@ namespace SourceGit.Views InitializeComponent(); } - private void OnDataGridSelectionChanged(object sender, SelectionChangedEventArgs e) + private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e) { - if (sender is DataGrid datagrid && datagrid.IsVisible && datagrid.SelectedItem != null) + if (DataContext is ViewModels.CommitDetail vm) { - datagrid.ScrollIntoView(datagrid.SelectedItem, null); - } - e.Handled = true; - } - - private void OnDataGridContextRequested(object sender, ContextRequestedEventArgs e) - { - if (sender is DataGrid datagrid && datagrid.SelectedItem != null) - { - var detail = DataContext as ViewModels.CommitDetail; - var menu = detail.CreateChangeContextMenu(datagrid.SelectedItem as Models.Change); - datagrid.OpenContextMenu(menu); - } - - e.Handled = true; - } - - private void OnTreeViewContextRequested(object sender, ContextRequestedEventArgs e) - { - if (sender is TreeView view && view.SelectedItem != null) - { - var detail = DataContext as ViewModels.CommitDetail; - var node = view.SelectedItem as ViewModels.FileTreeNode; - if (node != null && !node.IsFolder) + var selected = (sender as ChangeCollectionView)?.SelectedChanges; + if (selected != null && selected.Count == 1) { - var menu = detail.CreateChangeContextMenu(node.Backend as Models.Change); - view.OpenContextMenu(menu); - } + var menu = vm.CreateChangeContextMenu(selected[0]); + (sender as Control)?.OpenContextMenu(menu); + } } e.Handled = true; diff --git a/src/Views/CommitDetail.axaml b/src/Views/CommitDetail.axaml index 04972c42..2094d95b 100644 --- a/src/Views/CommitDetail.axaml +++ b/src/Views/CommitDetail.axaml @@ -22,40 +22,20 @@ - - - - - - - - - - - - - - - - - - - + + + + + diff --git a/src/Views/CommitDetail.axaml.cs b/src/Views/CommitDetail.axaml.cs index af2706b6..84cb8381 100644 --- a/src/Views/CommitDetail.axaml.cs +++ b/src/Views/CommitDetail.axaml.cs @@ -1,5 +1,5 @@ using Avalonia.Controls; -using Avalonia.Input; +using Avalonia.Interactivity; namespace SourceGit.Views { @@ -10,30 +10,30 @@ namespace SourceGit.Views InitializeComponent(); } - private void OnChangeListDoubleTapped(object sender, TappedEventArgs e) + private void OnChangeListContextRequested(object sender, ContextRequestedEventArgs e) { - if (DataContext is ViewModels.CommitDetail detail) + if (DataContext is ViewModels.CommitDetail vm && sender is ChangeCollectionView view) { - var datagrid = sender as DataGrid; - detail.ActivePageIndex = 1; - detail.SelectedChange = datagrid.SelectedItem as Models.Change; + var selected = view.SelectedChanges; + if (selected != null && selected.Count == 1) + { + var menu = vm.CreateChangeContextMenu(selected[0]); + view.OpenContextMenu(menu); + } } e.Handled = true; } - private void OnChangeListContextRequested(object sender, ContextRequestedEventArgs e) + private void OnChangeDoubleTapped(object sender, RoutedEventArgs e) { - if (DataContext is ViewModels.CommitDetail detail) + if (DataContext is ViewModels.CommitDetail vm && sender is ChangeCollectionView view) { - var datagrid = sender as DataGrid; - if (datagrid.SelectedItem == null) + var selected = view.SelectedChanges; + if (selected != null && selected.Count == 1) { - e.Handled = true; - return; + vm.ActivePageIndex = 1; + vm.SelectedChanges = new() { selected[0] }; } - - var menu = detail.CreateChangeContextMenu(datagrid.SelectedItem as Models.Change); - datagrid.OpenContextMenu(menu); } e.Handled = true; } diff --git a/src/Views/RevisionCompare.axaml b/src/Views/RevisionCompare.axaml index ac3f91ee..9411c174 100644 --- a/src/Views/RevisionCompare.axaml +++ b/src/Views/RevisionCompare.axaml @@ -72,7 +72,7 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/src/Views/RevisionCompare.axaml.cs b/src/Views/RevisionCompare.axaml.cs index a9e80676..e3ecb2b7 100644 --- a/src/Views/RevisionCompare.axaml.cs +++ b/src/Views/RevisionCompare.axaml.cs @@ -10,38 +10,12 @@ namespace SourceGit.Views InitializeComponent(); } - private void OnDataGridSelectionChanged(object sender, SelectionChangedEventArgs e) + private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e) { - if (sender is DataGrid datagrid && datagrid.IsVisible) + if (DataContext is ViewModels.RevisionCompare vm && sender is ChangeCollectionView view) { - datagrid.ScrollIntoView(datagrid.SelectedItem, null); - } - e.Handled = true; - } - - private void OnDataGridContextRequested(object sender, ContextRequestedEventArgs e) - { - if (sender is DataGrid datagrid && datagrid.SelectedItem != null) - { - var compare = DataContext as ViewModels.RevisionCompare; - var menu = compare.CreateChangeContextMenu(datagrid.SelectedItem as Models.Change); - datagrid.OpenContextMenu(menu); - } - - e.Handled = true; - } - - private void OnTreeViewContextRequested(object sender, ContextRequestedEventArgs e) - { - if (sender is TreeView view && view.SelectedItem != null) - { - var compare = DataContext as ViewModels.RevisionCompare; - var node = view.SelectedItem as ViewModels.FileTreeNode; - if (node != null && !node.IsFolder) - { - var menu = compare.CreateChangeContextMenu(node.Backend as Models.Change); - view.OpenContextMenu(menu); - } + var menu = vm.CreateChangeContextMenu(); + view.OpenContextMenu(menu); } e.Handled = true; @@ -49,11 +23,8 @@ namespace SourceGit.Views private void OnPressedSHA(object sender, PointerPressedEventArgs e) { - if (sender is TextBlock block) - { - var compare = DataContext as ViewModels.RevisionCompare; - compare.NavigateTo(block.Text); - } + if (DataContext is ViewModels.RevisionCompare vm && sender is TextBlock block) + vm.NavigateTo(block.Text); e.Handled = true; } diff --git a/src/Views/RevisionFiles.axaml b/src/Views/RevisionFiles.axaml index 2f1eb8d2..c46632f6 100644 --- a/src/Views/RevisionFiles.axaml +++ b/src/Views/RevisionFiles.axaml @@ -41,29 +41,22 @@ - - - - - - - - + + + + - - - + + + diff --git a/src/Views/RevisionFiles.axaml.cs b/src/Views/RevisionFiles.axaml.cs index 7865542d..730e9b34 100644 --- a/src/Views/RevisionFiles.axaml.cs +++ b/src/Views/RevisionFiles.axaml.cs @@ -213,14 +213,16 @@ namespace SourceGit.Views InitializeComponent(); } - private void OnTreeViewContextRequested(object sender, ContextRequestedEventArgs e) + private void OnFileContextRequested(object sender, ContextRequestedEventArgs e) { - var detail = DataContext as ViewModels.CommitDetail; - var node = detail.SelectedRevisionFileNode; - if (!node.IsFolder) + if (DataContext is ViewModels.CommitDetail vm && sender is TreeDataGrid tree) { - var menu = detail.CreateRevisionFileContextMenu(node.Backend as Models.Object); - (sender as Control)?.OpenContextMenu(menu); + var selected = tree.RowSelection.SelectedItem as ViewModels.FileTreeNode; + if (selected != null && !selected.IsFolder && selected.Backend is Models.Object obj) + { + var menu = vm.CreateRevisionFileContextMenu(obj); + tree.OpenContextMenu(menu); + } } e.Handled = true; diff --git a/src/Views/WorkingCopy.axaml b/src/Views/WorkingCopy.axaml index bd65724a..df3ae7d6 100644 --- a/src/Views/WorkingCopy.axaml +++ b/src/Views/WorkingCopy.axaml @@ -21,7 +21,7 @@ - + @@ -31,7 +31,7 @@ Width="26" Height="14" Padding="0" ToolTip.Tip="{DynamicResource Text.WorkingCopy.Unstaged.ViewAssumeUnchaged}" - Click="ViewAssumeUnchanged"> + Command="{Binding OpenAssumeUnchanged}"> + Command="{Binding StageSelected}"> @@ -56,130 +56,33 @@ Classes="icon_button" Width="26" Height="14" Padding="0" - ToolTip.Tip="{DynamicResource Text.WorkingCopy.Unstaged.StageAll}" Click="StageAll"> + ToolTip.Tip="{DynamicResource Text.WorkingCopy.Unstaged.StageAll}" + Command="{Binding StageAll}"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + Content="{DynamicResource Text.WorkingCopy.Amend}"/> + Command="{Binding Commit}"/>