From 4249653ed6774ac748b41a11d513d50eab2c09a9 Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 27 May 2024 17:21:28 +0800 Subject: [PATCH 01/43] feature: add context menu for both branch and commit to compare selected with current HEAD --- src/Commands/QueryCommits.cs | 1 + src/Commands/QuerySingleCommit.cs | 167 ++++++++++++++++++++++++++++++ src/Resources/Icons.axaml | 1 + src/Resources/Locales/en_US.axaml | 4 +- src/Resources/Locales/zh_CN.axaml | 2 + src/ViewModels/Histories.cs | 38 +++++-- src/ViewModels/Repository.cs | 45 ++++++++ src/Views/Histories.axaml.cs | 6 +- 8 files changed, 254 insertions(+), 10 deletions(-) create mode 100644 src/Commands/QuerySingleCommit.cs diff --git a/src/Commands/QueryCommits.cs b/src/Commands/QueryCommits.cs index 618ff014..c165a658 100644 --- a/src/Commands/QueryCommits.cs +++ b/src/Commands/QueryCommits.cs @@ -17,6 +17,7 @@ namespace SourceGit.Commands public QueryCommits(string repo, string limits, bool needFindHead = true) { WorkingDirectory = repo; + Context = repo; Args = "log --date-order --decorate=full --pretty=raw " + limits; findFirstMerged = needFindHead; } diff --git a/src/Commands/QuerySingleCommit.cs b/src/Commands/QuerySingleCommit.cs new file mode 100644 index 00000000..5c0fd760 --- /dev/null +++ b/src/Commands/QuerySingleCommit.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; + +namespace SourceGit.Commands +{ + public class QuerySingleCommit : Command + { + private const string GPGSIG_START = "gpgsig -----BEGIN PGP SIGNATURE-----"; + private const string GPGSIG_END = " -----END PGP SIGNATURE-----"; + + public QuerySingleCommit(string repo, string sha) { + WorkingDirectory = repo; + Context = repo; + Args = $"show --pretty=raw --decorate=full -s {sha}"; + } + + public Models.Commit Result() + { + var succ = Exec(); + if (!succ) + return null; + + _commit.Message.Trim(); + return _commit; + } + + protected override void OnReadline(string line) + { + if (isSkipingGpgsig) + { + if (line.StartsWith(GPGSIG_END, StringComparison.Ordinal)) + isSkipingGpgsig = false; + return; + } + else if (line.StartsWith(GPGSIG_START, StringComparison.Ordinal)) + { + isSkipingGpgsig = true; + return; + } + + if (line.StartsWith("commit ", StringComparison.Ordinal)) + { + line = line.Substring(7); + + var decoratorStart = line.IndexOf('(', StringComparison.Ordinal); + if (decoratorStart < 0) + { + _commit.SHA = line.Trim(); + } + else + { + _commit.SHA = line.Substring(0, decoratorStart).Trim(); + ParseDecorators(_commit.Decorators, line.Substring(decoratorStart + 1)); + } + + return; + } + + if (line.StartsWith("tree ", StringComparison.Ordinal)) + { + return; + } + else if (line.StartsWith("parent ", StringComparison.Ordinal)) + { + _commit.Parents.Add(line.Substring("parent ".Length)); + } + else if (line.StartsWith("author ", StringComparison.Ordinal)) + { + Models.User user = Models.User.Invalid; + ulong time = 0; + Models.Commit.ParseUserAndTime(line.Substring(7), ref user, ref time); + _commit.Author = user; + _commit.AuthorTime = time; + } + else if (line.StartsWith("committer ", StringComparison.Ordinal)) + { + Models.User user = Models.User.Invalid; + ulong time = 0; + Models.Commit.ParseUserAndTime(line.Substring(10), ref user, ref time); + _commit.Committer = user; + _commit.CommitterTime = time; + } + else if (string.IsNullOrEmpty(_commit.Subject)) + { + _commit.Subject = line.Trim(); + } + else + { + _commit.Message += (line.Trim() + "\n"); + } + } + + private bool ParseDecorators(List decorators, string data) + { + bool isHeadOfCurrent = false; + + var subs = data.Split(new char[] { ',', ')', '(' }, StringSplitOptions.RemoveEmptyEntries); + foreach (var sub in subs) + { + var d = sub.Trim(); + if (d.StartsWith("tag: refs/tags/", StringComparison.Ordinal)) + { + decorators.Add(new Models.Decorator() + { + Type = Models.DecoratorType.Tag, + Name = d.Substring(15).Trim(), + }); + } + else if (d.EndsWith("/HEAD", StringComparison.Ordinal)) + { + continue; + } + else if (d.StartsWith("HEAD -> refs/heads/", StringComparison.Ordinal)) + { + isHeadOfCurrent = true; + decorators.Add(new Models.Decorator() + { + Type = Models.DecoratorType.CurrentBranchHead, + Name = d.Substring(19).Trim(), + }); + } + else if (d.Equals("HEAD")) + { + isHeadOfCurrent = true; + decorators.Add(new Models.Decorator() + { + Type = Models.DecoratorType.CurrentCommitHead, + Name = d.Trim(), + }); + } + else if (d.StartsWith("refs/heads/", StringComparison.Ordinal)) + { + decorators.Add(new Models.Decorator() + { + Type = Models.DecoratorType.LocalBranchHead, + Name = d.Substring(11).Trim(), + }); + } + else if (d.StartsWith("refs/remotes/", StringComparison.Ordinal)) + { + decorators.Add(new Models.Decorator() + { + Type = Models.DecoratorType.RemoteBranchHead, + Name = d.Substring(13).Trim(), + }); + } + } + + decorators.Sort((l, r) => + { + if (l.Type != r.Type) + { + return (int)l.Type - (int)r.Type; + } + else + { + return l.Name.CompareTo(r.Name); + } + }); + + return isHeadOfCurrent; + } + + private Models.Commit _commit = new Models.Commit(); + private bool isSkipingGpgsig = false; + } +} diff --git a/src/Resources/Icons.axaml b/src/Resources/Icons.axaml index b61e6839..b6369398 100644 --- a/src/Resources/Icons.axaml +++ b/src/Resources/Icons.axaml @@ -97,4 +97,5 @@ M884 159l-18-18a43 43 0 00-38-12l-235 43a166 166 0 00-101 60L400 349a128 128 0 00-148 47l-120 171a21 21 0 005 29l17 12a128 128 0 00178-32l27-38 124 124-38 27a128 128 0 00-32 178l12 17a21 21 0 0029 5l171-120a128 128 0 0047-148l117-92A166 166 0 00853 431l43-235a43 43 0 00-12-38zm-177 249a64 64 0 110-90 64 64 0 010 90zm-373 312a21 21 0 010 30l-139 139a21 21 0 01-30 0l-30-30a21 21 0 010-30l139-139a21 21 0 0130 0z M408 232C408 210 426 192 448 192h416a40 40 0 110 80H448a40 40 0 01-40-40zM408 512c0-22 18-40 40-40h416a40 40 0 110 80H448A40 40 0 01408 512zM448 752A40 40 0 00448 832h416a40 40 0 100-80H448zM32 480l132 0 0-128 64 0 0 128 132 0 0 64-132 0 0 128-64 0 0-128-132 0Z M408 232C408 210 426 192 448 192h416a40 40 0 110 80H448a40 40 0 01-40-40zM408 512c0-22 18-40 40-40h416a40 40 0 110 80H448A40 40 0 01408 512zM448 752A40 40 0 00448 832h416a40 40 0 100-80H448zM32 480l328 0 0 64-328 0Z + M645 448l64 64 220-221L704 64l-64 64 115 115H128v90h628zM375 576l-64-64-220 224L314 960l64-64-116-115H896v-90H262z diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 685cb1be..68f92ec1 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -33,6 +33,7 @@ Blame BLAME ON THIS FILE IS NOT SUPPORTED!!! Checkout${0}$ + Compare with HEAD Copy Branch Name Delete${0}$ Delete selected {0} branches @@ -76,8 +77,9 @@ Repository URL : CLOSE Cherry-Pick This Commit - Copy SHA Checkout Commit + Compare with HEAD + Copy SHA Rebase${0}$to Here Reset${0}$to Here Revert Commit diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index ac48aec9..12dccf35 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -33,6 +33,7 @@ 逐行追溯(blame) 选中文件不支持该操作!!! 检出(checkout)${0}$ + 与当前HEAD比较 复制分支名 删除${0}$ 删除选中的 {0} 个分支 @@ -77,6 +78,7 @@ 关闭 挑选(cherry-pick)此提交 检出此提交 + 与当前HEAD比较 复制提交指纹 变基(rebase)${0}$到此处 重置(reset)${0}$到此处 diff --git a/src/ViewModels/Histories.cs b/src/ViewModels/Histories.cs index c28b4ade..98659e2f 100644 --- a/src/ViewModels/Histories.cs +++ b/src/ViewModels/Histories.cs @@ -69,7 +69,7 @@ namespace SourceGit.ViewModels public Models.Commit AutoSelectedCommit { get => _autoSelectedCommit; - private set => SetProperty(ref _autoSelectedCommit, value); + set => SetProperty(ref _autoSelectedCommit, value); } public long NavigationId @@ -81,7 +81,7 @@ namespace SourceGit.ViewModels public object DetailContext { get => _detailContext; - private set => SetProperty(ref _detailContext, value); + set => SetProperty(ref _detailContext, value); } public Histories(Repository repo) @@ -171,17 +171,16 @@ namespace SourceGit.ViewModels } } - public ContextMenu MakeContextMenu() + public ContextMenu MakeContextMenu(DataGrid datagrid) { - var detail = _detailContext as CommitDetail; - if (detail == null) + if (datagrid.SelectedItems.Count != 1) return null; var current = _repo.Branches.Find(x => x.IsCurrent); if (current == null) return null; - var commit = detail.Commit; + var commit = datagrid.SelectedItem as Models.Commit; var menu = new ContextMenu(); var tags = new List(); @@ -317,6 +316,33 @@ namespace SourceGit.ViewModels menu.Items.Add(new MenuItem() { Header = "-" }); + if (current.Head != commit.SHA) + { + var compare = new MenuItem(); + compare.Header = App.Text("CommitCM.CompareWithHead"); + compare.Icon = App.CreateMenuIcon("Icons.Compare"); + compare.Click += (o, e) => + { + var head = _commits.Find(x => x.SHA == current.Head); + if (head == null) + { + _repo.SearchResultSelectedCommit = null; + head = new Commands.QuerySingleCommit(_repo.FullPath, current.Head).Result(); + if (head != null) + DetailContext = new RevisionCompare(_repo.FullPath, commit, head); + } + else + { + datagrid.SelectedItems.Add(head); + } + + e.Handled = true; + }; + + menu.Items.Add(compare); + menu.Items.Add(new MenuItem() { Header = "-" }); + } + var createBranch = new MenuItem(); createBranch.Icon = App.CreateMenuIcon("Icons.Branch.Add"); createBranch.Header = App.Text("CreateBranch"); diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 94a04172..c35ddecf 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -928,6 +928,27 @@ namespace SourceGit.ViewModels menu.Items.Add(merge); menu.Items.Add(rebase); + + var compare = new MenuItem(); + compare.Header = App.Text("BranchCM.CompareWithHead"); + compare.Icon = App.CreateMenuIcon("Icons.Compare"); + compare.Click += (o, e) => + { + SearchResultSelectedCommit = null; + + if (_histories != null) + { + var target = new Commands.QuerySingleCommit(FullPath, branch.Head).Result(); + var head = new Commands.QuerySingleCommit(FullPath, current.Head).Result(); + _histories.AutoSelectedCommit = null; + _histories.DetailContext = new RevisionCompare(FullPath, target, head); + } + + e.Handled = true; + }; + + menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(compare); } var type = GitFlow.GetBranchType(branch.Name); @@ -1197,6 +1218,30 @@ namespace SourceGit.ViewModels menu.Items.Add(merge); menu.Items.Add(rebase); menu.Items.Add(new MenuItem() { Header = "-" }); + + if (current.Head != branch.Head) + { + var compare = new MenuItem(); + compare.Header = App.Text("BranchCM.CompareWithHead"); + compare.Icon = App.CreateMenuIcon("Icons.Compare"); + compare.Click += (o, e) => + { + SearchResultSelectedCommit = null; + + if (_histories != null) + { + var target = new Commands.QuerySingleCommit(FullPath, branch.Head).Result(); + var head = new Commands.QuerySingleCommit(FullPath, current.Head).Result(); + _histories.AutoSelectedCommit = null; + _histories.DetailContext = new RevisionCompare(FullPath, target, head); + } + + e.Handled = true; + }; + + menu.Items.Add(compare); + menu.Items.Add(new MenuItem() { Header = "-" }); + } } var delete = new MenuItem(); diff --git a/src/Views/Histories.axaml.cs b/src/Views/Histories.axaml.cs index 79458d10..f1bd9698 100644 --- a/src/Views/Histories.axaml.cs +++ b/src/Views/Histories.axaml.cs @@ -297,10 +297,10 @@ namespace SourceGit.Views private void OnCommitDataGridContextRequested(object sender, ContextRequestedEventArgs e) { - if (DataContext is ViewModels.Histories histories) + if (DataContext is ViewModels.Histories histories && sender is DataGrid datagrid) { - var menu = histories.MakeContextMenu(); - (sender as Control)?.OpenContextMenu(menu); + var menu = histories.MakeContextMenu(datagrid); + datagrid.OpenContextMenu(menu); } e.Handled = true; } From 211e4b24c1fe9275854d8e2b2a7d051e212420cc Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 27 May 2024 20:09:19 +0800 Subject: [PATCH 02/43] ux: layout for CheckoutCommit --- src/Views/CheckoutCommit.axaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Views/CheckoutCommit.axaml b/src/Views/CheckoutCommit.axaml index d59c4833..f73e2041 100644 --- a/src/Views/CheckoutCommit.axaml +++ b/src/Views/CheckoutCommit.axaml @@ -18,7 +18,7 @@ Foreground="{DynamicResource Brush.FG2}" FontStyle="Italic"/> - + Date: Mon, 27 May 2024 21:05:15 +0800 Subject: [PATCH 03/43] feature: add a context menu item to compare selected branch/revision with current worktree --- src/Resources/Locales/en_US.axaml | 3 ++ src/Resources/Locales/zh_CN.axaml | 3 ++ src/ViewModels/Histories.cs | 23 ++++++++++--- src/ViewModels/Repository.cs | 40 ++++++++++++++++++++++- src/ViewModels/RevisionCompare.cs | 26 ++++++++++++--- src/Views/RevisionCompare.axaml | 54 ++++++++++++++++++++----------- 6 files changed, 119 insertions(+), 30 deletions(-) diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 68f92ec1..bc2bc9c0 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -34,6 +34,7 @@ BLAME ON THIS FILE IS NOT SUPPORTED!!! Checkout${0}$ Compare with HEAD + Compare with Worktree Copy Branch Name Delete${0}$ Delete selected {0} branches @@ -79,6 +80,7 @@ Cherry-Pick This Commit Checkout Commit Compare with HEAD + Compare with Worktree Copy SHA Rebase${0}$to Here Reset${0}$to Here @@ -478,4 +480,5 @@ STAGE ALL VIEW ASSUME UNCHANGED Right-click the selected file(s), and make your choice to resolve conflicts. + Current Worktree diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 12dccf35..3dffd574 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -34,6 +34,7 @@ 选中文件不支持该操作!!! 检出(checkout)${0}$ 与当前HEAD比较 + 与本地工作树比较 复制分支名 删除${0}$ 删除选中的 {0} 个分支 @@ -79,6 +80,7 @@ 挑选(cherry-pick)此提交 检出此提交 与当前HEAD比较 + 与本地工作树比较 复制提交指纹 变基(rebase)${0}$到此处 重置(reset)${0}$到此处 @@ -478,4 +480,5 @@ 暂存所有 查看忽略变更文件 请选中冲突文件,打开右键菜单,选择合适的解决方式 + 本地工作树 diff --git a/src/ViewModels/Histories.cs b/src/ViewModels/Histories.cs index 98659e2f..d759126c 100644 --- a/src/ViewModels/Histories.cs +++ b/src/ViewModels/Histories.cs @@ -318,10 +318,10 @@ namespace SourceGit.ViewModels if (current.Head != commit.SHA) { - var compare = new MenuItem(); - compare.Header = App.Text("CommitCM.CompareWithHead"); - compare.Icon = App.CreateMenuIcon("Icons.Compare"); - compare.Click += (o, e) => + var compareWithHead = new MenuItem(); + compareWithHead.Header = App.Text("CommitCM.CompareWithHead"); + compareWithHead.Icon = App.CreateMenuIcon("Icons.Compare"); + compareWithHead.Click += (o, e) => { var head = _commits.Find(x => x.SHA == current.Head); if (head == null) @@ -338,8 +338,21 @@ namespace SourceGit.ViewModels e.Handled = true; }; + menu.Items.Add(compareWithHead); + + if (_repo.WorkingCopyChangesCount > 0) + { + var compareWithWorktree = new MenuItem(); + compareWithWorktree.Header = App.Text("CommitCM.CompareWithWorktree"); + compareWithWorktree.Icon = App.CreateMenuIcon("Icons.Compare"); + compareWithWorktree.Click += (o, e) => + { + DetailContext = new RevisionCompare(_repo.FullPath, commit, null); + e.Handled = true; + }; + menu.Items.Add(compareWithWorktree); + } - menu.Items.Add(compare); menu.Items.Add(new MenuItem() { Header = "-" }); } diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index c35ddecf..df9b3f47 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -947,6 +947,25 @@ namespace SourceGit.ViewModels e.Handled = true; }; + if (WorkingCopyChangesCount > 0) + { + var compareWithWorktree = new MenuItem(); + compareWithWorktree.Header = App.Text("BranchCM.CompareWithWorktree"); + compareWithWorktree.Icon = App.CreateMenuIcon("Icons.Compare"); + compareWithWorktree.Click += (o, e) => + { + SearchResultSelectedCommit = null; + + if (_histories != null) + { + var target = new Commands.QuerySingleCommit(FullPath, branch.Head).Result(); + _histories.AutoSelectedCommit = null; + _histories.DetailContext = new RevisionCompare(FullPath, target, null); + } + }; + menu.Items.Add(compareWithWorktree); + } + menu.Items.Add(new MenuItem() { Header = "-" }); menu.Items.Add(compare); } @@ -1238,8 +1257,27 @@ namespace SourceGit.ViewModels e.Handled = true; }; - menu.Items.Add(compare); + + if (WorkingCopyChangesCount > 0) + { + var compareWithWorktree = new MenuItem(); + compareWithWorktree.Header = App.Text("BranchCM.CompareWithWorktree"); + compareWithWorktree.Icon = App.CreateMenuIcon("Icons.Compare"); + compareWithWorktree.Click += (o, e) => + { + SearchResultSelectedCommit = null; + + if (_histories != null) + { + var target = new Commands.QuerySingleCommit(FullPath, branch.Head).Result(); + _histories.AutoSelectedCommit = null; + _histories.DetailContext = new RevisionCompare(FullPath, target, null); + } + }; + menu.Items.Add(compareWithWorktree); + } + menu.Items.Add(new MenuItem() { Header = "-" }); } } diff --git a/src/ViewModels/RevisionCompare.cs b/src/ViewModels/RevisionCompare.cs index 1ed85c9e..a2fd25ca 100644 --- a/src/ViewModels/RevisionCompare.cs +++ b/src/ViewModels/RevisionCompare.cs @@ -10,6 +10,11 @@ using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.ViewModels { + public class CompareTargetWorktree + { + public string SHA => string.Empty; + } + public class RevisionCompare : ObservableObject { public Models.Commit StartPoint @@ -18,7 +23,7 @@ namespace SourceGit.ViewModels private set; } - public Models.Commit EndPoint + public object EndPoint { get; private set; @@ -51,7 +56,7 @@ namespace SourceGit.ViewModels else { SelectedNode = FileTreeNode.SelectByPath(_changeTree, value.Path); - DiffContext = new DiffContext(_repo, new Models.DiffOption(StartPoint.SHA, EndPoint.SHA, value), _diffContext); + DiffContext = new DiffContext(_repo, new Models.DiffOption(StartPoint.SHA, _endPoint, value), _diffContext); } } } @@ -98,11 +103,21 @@ namespace SourceGit.ViewModels { _repo = repo; StartPoint = startPoint; - EndPoint = endPoint; + + if (endPoint == null) + { + EndPoint = new CompareTargetWorktree(); + _endPoint = string.Empty; + } + else + { + EndPoint = endPoint; + _endPoint = endPoint.SHA; + } Task.Run(() => { - _changes = new Commands.CompareRevisions(_repo, startPoint.SHA, endPoint.SHA).Result(); + _changes = new Commands.CompareRevisions(_repo, startPoint.SHA, _endPoint).Result(); var visible = _changes; if (!string.IsNullOrWhiteSpace(_searchFilter)) @@ -162,7 +177,7 @@ namespace SourceGit.ViewModels diffWithMerger.Icon = App.CreateMenuIcon("Icons.Diff"); diffWithMerger.Click += (_, ev) => { - var opt = new Models.DiffOption(StartPoint.SHA, EndPoint.SHA, change); + var opt = new Models.DiffOption(StartPoint.SHA, _endPoint, change); var type = Preference.Instance.ExternalMergeToolType; var exec = Preference.Instance.ExternalMergeToolPath; @@ -234,6 +249,7 @@ namespace SourceGit.ViewModels } private string _repo = string.Empty; + private string _endPoint = string.Empty; private List _changes = null; private List _visibleChanges = null; private List _changeTree = null; diff --git a/src/Views/RevisionCompare.axaml b/src/Views/RevisionCompare.axaml index c541cfe2..ac3f91ee 100644 --- a/src/Views/RevisionCompare.axaml +++ b/src/Views/RevisionCompare.axaml @@ -11,39 +11,55 @@ x:DataType="vm:RevisionCompare" Background="{DynamicResource Brush.Window}"> - - + + - + - - + + + + + - + - - - - - - - - + + + + + + + + + + + + + + - - + + + + + + + + + From b192a1c4235f0080f1e3b444d265661a567d75cd Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 28 May 2024 21:19:53 +0800 Subject: [PATCH 04/43] 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}"/> - @@ -222,7 +222,7 @@ - + @@ -287,7 +287,7 @@ - + diff --git a/src/Views/Repository.axaml.cs b/src/Views/Repository.axaml.cs index ed26905a..8b66ceb9 100644 --- a/src/Views/Repository.axaml.cs +++ b/src/Views/Repository.axaml.cs @@ -63,7 +63,7 @@ namespace SourceGit.Views InitializeComponent(); } - private void OnOpenWithExternalTools(object sender, RoutedEventArgs e) + private void OpenWithExternalTools(object sender, RoutedEventArgs e) { if (sender is Button button && DataContext is ViewModels.Repository repo) { @@ -73,6 +73,58 @@ namespace SourceGit.Views } } + private void OpenGitFlowMenu(object sender, RoutedEventArgs e) + { + if (DataContext is ViewModels.Repository repo) + { + var menu = repo.CreateContextMenuForGitFlow(); + (sender as Control)?.OpenContextMenu(menu); + } + + e.Handled = true; + } + + private async void OpenStatistics(object sender, RoutedEventArgs e) + { + if (DataContext is ViewModels.Repository repo) + { + var dialog = new Statistics() { DataContext = new ViewModels.Statistics(repo.FullPath) }; + await dialog.ShowDialog(TopLevel.GetTopLevel(this) as Window); + e.Handled = true; + } + } + + private void OnSearchCommitPanelPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + { + var grid = sender as Grid; + if (e.Property == IsVisibleProperty && grid.IsVisible) + txtSearchCommitsBox.Focus(); + } + + private void OnSearchKeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.Enter) + { + if (DataContext is ViewModels.Repository repo) + repo.StartSearchCommits(); + + e.Handled = true; + } + } + + private void OnSearchResultDataGridSelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (sender is DataGrid datagrid && datagrid.SelectedItem != null) + { + if (DataContext is ViewModels.Repository repo) + { + var commit = datagrid.SelectedItem as Models.Commit; + repo.NavigateToCommit(commit.SHA); + } + } + e.Handled = true; + } + private void OnLocalBranchTreeSelectionChanged(object sender, SelectionChangedEventArgs e) { if (sender is TreeView tree && tree.SelectedItem != null && DataContext is ViewModels.Repository repo) @@ -113,74 +165,6 @@ namespace SourceGit.Views } } - private void OnTagDataGridSelectionChanged(object sender, SelectionChangedEventArgs e) - { - if (sender is DataGrid datagrid && datagrid.SelectedItem != null) - { - localBranchTree.UnselectAll(); - remoteBranchTree.UnselectAll(); - - var tag = datagrid.SelectedItem as Models.Tag; - if (DataContext is ViewModels.Repository repo) - repo.NavigateToCommit(tag.SHA); - } - } - - private void OnSearchCommitPanelPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) - { - var grid = sender as Grid; - if (e.Property == IsVisibleProperty && grid.IsVisible) - txtSearchCommitsBox.Focus(); - } - - private void OnSearchKeyDown(object sender, KeyEventArgs e) - { - if (e.Key == Key.Enter) - { - if (DataContext is ViewModels.Repository repo) - repo.StartSearchCommits(); - - e.Handled = true; - } - } - - private void OnSearchResultDataGridSelectionChanged(object sender, SelectionChangedEventArgs e) - { - if (sender is DataGrid datagrid && datagrid.SelectedItem != null) - { - if (DataContext is ViewModels.Repository repo) - { - var commit = datagrid.SelectedItem as Models.Commit; - repo.NavigateToCommit(commit.SHA); - } - } - e.Handled = true; - } - - private void OnToggleFilter(object sender, RoutedEventArgs e) - { - if (sender is ToggleButton toggle) - { - var filter = string.Empty; - if (toggle.DataContext is ViewModels.BranchTreeNode node) - { - if (node.IsBranch) - filter = (node.Backend as Models.Branch).FullName; - } - else if (toggle.DataContext is Models.Tag tag) - { - filter = tag.Name; - } - - if (!string.IsNullOrEmpty(filter) && DataContext is ViewModels.Repository repo) - { - repo.UpdateFilter(filter, toggle.IsChecked == true); - } - } - - e.Handled = true; - } - private void OnLocalBranchContextMenuRequested(object sender, ContextRequestedEventArgs e) { remoteBranchTree.UnselectAll(); @@ -193,11 +177,11 @@ namespace SourceGit.Views e.Handled = true; return; } - + var branches = new List(); foreach (var item in tree.SelectedItems) CollectBranchesFromNode(branches, item as ViewModels.BranchTreeNode); - + if (branches.Count == 1) { var item = (e.Source as Control)?.FindAncestorOfType(true); @@ -229,7 +213,7 @@ namespace SourceGit.Views { localBranchTree.UnselectAll(); tagsList.SelectedItem = null; - + var repo = DataContext as ViewModels.Repository; var tree = sender as TreeView; if (tree.SelectedItems.Count == 0) @@ -249,12 +233,12 @@ namespace SourceGit.Views var menu = repo.CreateContextMenuForRemote(node.Backend as Models.Remote); item.OpenContextMenu(menu); } - + e.Handled = true; return; } } - + var branches = new List(); foreach (var item in tree.SelectedItems) CollectBranchesFromNode(branches, item as ViewModels.BranchTreeNode); @@ -286,6 +270,39 @@ namespace SourceGit.Views e.Handled = true; } + private void OnDoubleTappedBranchNode(object sender, TappedEventArgs e) + { + if (!ViewModels.PopupHost.CanCreatePopup()) + return; + + if (sender is Grid grid && DataContext is ViewModels.Repository repo) + { + var node = grid.DataContext as ViewModels.BranchTreeNode; + if (node != null && node.IsBranch) + { + var branch = node.Backend as Models.Branch; + if (branch.IsCurrent) + return; + + repo.CheckoutBranch(branch); + e.Handled = true; + } + } + } + + private void OnTagDataGridSelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (sender is DataGrid datagrid && datagrid.SelectedItem != null) + { + localBranchTree.UnselectAll(); + remoteBranchTree.UnselectAll(); + + var tag = datagrid.SelectedItem as Models.Tag; + if (DataContext is ViewModels.Repository repo) + repo.NavigateToCommit(tag.SHA); + } + } + private void OnTagContextRequested(object sender, ContextRequestedEventArgs e) { if (sender is DataGrid datagrid && datagrid.SelectedItem != null && DataContext is ViewModels.Repository repo) @@ -298,6 +315,30 @@ namespace SourceGit.Views e.Handled = true; } + private void OnToggleFilter(object sender, RoutedEventArgs e) + { + if (sender is ToggleButton toggle) + { + var filter = string.Empty; + if (toggle.DataContext is ViewModels.BranchTreeNode node) + { + if (node.IsBranch) + filter = (node.Backend as Models.Branch).FullName; + } + else if (toggle.DataContext is Models.Tag tag) + { + filter = tag.Name; + } + + if (!string.IsNullOrEmpty(filter) && DataContext is ViewModels.Repository repo) + { + repo.UpdateFilter(filter, toggle.IsChecked == true); + } + } + + e.Handled = true; + } + private void OnSubmoduleContextRequested(object sender, ContextRequestedEventArgs e) { if (sender is DataGrid datagrid && datagrid.SelectedItem != null && DataContext is ViewModels.Repository repo) @@ -310,17 +351,6 @@ namespace SourceGit.Views e.Handled = true; } - private void OpenGitFlowMenu(object sender, RoutedEventArgs e) - { - if (DataContext is ViewModels.Repository repo) - { - var menu = repo.CreateContextMenuForGitFlow(); - (sender as Control)?.OpenContextMenu(menu); - } - - e.Handled = true; - } - private async void UpdateSubmodules(object sender, RoutedEventArgs e) { if (DataContext is ViewModels.Repository repo) @@ -334,36 +364,6 @@ namespace SourceGit.Views e.Handled = true; } - - private void OnDoubleTappedLocalBranchNode(object sender, TappedEventArgs e) - { - if (!ViewModels.PopupHost.CanCreatePopup()) - return; - - if (sender is Grid grid && DataContext is ViewModels.Repository repo) - { - var node = grid.DataContext as ViewModels.BranchTreeNode; - if (node != null && node.IsBranch) - { - var branch = node.Backend as Models.Branch; - if (branch.IsCurrent) - return; - - repo.CheckoutLocalBranch(branch.Name); - e.Handled = true; - } - } - } - - private async void OpenStatistics(object sender, RoutedEventArgs e) - { - if (DataContext is ViewModels.Repository repo) - { - var dialog = new Statistics() { DataContext = new ViewModels.Statistics(repo.FullPath) }; - await dialog.ShowDialog(TopLevel.GetTopLevel(this) as Window); - e.Handled = true; - } - } private void CollectBranchesFromNode(List outs, ViewModels.BranchTreeNode node) { From fdc41515b7389a3d4ab10388a2cd820cff048517 Mon Sep 17 00:00:00 2001 From: leo Date: Wed, 29 May 2024 17:08:41 +0800 Subject: [PATCH 10/43] ux: remove text decorators of commit SHA in the left panel of FileHistories --- src/Views/FileHistories.axaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Views/FileHistories.axaml b/src/Views/FileHistories.axaml index 436ca4b1..a2557275 100644 --- a/src/Views/FileHistories.axaml +++ b/src/Views/FileHistories.axaml @@ -86,7 +86,7 @@ - + From 7154221946fbc86ca08343a2d1a3fea9d4ddf1f9 Mon Sep 17 00:00:00 2001 From: leo Date: Wed, 29 May 2024 17:31:01 +0800 Subject: [PATCH 11/43] ux: new style for ChangeViewModeSwitcher --- src/Resources/Styles.axaml | 6 ++++++ src/Views/ChangeViewModeSwitcher.axaml | 8 ++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Resources/Styles.axaml b/src/Resources/Styles.axaml index 2f8bb671..85bf7602 100644 --- a/src/Resources/Styles.axaml +++ b/src/Resources/Styles.axaml @@ -60,6 +60,12 @@ + + + + diff --git a/src/Views/CommitChanges.axaml.cs b/src/Views/CommitChanges.axaml.cs index d9ac6c2e..f1d7633f 100644 --- a/src/Views/CommitChanges.axaml.cs +++ b/src/Views/CommitChanges.axaml.cs @@ -1,5 +1,5 @@ -using Avalonia; using Avalonia.Controls; +using Avalonia.Interactivity; namespace SourceGit.Views { @@ -19,7 +19,19 @@ namespace SourceGit.Views { var menu = vm.CreateChangeContextMenu(selected[0]); (sender as Control)?.OpenContextMenu(menu); - } + } + } + + e.Handled = true; + } + + private void OnChangeDoubleTapped(object sender, RoutedEventArgs e) + { + if (sender is ChangeCollectionView view) + { + var selected = view.tree?.RowSelection?.SelectedItem as ViewModels.FileTreeNode; + if (selected != null && selected.IsFolder) + selected.IsExpanded = !selected.IsExpanded; } e.Handled = true; diff --git a/src/Views/RevisionCompare.axaml b/src/Views/RevisionCompare.axaml index 9411c174..5e7d2ee1 100644 --- a/src/Views/RevisionCompare.axaml +++ b/src/Views/RevisionCompare.axaml @@ -105,7 +105,14 @@ ViewMode="{Binding Source={x:Static vm:Preference.Instance}, Path=CommitChangeViewMode}" Changes="{Binding VisibleChanges}" SelectedChanges="{Binding SelectedChanges, Mode=TwoWay}" - ContextRequested="OnChangeContextRequested"/> + ContextRequested="OnChangeContextRequested" + ChangeDoubleTapped="OnChangeDoubleTapped"> + + + + diff --git a/src/Views/RevisionCompare.axaml.cs b/src/Views/RevisionCompare.axaml.cs index e3ecb2b7..a7066639 100644 --- a/src/Views/RevisionCompare.axaml.cs +++ b/src/Views/RevisionCompare.axaml.cs @@ -1,5 +1,6 @@ using Avalonia.Controls; using Avalonia.Input; +using Avalonia.Interactivity; namespace SourceGit.Views { @@ -21,6 +22,18 @@ namespace SourceGit.Views e.Handled = true; } + private void OnChangeDoubleTapped(object sender, RoutedEventArgs e) + { + if (sender is ChangeCollectionView view) + { + var selected = view.tree?.RowSelection?.SelectedItem as ViewModels.FileTreeNode; + if (selected != null && selected.IsFolder) + selected.IsExpanded = !selected.IsExpanded; + } + + e.Handled = true; + } + private void OnPressedSHA(object sender, PointerPressedEventArgs e) { if (DataContext is ViewModels.RevisionCompare vm && sender is TextBlock block) diff --git a/src/Views/RevisionFiles.axaml b/src/Views/RevisionFiles.axaml index c46632f6..ea98bef2 100644 --- a/src/Views/RevisionFiles.axaml +++ b/src/Views/RevisionFiles.axaml @@ -56,6 +56,12 @@ + + + + From a382a3e5640791a2d320cd08aa5b2f7d51ad49a2 Mon Sep 17 00:00:00 2001 From: leo Date: Wed, 29 May 2024 20:51:24 +0800 Subject: [PATCH 16/43] revert: csproj changes for debuging --- src/SourceGit.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SourceGit.csproj b/src/SourceGit.csproj index 2830712a..a3d38391 100644 --- a/src/SourceGit.csproj +++ b/src/SourceGit.csproj @@ -1,6 +1,6 @@  - Exe + WinExe net8.0 true App.manifest From 7ec21e2e533997bdd4a11128d03811ef9e255243 Mon Sep 17 00:00:00 2001 From: "DESKTOP-L3MJ80L\\hamme" Date: Wed, 29 May 2024 21:31:46 +0800 Subject: [PATCH 17/43] Fix the crash caused by pushing without a branch --- src/ViewModels/Repository.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 93f29c2a..e0fda542 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -385,7 +385,10 @@ namespace SourceGit.ViewModels } if (Branches.Find(x => x.IsCurrent) == null) + { App.RaiseException(_fullpath, "Can NOT found current branch!!!"); + return; + } PopupHost.ShowPopup(new Push(this, null)); } From ea1bfad84d26c804635c4f85b2abe54edf53f289 Mon Sep 17 00:00:00 2001 From: leo Date: Wed, 29 May 2024 21:56:03 +0800 Subject: [PATCH 18/43] revert: disable double-click folding/unfolding because it will cause IndexOutOfRange exception --- src/ViewModels/CommitDetail.cs | 7 ------- src/Views/CommitChanges.axaml | 9 +-------- src/Views/CommitChanges.axaml.cs | 13 ------------- src/Views/RevisionCompare.axaml | 9 +-------- src/Views/RevisionCompare.axaml.cs | 13 ------------- src/Views/RevisionFiles.axaml | 6 ------ 6 files changed, 2 insertions(+), 55 deletions(-) diff --git a/src/ViewModels/CommitDetail.cs b/src/ViewModels/CommitDetail.cs index b4a1fadf..6f26465f 100644 --- a/src/ViewModels/CommitDetail.cs +++ b/src/ViewModels/CommitDetail.cs @@ -513,13 +513,6 @@ namespace SourceGit.ViewModels var selection = new Models.TreeDataGridSelectionModel(source, x => x.Children); selection.SingleSelect = true; - selection.RowDoubleTapped += (s, e) => - { - var model = s as Models.TreeDataGridSelectionModel; - var node = model.SelectedItem; - if (node != null && node.IsFolder) - node.IsExpanded = !node.IsExpanded; - }; selection.SelectionChanged += (s, _) => { if (s is Models.TreeDataGridSelectionModel selection) diff --git a/src/Views/CommitChanges.axaml b/src/Views/CommitChanges.axaml index 78c6bdb8..eccd0952 100644 --- a/src/Views/CommitChanges.axaml +++ b/src/Views/CommitChanges.axaml @@ -50,14 +50,7 @@ ViewMode="{Binding Source={x:Static vm:Preference.Instance}, Path=CommitChangeViewMode}" Changes="{Binding VisibleChanges}" SelectedChanges="{Binding SelectedChanges, Mode=TwoWay}" - ContextRequested="OnChangeContextRequested" - ChangeDoubleTapped="OnChangeDoubleTapped"> - - - - + ContextRequested="OnChangeContextRequested"/> diff --git a/src/Views/CommitChanges.axaml.cs b/src/Views/CommitChanges.axaml.cs index f1d7633f..b8200a32 100644 --- a/src/Views/CommitChanges.axaml.cs +++ b/src/Views/CommitChanges.axaml.cs @@ -1,5 +1,4 @@ using Avalonia.Controls; -using Avalonia.Interactivity; namespace SourceGit.Views { @@ -24,17 +23,5 @@ namespace SourceGit.Views e.Handled = true; } - - private void OnChangeDoubleTapped(object sender, RoutedEventArgs e) - { - if (sender is ChangeCollectionView view) - { - var selected = view.tree?.RowSelection?.SelectedItem as ViewModels.FileTreeNode; - if (selected != null && selected.IsFolder) - selected.IsExpanded = !selected.IsExpanded; - } - - e.Handled = true; - } } } diff --git a/src/Views/RevisionCompare.axaml b/src/Views/RevisionCompare.axaml index 5e7d2ee1..9411c174 100644 --- a/src/Views/RevisionCompare.axaml +++ b/src/Views/RevisionCompare.axaml @@ -105,14 +105,7 @@ ViewMode="{Binding Source={x:Static vm:Preference.Instance}, Path=CommitChangeViewMode}" Changes="{Binding VisibleChanges}" SelectedChanges="{Binding SelectedChanges, Mode=TwoWay}" - ContextRequested="OnChangeContextRequested" - ChangeDoubleTapped="OnChangeDoubleTapped"> - - - - + ContextRequested="OnChangeContextRequested"/> diff --git a/src/Views/RevisionCompare.axaml.cs b/src/Views/RevisionCompare.axaml.cs index a7066639..e3ecb2b7 100644 --- a/src/Views/RevisionCompare.axaml.cs +++ b/src/Views/RevisionCompare.axaml.cs @@ -1,6 +1,5 @@ using Avalonia.Controls; using Avalonia.Input; -using Avalonia.Interactivity; namespace SourceGit.Views { @@ -22,18 +21,6 @@ namespace SourceGit.Views e.Handled = true; } - private void OnChangeDoubleTapped(object sender, RoutedEventArgs e) - { - if (sender is ChangeCollectionView view) - { - var selected = view.tree?.RowSelection?.SelectedItem as ViewModels.FileTreeNode; - if (selected != null && selected.IsFolder) - selected.IsExpanded = !selected.IsExpanded; - } - - e.Handled = true; - } - private void OnPressedSHA(object sender, PointerPressedEventArgs e) { if (DataContext is ViewModels.RevisionCompare vm && sender is TextBlock block) diff --git a/src/Views/RevisionFiles.axaml b/src/Views/RevisionFiles.axaml index ea98bef2..c46632f6 100644 --- a/src/Views/RevisionFiles.axaml +++ b/src/Views/RevisionFiles.axaml @@ -56,12 +56,6 @@ - - - - From 55c9fae110b368c8a94405edf2ad9a09b9866c87 Mon Sep 17 00:00:00 2001 From: leo Date: Thu, 30 May 2024 09:53:07 +0800 Subject: [PATCH 19/43] feature: new way to expand/collapse folder node in TreeDataGrid --- src/{ViewModels => Models}/FileTreeNode.cs | 40 +++------------------- src/Models/TreeDataGridSelectionModel.cs | 22 +++++++++++- src/Resources/Styles.axaml | 4 +++ src/ViewModels/CommitDetail.cs | 24 ++++++------- src/ViewModels/Repository.cs | 1 + src/ViewModels/RevisionCompare.cs | 6 +--- src/Views/ChangeCollectionView.axaml | 2 +- src/Views/ChangeCollectionView.axaml.cs | 28 +++++++-------- src/Views/RevisionFiles.axaml | 2 +- src/Views/RevisionFiles.axaml.cs | 2 +- 10 files changed, 58 insertions(+), 73 deletions(-) rename src/{ViewModels => Models}/FileTreeNode.cs (84%) diff --git a/src/ViewModels/FileTreeNode.cs b/src/Models/FileTreeNode.cs similarity index 84% rename from src/ViewModels/FileTreeNode.cs rename to src/Models/FileTreeNode.cs index 8e3ba076..ad1298c9 100644 --- a/src/ViewModels/FileTreeNode.cs +++ b/src/Models/FileTreeNode.cs @@ -1,24 +1,17 @@ using System; using System.Collections.Generic; -using CommunityToolkit.Mvvm.ComponentModel; - -namespace SourceGit.ViewModels +namespace SourceGit.Models { - public class FileTreeNode : ObservableObject + public class FileTreeNode { public string FullPath { get; set; } = string.Empty; public bool IsFolder { get; set; } = false; + public bool IsExpanded { get; set; } = false; public object Backend { get; set; } = null; public List Children { get; set; } = new List(); - public bool IsExpanded - { - get => _isExpanded; - set => SetProperty(ref _isExpanded, value); - } - - public static List Build(List changes, bool expanded) + public static List Build(List changes, bool expanded) { var nodes = new List(); var folders = new Dictionary(); @@ -93,7 +86,7 @@ namespace SourceGit.ViewModels return nodes; } - public static List Build(List files, bool expanded) + public static List Build(List files, bool expanded) { var nodes = new List(); var folders = new Dictionary(); @@ -168,27 +161,6 @@ namespace SourceGit.ViewModels return nodes; } - public static FileTreeNode SelectByPath(List nodes, string path) - { - foreach (var node in nodes) - { - if (node.FullPath == path) - return node; - - if (node.IsFolder && path.StartsWith(node.FullPath + "/", StringComparison.Ordinal)) - { - var foundInChildren = SelectByPath(node.Children, path); - if (foundInChildren != null) - { - node.IsExpanded = true; - } - return foundInChildren; - } - } - - return null; - } - private static void Sort(List nodes) { nodes.Sort((l, r) => @@ -209,7 +181,5 @@ namespace SourceGit.ViewModels Sort(node.Children); } } - - private bool _isExpanded = true; } } diff --git a/src/Models/TreeDataGridSelectionModel.cs b/src/Models/TreeDataGridSelectionModel.cs index efdda2a4..b016d739 100644 --- a/src/Models/TreeDataGridSelectionModel.cs +++ b/src/Models/TreeDataGridSelectionModel.cs @@ -188,7 +188,12 @@ namespace SourceGit.Models } else if (e.ClickCount % 2 == 0) { - _rowDoubleTapped?.Invoke(this, e); + var focus = _source.Rows[row.RowIndex]; + if (focus is IExpander expander && HasChildren(focus)) + expander.IsExpanded = !expander.IsExpanded; + else + _rowDoubleTapped?.Invoke(this, e); + e.Handled = true; } else if (sender.RowSelection.Count > 1) @@ -420,7 +425,22 @@ namespace SourceGit.Models protected override IEnumerable GetChildren(TModel node) { + if (node == null) + return null; + return _childrenGetter?.Invoke(node); } + + private bool HasChildren(IRow row) + { + var children = GetChildren(row.Model as TModel); + if (children != null) + { + foreach (var c in children) + return true; + } + + return false; + } } } diff --git a/src/Resources/Styles.axaml b/src/Resources/Styles.axaml index 37a0670f..6b76c2cf 100644 --- a/src/Resources/Styles.axaml +++ b/src/Resources/Styles.axaml @@ -1123,6 +1123,10 @@ + + diff --git a/src/ViewModels/CommitDetail.cs b/src/ViewModels/CommitDetail.cs index 6f26465f..b96c1fe1 100644 --- a/src/ViewModels/CommitDetail.cs +++ b/src/ViewModels/CommitDetail.cs @@ -5,7 +5,6 @@ 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; @@ -77,7 +76,7 @@ namespace SourceGit.ViewModels } } - public HierarchicalTreeDataGridSource RevisionFiles + public HierarchicalTreeDataGridSource RevisionFiles { get => _revisionFiles; private set => SetProperty(ref _revisionFiles, value); @@ -336,7 +335,6 @@ namespace SourceGit.ViewModels } } - var tree = FileTreeNode.Build(visible, true); Dispatcher.UIThread.Invoke(() => { Changes = changes; @@ -362,7 +360,7 @@ namespace SourceGit.ViewModels } } - var tree = FileTreeNode.Build(visible, isSearching || visible.Count <= 100); + var tree = Models.FileTreeNode.Build(visible, isSearching || visible.Count <= 100); Dispatcher.UIThread.Invoke(() => BuildRevisionFilesSource(tree)); }); } @@ -406,7 +404,7 @@ namespace SourceGit.ViewModels } } - BuildRevisionFilesSource(FileTreeNode.Build(visible, isSearching || visible.Count < 100)); + BuildRevisionFilesSource(Models.FileTreeNode.Build(visible, isSearching || visible.Count < 100)); } private void RefreshViewRevisionFile(Models.Object file) @@ -493,29 +491,29 @@ namespace SourceGit.ViewModels } } - private void BuildRevisionFilesSource(List tree) + private void BuildRevisionFilesSource(List tree) { - var source = new HierarchicalTreeDataGridSource(tree) + var source = new HierarchicalTreeDataGridSource(tree) { Columns = { - new HierarchicalExpanderColumn( - new TemplateColumn("Icon", "FileTreeNodeExpanderTemplate", null, GridLength.Auto), + new HierarchicalExpanderColumn( + new TemplateColumn("Icon", "FileTreeNodeExpanderTemplate", null, GridLength.Auto), x => x.Children, x => x.Children.Count > 0, x => x.IsExpanded), - new TextColumn( + new TextColumn( null, x => string.Empty, GridLength.Star) } }; - var selection = new Models.TreeDataGridSelectionModel(source, x => x.Children); + var selection = new Models.TreeDataGridSelectionModel(source, x => x.Children); selection.SingleSelect = true; selection.SelectionChanged += (s, _) => { - if (s is Models.TreeDataGridSelectionModel selection) + if (s is Models.TreeDataGridSelectionModel selection) RefreshViewRevisionFile(selection.SelectedItem?.Backend as Models.Object); }; @@ -537,7 +535,7 @@ namespace SourceGit.ViewModels private string _searchChangeFilter = string.Empty; private DiffContext _diffContext = null; private List _revisionFilesBackup = null; - private HierarchicalTreeDataGridSource _revisionFiles = 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/Repository.cs b/src/ViewModels/Repository.cs index e0fda542..c05ae641 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -389,6 +389,7 @@ namespace SourceGit.ViewModels App.RaiseException(_fullpath, "Can NOT found current branch!!!"); return; } + PopupHost.ShowPopup(new Push(this, null)); } diff --git a/src/ViewModels/RevisionCompare.cs b/src/ViewModels/RevisionCompare.cs index 536d71bc..9026aa7f 100644 --- a/src/ViewModels/RevisionCompare.cs +++ b/src/ViewModels/RevisionCompare.cs @@ -99,11 +99,7 @@ namespace SourceGit.ViewModels } } - var tree = FileTreeNode.Build(visible, true); - Dispatcher.UIThread.Invoke(() => - { - VisibleChanges = visible; - }); + Dispatcher.UIThread.Invoke(() => VisibleChanges = visible); }); } diff --git a/src/Views/ChangeCollectionView.axaml b/src/Views/ChangeCollectionView.axaml index 9b37cb17..b09d76d2 100644 --- a/src/Views/ChangeCollectionView.axaml +++ b/src/Views/ChangeCollectionView.axaml @@ -16,7 +16,7 @@ CanUserSortColumns="False" ScrollViewer.BringIntoViewOnFocusChange="True"> - + diff --git a/src/Views/ChangeCollectionView.axaml.cs b/src/Views/ChangeCollectionView.axaml.cs index a2dd928d..6ce04ff6 100644 --- a/src/Views/ChangeCollectionView.axaml.cs +++ b/src/Views/ChangeCollectionView.axaml.cs @@ -91,29 +91,25 @@ namespace SourceGit.Views var viewMode = ViewMode; if (viewMode == Models.ChangeViewMode.Tree) { - var filetree = ViewModels.FileTreeNode.Build(changes, true); - var source = new HierarchicalTreeDataGridSource(filetree) + var filetree = Models.FileTreeNode.Build(changes, true); + var source = new HierarchicalTreeDataGridSource(filetree) { Columns = { - new HierarchicalExpanderColumn( - new TemplateColumn(null, "TreeModeTemplate", null, GridLength.Auto), + 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) + x => x.IsExpanded) } }; - var selection = new Models.TreeDataGridSelectionModel(source, x => x.Children); + var selection = new Models.TreeDataGridSelectionModel(source, x => x.Children); selection.SingleSelect = SingleSelect; selection.RowDoubleTapped += (_, e) => RaiseEvent(new RoutedEventArgs(ChangeDoubleTappedEvent)); selection.SelectionChanged += (s, _) => { - if (!_isSelecting && s is Models.TreeDataGridSelectionModel model) + if (!_isSelecting && s is Models.TreeDataGridSelectionModel model) { var selection = new List(); foreach (var c in model.SelectedItems) @@ -202,7 +198,7 @@ namespace SourceGit.Views else changeSelection.Select(selected); } - else if (tree.Source.Selection is Models.TreeDataGridSelectionModel treeSelection) + else if (tree.Source.Selection is Models.TreeDataGridSelectionModel treeSelection) { if (selected == null || selected.Count == 0) { @@ -215,9 +211,9 @@ namespace SourceGit.Views foreach (var c in selected) set.Add(c); - var nodes = new List(); + var nodes = new List(); foreach (var node in tree.Source.Items) - CollectSelectedNodeByChange(nodes, node as ViewModels.FileTreeNode, set); + CollectSelectedNodeByChange(nodes, node as Models.FileTreeNode, set); if (nodes.Count == 0) { @@ -231,7 +227,7 @@ namespace SourceGit.Views _isSelecting = false; } - private void CollectChangesInNode(List outs, ViewModels.FileTreeNode node) + private void CollectChangesInNode(List outs, Models.FileTreeNode node) { if (node.IsFolder) { @@ -246,7 +242,7 @@ namespace SourceGit.Views } } - private void CollectSelectedNodeByChange(List outs, ViewModels.FileTreeNode node, HashSet selected) + private void CollectSelectedNodeByChange(List outs, Models.FileTreeNode node, HashSet selected) { if (node == null) return; diff --git a/src/Views/RevisionFiles.axaml b/src/Views/RevisionFiles.axaml index c46632f6..82d2651c 100644 --- a/src/Views/RevisionFiles.axaml +++ b/src/Views/RevisionFiles.axaml @@ -48,7 +48,7 @@ Source="{Binding RevisionFiles}" ContextRequested="OnFileContextRequested"> - + diff --git a/src/Views/RevisionFiles.axaml.cs b/src/Views/RevisionFiles.axaml.cs index 730e9b34..2720009b 100644 --- a/src/Views/RevisionFiles.axaml.cs +++ b/src/Views/RevisionFiles.axaml.cs @@ -217,7 +217,7 @@ namespace SourceGit.Views { if (DataContext is ViewModels.CommitDetail vm && sender is TreeDataGrid tree) { - var selected = tree.RowSelection.SelectedItem as ViewModels.FileTreeNode; + var selected = tree.RowSelection.SelectedItem as Models.FileTreeNode; if (selected != null && !selected.IsFolder && selected.Backend is Models.Object obj) { var menu = vm.CreateRevisionFileContextMenu(obj); From 04f42934215eb1cca6832228995903519507020e Mon Sep 17 00:00:00 2001 From: leo Date: Thu, 30 May 2024 10:00:24 +0800 Subject: [PATCH 20/43] fix: remove binding to SelectedChanges from change list in the INFORMATION page --- src/Views/CommitDetail.axaml | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Views/CommitDetail.axaml b/src/Views/CommitDetail.axaml index 659ba19d..802c1ab8 100644 --- a/src/Views/CommitDetail.axaml +++ b/src/Views/CommitDetail.axaml @@ -38,7 +38,6 @@ Margin="8,0,0,0" ViewMode="List" Changes="{Binding Changes}" - SelectedChanges="{Binding SelectedChanges, Mode=TwoWay}" ContextRequested="OnChangeListContextRequested" ChangeDoubleTapped="OnChangeDoubleTapped"> From fa3a3b2dad50de23d8b97b531bdb0a099c9bd263 Mon Sep 17 00:00:00 2001 From: leo Date: Thu, 30 May 2024 15:13:59 +0800 Subject: [PATCH 21/43] refactor: code cleanup --- src/Converters/BoolConverters.cs | 9 ++-- src/Converters/StringConverters.cs | 2 +- src/Converters/WindowStateConverters.cs | 14 ------ src/Resources/Styles.axaml | 6 +++ src/ViewModels/AssumeUnchangedManager.cs | 4 +- src/ViewModels/Clone.cs | 4 +- src/ViewModels/Repository.cs | 17 ++----- src/ViewModels/StashesPage.cs | 56 ++++++++++++--------- src/ViewModels/Welcome.cs | 14 +++--- src/ViewModels/WorkingCopy.cs | 56 +++++++++++++++++++-- src/Views/AssumeUnchangedManager.axaml | 4 +- src/Views/AssumeUnchangedManager.axaml.cs | 7 +++ src/Views/CaptionButtons.axaml | 4 +- src/Views/CommitBaseInfo.axaml | 16 +++--- src/Views/DiffView.axaml | 3 +- src/Views/Histories.axaml | 1 - src/Views/Histories.axaml.cs | 20 +------- src/Views/Launcher.axaml | 16 ++++-- src/Views/Launcher.axaml.cs | 61 ----------------------- src/Views/Repository.axaml | 16 +++++- src/Views/Repository.axaml.cs | 45 ----------------- src/Views/StashesPage.axaml | 10 +--- src/Views/StashesPage.axaml.cs | 10 ++++ src/Views/Welcome.axaml | 2 +- src/Views/WorkingCopy.axaml | 10 +--- 25 files changed, 174 insertions(+), 233 deletions(-) diff --git a/src/Converters/BoolConverters.cs b/src/Converters/BoolConverters.cs index c860a1a6..2eb8c60a 100644 --- a/src/Converters/BoolConverters.cs +++ b/src/Converters/BoolConverters.cs @@ -1,18 +1,17 @@ -using Avalonia.Controls; -using Avalonia.Data.Converters; +using Avalonia.Data.Converters; using Avalonia.Media; namespace SourceGit.Converters { public static class BoolConverters { + public static readonly FuncValueConverter ToPageTabWidth = + new FuncValueConverter(x => x ? 200 : double.NaN); + public static readonly FuncValueConverter HalfIfFalse = new FuncValueConverter(x => x ? 1 : 0.5); public static readonly FuncValueConverter BoldIfTrue = new FuncValueConverter(x => x ? FontWeight.Bold : FontWeight.Regular); - - public static readonly FuncValueConverter ToStarOrAutoGridLength = - new(value => value ? new GridLength(1, GridUnitType.Star) : new GridLength(1, GridUnitType.Auto)); } } diff --git a/src/Converters/StringConverters.cs b/src/Converters/StringConverters.cs index 35491a16..aa687f23 100644 --- a/src/Converters/StringConverters.cs +++ b/src/Converters/StringConverters.cs @@ -69,7 +69,7 @@ namespace SourceGit.Converters public static readonly FormatByResourceKeyConverter FormatByResourceKey = new FormatByResourceKeyConverter(); public static readonly FuncValueConverter ToShortSHA = - new FuncValueConverter(v => v.Length > 10 ? v.Substring(0, 10) : v); + new FuncValueConverter(v => v == null ? string.Empty : (v.Length > 10 ? v.Substring(0, 10) : v)); public static readonly FuncValueConverter UnderRecommendGitVersion = new(v => diff --git a/src/Converters/WindowStateConverters.cs b/src/Converters/WindowStateConverters.cs index 2c3b2ac6..7122dc1f 100644 --- a/src/Converters/WindowStateConverters.cs +++ b/src/Converters/WindowStateConverters.cs @@ -3,7 +3,6 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Data.Converters; -using Avalonia.Media; namespace SourceGit.Converters { @@ -39,19 +38,6 @@ namespace SourceGit.Converters } }); - public static readonly FuncValueConverter ToMaxOrRestoreIcon = - new FuncValueConverter(state => - { - if (state == WindowState.Maximized) - { - return Application.Current?.FindResource("Icons.Window.Restore") as StreamGeometry; - } - else - { - return Application.Current?.FindResource("Icons.Window.Maximize") as StreamGeometry; - } - }); - public static readonly FuncValueConverter IsNormal = new FuncValueConverter(state => state == WindowState.Normal); } diff --git a/src/Resources/Styles.axaml b/src/Resources/Styles.axaml index 6b76c2cf..85ccca13 100644 --- a/src/Resources/Styles.axaml +++ b/src/Resources/Styles.axaml @@ -66,6 +66,12 @@ + + - + + - - - + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/CommitDetail.axaml.cs b/src/Views/CommitDetail.axaml.cs index 84cb8381..75806e59 100644 --- a/src/Views/CommitDetail.axaml.cs +++ b/src/Views/CommitDetail.axaml.cs @@ -1,5 +1,5 @@ using Avalonia.Controls; -using Avalonia.Interactivity; +using Avalonia.Input; namespace SourceGit.Views { @@ -10,31 +10,33 @@ namespace SourceGit.Views InitializeComponent(); } - private void OnChangeListContextRequested(object sender, ContextRequestedEventArgs e) + private void OnChangeListDoubleTapped(object sender, TappedEventArgs e) { - if (DataContext is ViewModels.CommitDetail vm && sender is ChangeCollectionView view) + if (DataContext is ViewModels.CommitDetail detail) { - var selected = view.SelectedChanges; - if (selected != null && selected.Count == 1) - { - var menu = vm.CreateChangeContextMenu(selected[0]); - view.OpenContextMenu(menu); - } + var datagrid = sender as DataGrid; + detail.ActivePageIndex = 1; + detail.SelectedChanges = new () { datagrid.SelectedItem as Models.Change }; } + e.Handled = true; } - private void OnChangeDoubleTapped(object sender, RoutedEventArgs e) + private void OnChangeListContextRequested(object sender, ContextRequestedEventArgs e) { - if (DataContext is ViewModels.CommitDetail vm && sender is ChangeCollectionView view) + if (DataContext is ViewModels.CommitDetail detail) { - var selected = view.SelectedChanges; - if (selected != null && selected.Count == 1) + var datagrid = sender as DataGrid; + if (datagrid.SelectedItem == null) { - vm.ActivePageIndex = 1; - vm.SelectedChanges = new() { selected[0] }; + e.Handled = true; + return; } + + var menu = detail.CreateChangeContextMenu(datagrid.SelectedItem as Models.Change); + datagrid.OpenContextMenu(menu); } + e.Handled = true; } } From 6a98af17e4065fed8d90e03943e5682cfb80a347 Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 3 Jun 2024 10:17:47 +0800 Subject: [PATCH 42/43] ux: add missing progress description --- src/ViewModels/CreateBranch.cs | 1 + src/ViewModels/GitFlowFinish.cs | 1 + src/ViewModels/GitFlowStart.cs | 1 + 3 files changed, 3 insertions(+) diff --git a/src/ViewModels/CreateBranch.cs b/src/ViewModels/CreateBranch.cs index 57cc9aff..ad270809 100644 --- a/src/ViewModels/CreateBranch.cs +++ b/src/ViewModels/CreateBranch.cs @@ -126,6 +126,7 @@ namespace SourceGit.ViewModels } else { + SetProgressDescription($"Create new branch '{_name}'"); Commands.Branch.Create(_repo.FullPath, _name, _baseOnRevision); } diff --git a/src/ViewModels/GitFlowFinish.cs b/src/ViewModels/GitFlowFinish.cs index 1520285a..1fad99ae 100644 --- a/src/ViewModels/GitFlowFinish.cs +++ b/src/ViewModels/GitFlowFinish.cs @@ -42,6 +42,7 @@ namespace SourceGit.ViewModels break; } + SetProgressDescription($"Git Flow - finishing {_branch.Name} ..."); var succ = new Commands.GitFlow(_repo.FullPath).Finish(_type, branch, KeepBranch); CallUIThread(() => _repo.SetWatcherEnabled(true)); return succ; diff --git a/src/ViewModels/GitFlowStart.cs b/src/ViewModels/GitFlowStart.cs index ddcec725..33fa3cf4 100644 --- a/src/ViewModels/GitFlowStart.cs +++ b/src/ViewModels/GitFlowStart.cs @@ -65,6 +65,7 @@ namespace SourceGit.ViewModels _repo.SetWatcherEnabled(false); return Task.Run(() => { + SetProgressDescription($"Git Flow - starting {_prefix}{_name} ..."); var succ = new Commands.GitFlow(_repo.FullPath).Start(_type, _name); CallUIThread(() => _repo.SetWatcherEnabled(true)); return succ; From 9d46a9ae783281b0070ff5a2faead56dc4d4af30 Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 3 Jun 2024 10:21:18 +0800 Subject: [PATCH 43/43] version: Release 8.15 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index b9d71048..d9316e8b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.14 \ No newline at end of file +8.15 \ No newline at end of file