From 04617c525ce640ca589f9c1a05163a49f23625a4 Mon Sep 17 00:00:00 2001 From: RussKie Date: Sat, 29 Aug 2020 22:55:18 +1000 Subject: [PATCH] Expose sort by/order in the left panel --- GitCommands/IDiffListSortService.cs | 3 - .../GitRefsSortByContextMenuItem.cs | 73 ++++++++++++++ .../GitRefsSortOrderContextMenuItem.cs | 75 ++++++++++++++ .../RepoObjectsTree.ContextActions.cs | 50 +++++++++- .../RepoObjectsTree.Nodes.Branches.cs | 36 ++++--- .../RepoObjectsTree.Nodes.Remotes.cs | 33 ++++--- .../RepoObjectsTree.Nodes.Tags.cs | 38 ++++--- .../BranchTreePanel/RepoObjectsTree.Nodes.cs | 2 +- GitUI/BranchTreePanel/RepoObjectsTree.cs | 17 +++- GitUI/Strings.cs | 6 ++ GitUI/Translation/English.xlf | 16 +++ GitUI/UserControls/FileStatusList.cs | 16 +-- .../SortDiffListContextMenuItem.cs | 14 +-- .../GitRefsSortByContextMenuItemTests.cs | 99 +++++++++++++++++++ .../GitRefsSortOrderContextMenuItemTests.cs | 99 +++++++++++++++++++ .../SortDiffListContextMenuItemTests.cs | 2 +- 16 files changed, 505 insertions(+), 74 deletions(-) create mode 100644 GitUI/BranchTreePanel/ContextMenu/GitRefsSortByContextMenuItem.cs create mode 100644 GitUI/BranchTreePanel/ContextMenu/GitRefsSortOrderContextMenuItem.cs create mode 100644 UnitTests/GitUI.Tests/BranchTreePanel/ContextMenu/GitRefsSortByContextMenuItemTests.cs create mode 100644 UnitTests/GitUI.Tests/BranchTreePanel/ContextMenu/GitRefsSortOrderContextMenuItemTests.cs diff --git a/GitCommands/IDiffListSortService.cs b/GitCommands/IDiffListSortService.cs index 163ce72032d..02b5ef2085e 100644 --- a/GitCommands/IDiffListSortService.cs +++ b/GitCommands/IDiffListSortService.cs @@ -1,9 +1,6 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; -using System.Text; -using System.Threading.Tasks; namespace GitCommands { diff --git a/GitUI/BranchTreePanel/ContextMenu/GitRefsSortByContextMenuItem.cs b/GitUI/BranchTreePanel/ContextMenu/GitRefsSortByContextMenuItem.cs new file mode 100644 index 00000000000..9a140983ce5 --- /dev/null +++ b/GitUI/BranchTreePanel/ContextMenu/GitRefsSortByContextMenuItem.cs @@ -0,0 +1,73 @@ +using System; +using System.Linq; +using System.Windows.Forms; +using GitCommands; +using GitCommands.Utils; +using GitUI.Properties; +using GitUIPluginInterfaces; + +namespace GitUI.BranchTreePanel.ContextMenu +{ + internal class GitRefsSortByContextMenuItem : ToolStripMenuItem + { + private readonly Action _onSortByChanged; + + public GitRefsSortByContextMenuItem(Action onSortByChanged) + { + _onSortByChanged = onSortByChanged; + + Image = Images.SortBy; + Text = Strings.SortBy; + + foreach (var option in EnumHelper.GetValues().Select(e => (Text: e.GetDescription(), Value: e))) + { + var item = new ToolStripMenuItem() + { + Text = option.Text, + Image = null, + Tag = option.Value + }; + + item.Click += Item_Click; + DropDownItems.Add(item); + } + + DropDownOpening += (s, e) => RequerySortingMethod(); + RequerySortingMethod(); + } + + private void RequerySortingMethod() + { + var currentSort = AppSettings.RefsSortBy; + foreach (ToolStripMenuItem item in DropDownItems) + { + item.Checked = currentSort.Equals(item.Tag); + } + } + + private void Item_Click(object sender, EventArgs e) + { + if (sender is ToolStripMenuItem item) + { + var sortingType = (GitRefsSortBy)item.Tag; + AppSettings.RefsSortBy = sortingType; + + _onSortByChanged?.Invoke(); + } + } + + internal TestAccessor GetTestAccessor() => new TestAccessor(this); + + internal struct TestAccessor + { + private readonly GitRefsSortByContextMenuItem _contextMenuItem; + + public TestAccessor(GitRefsSortByContextMenuItem menuitem) + { + _contextMenuItem = menuitem; + } + + public void RaiseDropDownOpening() => _contextMenuItem.RequerySortingMethod(); + } + } +} diff --git a/GitUI/BranchTreePanel/ContextMenu/GitRefsSortOrderContextMenuItem.cs b/GitUI/BranchTreePanel/ContextMenu/GitRefsSortOrderContextMenuItem.cs new file mode 100644 index 00000000000..3d1a0a15bd2 --- /dev/null +++ b/GitUI/BranchTreePanel/ContextMenu/GitRefsSortOrderContextMenuItem.cs @@ -0,0 +1,75 @@ +using System; +using System.Linq; +using System.Windows.Forms; +using GitCommands; +using GitCommands.Utils; +using GitUI.Properties; +using GitUIPluginInterfaces; + +namespace GitUI.BranchTreePanel.ContextMenu +{ + internal class GitRefsSortOrderContextMenuItem : ToolStripMenuItem + { + internal const string MenuItemName = "GitRefsSortOrderContextMenuItem"; + private readonly Action _onSortOrderChanged; + + public GitRefsSortOrderContextMenuItem(Action onSortOrderChanged) + { + _onSortOrderChanged = onSortOrderChanged; + + Image = Images.SortBy; + Text = Strings.SortOrder; + Name = MenuItemName; + + foreach (var option in EnumHelper.GetValues().Select(e => (Text: e.GetDescription(), Value: e))) + { + var item = new ToolStripMenuItem() + { + Text = option.Text, + Image = null, + Tag = option.Value + }; + + item.Click += Item_Click; + DropDownItems.Add(item); + } + + DropDownOpening += (s, e) => RequerySortingMethod(); + RequerySortingMethod(); + } + + private void RequerySortingMethod() + { + var currentSort = AppSettings.RefsSortOrder; + foreach (ToolStripMenuItem item in DropDownItems) + { + item.Checked = currentSort.Equals(item.Tag); + } + } + + private void Item_Click(object sender, EventArgs e) + { + if (sender is ToolStripMenuItem item) + { + var sortingType = (GitRefsSortOrder)item.Tag; + AppSettings.RefsSortOrder = sortingType; + + _onSortOrderChanged?.Invoke(); + } + } + + internal TestAccessor GetTestAccessor() => new TestAccessor(this); + + internal struct TestAccessor + { + private readonly GitRefsSortOrderContextMenuItem _contextMenuItem; + + public TestAccessor(GitRefsSortOrderContextMenuItem menuitem) + { + _contextMenuItem = menuitem; + } + + public void RaiseDropDownOpening() => _contextMenuItem.RequerySortingMethod(); + } + } +} diff --git a/GitUI/BranchTreePanel/RepoObjectsTree.ContextActions.cs b/GitUI/BranchTreePanel/RepoObjectsTree.ContextActions.cs index 5f96b3ba5bd..cb7a2440c8c 100644 --- a/GitUI/BranchTreePanel/RepoObjectsTree.ContextActions.cs +++ b/GitUI/BranchTreePanel/RepoObjectsTree.ContextActions.cs @@ -4,6 +4,7 @@ using System.Drawing; using System.Linq; using System.Windows.Forms; +using GitCommands; using GitUI.BranchTreePanel.ContextMenu; using GitUI.BranchTreePanel.Interfaces; using ResourceManager; @@ -13,6 +14,8 @@ namespace GitUI.BranchTreePanel partial class RepoObjectsTree : IMenuItemFactory { private TreeNode _lastRightClickedNode; + private GitRefsSortOrderContextMenuItem _sortOrderContextMenuItem; + private GitRefsSortByContextMenuItem _sortByContextMenuItem; /// /// Local branch context menu [git ref / rename / delete] actions @@ -104,7 +107,7 @@ private void ContextMenuBranchSpecific(ContextMenuStrip contextMenu) } var node = (contextMenu.SourceControl as TreeView)?.SelectedNode; - if (node == null) + if (node is null) { return; } @@ -121,7 +124,7 @@ private void ContextMenuRemoteRepoSpecific(ContextMenuStrip contextMenu) } var node = (contextMenu.SourceControl as TreeView)?.SelectedNode?.Tag as RemoteRepoNode; - if (node == null) + if (node is null) { return; } @@ -137,10 +140,38 @@ private void ContextMenuRemoteRepoSpecific(ContextMenuStrip contextMenu) mnubtnEnableRemoteAndFetch.Visible = !node.Enabled; } + private void ContextMenuSort(ContextMenuStrip contextMenu) + { + // We can only sort refs, i.e. branches and tags + if (contextMenu != menuBranch && + contextMenu != menuRemote && + contextMenu != menuTag) + { + return; + } + + // Add the following to the every participating context menu: + // + // --------- + // Sort By... + // Sort Order... + + if (!contextMenu.Items.Contains(_sortOrderContextMenuItem)) + { + AddContextMenuItems(contextMenu, + new ToolStripItem[] { _sortByContextMenuItem, _sortOrderContextMenuItem, new ToolStripSeparator() }, + tsmiMainMenuSpacer1); + } + + // If refs are sorted by git (GitRefsSortBy = Default) don't show sort order options + contextMenu.Items[GitRefsSortOrderContextMenuItem.MenuItemName].Visible = + AppSettings.RefsSortBy != GitUIPluginInterfaces.GitRefsSortBy.Default; + } + private void ContextMenuSubmoduleSpecific(ContextMenuStrip contextMenu) { TreeNode selectedNode = (contextMenu.SourceControl as TreeView)?.SelectedNode; - if (selectedNode == null) + if (selectedNode is null) { return; } @@ -184,6 +215,14 @@ private static void RegisterClick(ToolStripItem item, Action onClick) private void RegisterContextActions() { + _sortOrderContextMenuItem = new GitRefsSortOrderContextMenuItem(() => + { + _branchesTree.RefreshRefs(); + _remotesTree.RefreshRefs(); + _tagTree.RefreshRefs(); + }); + _sortByContextMenuItem = new GitRefsSortByContextMenuItem(() => _branchesTree.RefreshRefs()); + _localBranchMenuItems = new LocalBranchMenuItems(this); AddContextMenuItems(menuBranch, _localBranchMenuItems.Select(s => s.Item)); @@ -247,12 +286,13 @@ private void FilterInRevisionGrid(BaseBranchNode branch) private void contextMenu_Opening(object sender, CancelEventArgs e) { var contextMenu = sender as ContextMenuStrip; - if (contextMenu == null) + if (contextMenu is null) { return; } ContextMenuAddExpandCollapseTree(contextMenu); + ContextMenuSort(contextMenu); ContextMenuBranchSpecific(contextMenu); ContextMenuRemoteRepoSpecific(contextMenu); ContextMenuSubmoduleSpecific(contextMenu); @@ -278,7 +318,7 @@ private void contextMenu_Opening(object sender, CancelEventArgs e) private void AddContextMenuItems(ContextMenuStrip menu, IEnumerable items, ToolStripItem insertAfter = null) { menu.SuspendLayout(); - int index = insertAfter == null ? 0 : Math.Max(0, menu.Items.IndexOf(insertAfter) + 1); + int index = insertAfter is null ? 0 : Math.Max(0, menu.Items.IndexOf(insertAfter) + 1); items.ForEach(item => menu.Items.Insert(index++, item)); menu.ResumeLayout(); } diff --git a/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.Branches.cs b/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.Branches.cs index 586c1e2574d..04230808945 100644 --- a/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.Branches.cs +++ b/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.Branches.cs @@ -8,6 +8,7 @@ using GitCommands.Git; using GitUI.BranchTreePanel.Interfaces; using GitUI.Properties; +using GitUIPluginInterfaces; using JetBrains.Annotations; using Microsoft.VisualStudio.Threading; @@ -118,9 +119,10 @@ private class BaseBranchLeafNode : BaseBranchNode private bool _isMerged = false; - public BaseBranchLeafNode(Tree tree, string fullPath, string imageKeyUnmerged, string imageKeyMerged) + public BaseBranchLeafNode(Tree tree, in ObjectId objectId, string fullPath, string imageKeyUnmerged, string imageKeyMerged) : base(tree, fullPath) { + ObjectId = objectId; _imageKeyUnmerged = imageKeyUnmerged; _imageKeyMerged = imageKeyMerged; } @@ -141,6 +143,9 @@ public bool IsMerged } } + [CanBeNull] + public ObjectId ObjectId { get; } + protected override void ApplyStyle() { base.ApplyStyle(); @@ -150,8 +155,8 @@ protected override void ApplyStyle() private sealed class LocalBranchNode : BaseBranchLeafNode, IGitRefActions, ICanRename, ICanDelete { - public LocalBranchNode(Tree tree, string fullPath, bool isCurrent) - : base(tree, fullPath, nameof(Images.BranchLocal), nameof(Images.BranchLocalMerged)) + public LocalBranchNode(Tree tree, in ObjectId objectId, string fullPath, bool isCurrent) + : base(tree, objectId, fullPath, nameof(Images.BranchLocal), nameof(Images.BranchLocalMerged)) { IsActive = isCurrent; } @@ -264,14 +269,19 @@ public BranchTree(TreeNode treeNode, IGitUICommandsSource uiCommands, [CanBeNull _aheadBehindDataProvider = aheadBehindDataProvider; } - protected override Task OnAttachedAsync() - { - return ReloadNodesAsync(LoadNodesAsync); - } + protected override Task OnAttachedAsync() => ReloadNodesAsync(LoadNodesAsync); + + protected override Task PostRepositoryChangedAsync() => ReloadNodesAsync(LoadNodesAsync); - protected override Task PostRepositoryChangedAsync() + /// + /// Requests to refresh the data tree retaining the current filtering rules. + /// + internal void RefreshRefs() { - return ReloadNodesAsync(LoadNodesAsync); + ThreadHelper.JoinableTaskFactory.RunAsync(async () => + { + await ReloadNodesAsync(LoadNodesAsync); + }); } private async Task LoadNodesAsync(CancellationToken token) @@ -279,11 +289,11 @@ private async Task LoadNodesAsync(CancellationToken token) await TaskScheduler.Default; token.ThrowIfCancellationRequested(); - var branchNames = Module.GetRefs(tags: false, branches: true).Select(b => b.Name); - return FillBranchTree(branchNames, token); + IReadOnlyList branches = Module.GetRefs(tags: false, branches: true); + return FillBranchTree(branches, token); } - private Nodes FillBranchTree(IEnumerable branches, CancellationToken token) + private Nodes FillBranchTree(IReadOnlyList branches, CancellationToken token) { #region ex @@ -323,7 +333,7 @@ private Nodes FillBranchTree(IEnumerable branches, CancellationToken tok foreach (var branch in branches) { token.ThrowIfCancellationRequested(); - var localBranchNode = new LocalBranchNode(this, branch, branch == currentBranch); + var localBranchNode = new LocalBranchNode(this, branch.ObjectId, branch.Name, branch.Name == currentBranch); if (aheadBehindData != null && aheadBehindData.ContainsKey(localBranchNode.FullPath)) { diff --git a/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.Remotes.cs b/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.Remotes.cs index b926df06b76..8abc538df09 100644 --- a/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.Remotes.cs +++ b/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.Remotes.cs @@ -27,14 +27,19 @@ public RemoteBranchTree(TreeNode treeNode, IGitUICommandsSource uiCommands) { } - protected override Task OnAttachedAsync() - { - return ReloadNodesAsync(LoadNodesAsync); - } + protected override Task OnAttachedAsync() => ReloadNodesAsync(LoadNodesAsync); - protected override Task PostRepositoryChangedAsync() + protected override Task PostRepositoryChangedAsync() => ReloadNodesAsync(LoadNodesAsync); + + /// + /// Requests to refresh the data tree retaining the current filtering rules. + /// + internal void RefreshRefs() { - return ReloadNodesAsync(LoadNodesAsync); + ThreadHelper.JoinableTaskFactory.RunAsync(async () => + { + await ReloadNodesAsync(LoadNodesAsync); + }); } private async Task LoadNodesAsync(CancellationToken token) @@ -44,10 +49,8 @@ private async Task LoadNodesAsync(CancellationToken token) var nodes = new Nodes(this); var pathToNodes = new Dictionary(); - var branches = Module.GetRefs(tags: true, branches: true) - .Where(branch => branch.IsRemote && !branch.IsTag) - .OrderBy(branch => branch.Name) - .Select(branch => branch.Name); + IEnumerable branches = Module.GetRefs(tags: true, branches: true) + .Where(branch => branch.IsRemote && !branch.IsTag); token.ThrowIfCancellationRequested(); @@ -57,13 +60,13 @@ private async Task LoadNodesAsync(CancellationToken token) var remotesManager = new ConfigFileRemoteSettingsManager(() => Module); // Create nodes for enabled remotes with branches - foreach (var branchPath in branches) + foreach (IGitRef branch in branches) { token.ThrowIfCancellationRequested(); - var remoteName = branchPath.SubstringUntil('/'); + var remoteName = branch.Name.SubstringUntil('/'); if (remoteByName.TryGetValue(remoteName, out var remote)) { - var remoteBranchNode = new RemoteBranchNode(this, branchPath); + var remoteBranchNode = new RemoteBranchNode(this, branch.ObjectId, branch.Name); var parent = remoteBranchNode.CreateRootNode( pathToNodes, (tree, parentPath) => CreateRemoteBranchPathNode(tree, parentPath, remote)); @@ -157,8 +160,8 @@ internal bool FetchPruneAll() private sealed class RemoteBranchNode : BaseBranchLeafNode, IGitRefActions, ICanDelete, ICanRename { - public RemoteBranchNode(Tree tree, string fullPath) - : base(tree, fullPath, nameof(Images.BranchRemote), nameof(Images.BranchRemoteMerged)) + public RemoteBranchNode(Tree tree, in ObjectId objectId, string fullPath) + : base(tree, objectId, fullPath, nameof(Images.BranchRemote), nameof(Images.BranchRemoteMerged)) { } diff --git a/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.Tags.cs b/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.Tags.cs index 55c3d6214d3..e29fc527d0e 100644 --- a/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.Tags.cs +++ b/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.Tags.cs @@ -6,6 +6,7 @@ using GitUI.CommandsDialogs; using GitUI.Properties; using GitUIPluginInterfaces; +using JetBrains.Annotations; using Microsoft.VisualStudio.Threading; namespace GitUI.BranchTreePanel @@ -14,13 +15,15 @@ partial class RepoObjectsTree { private class TagNode : BaseBranchNode, IGitRefActions, ICanDelete { - private readonly IGitRef _tagInfo; - - public TagNode(Tree tree, string fullPath, IGitRef tagInfo) : base(tree, fullPath) + public TagNode(Tree tree, in ObjectId objectId, string fullPath) + : base(tree, fullPath) { - _tagInfo = tagInfo; + ObjectId = objectId; } + [CanBeNull] + public ObjectId ObjectId { get; } + internal override void OnSelected() { if (Tree.IgnoreSelectionChangedEvent) @@ -39,12 +42,12 @@ internal override void OnDoubleClick() public bool CreateBranch() { - return UICommands.StartCreateBranchDialog(TreeViewNode.TreeView, _tagInfo.ObjectId); + return UICommands.StartCreateBranchDialog(TreeViewNode.TreeView, ObjectId); } public bool Delete() { - return UICommands.StartDeleteTagDialog(TreeViewNode.TreeView, _tagInfo.Name); + return UICommands.StartDeleteTagDialog(TreeViewNode.TreeView, Name); } public bool Merge() @@ -75,14 +78,19 @@ public TagTree(TreeNode treeNode, IGitUICommandsSource uiCommands) { } - protected override Task OnAttachedAsync() - { - return ReloadNodesAsync(LoadNodesAsync); - } + protected override Task OnAttachedAsync() => ReloadNodesAsync(LoadNodesAsync); + + protected override Task PostRepositoryChangedAsync() => ReloadNodesAsync(LoadNodesAsync); - protected override Task PostRepositoryChangedAsync() + /// + /// Requests to refresh the data tree retaining the current filtering rules. + /// + internal void RefreshRefs() { - return ReloadNodesAsync(LoadNodesAsync); + ThreadHelper.JoinableTaskFactory.RunAsync(async () => + { + await ReloadNodesAsync(LoadNodesAsync); + }); } private async Task LoadNodesAsync(CancellationToken token) @@ -92,14 +100,14 @@ private async Task LoadNodesAsync(CancellationToken token) return FillTagTree(Module.GetRefs(tags: true, branches: false), token); } - private Nodes FillTagTree(IEnumerable tags, CancellationToken token) + private Nodes FillTagTree(IReadOnlyList tags, CancellationToken token) { var nodes = new Nodes(this); var pathToNodes = new Dictionary(); - foreach (var tag in tags) + foreach (IGitRef tag in tags) { token.ThrowIfCancellationRequested(); - var branchNode = new TagNode(this, tag.Name, tag); + var branchNode = new TagNode(this, tag.ObjectId, tag.Name); var parent = branchNode.CreateRootNode(pathToNodes, (tree, parentPath) => new BasePathNode(tree, parentPath)); if (parent != null) diff --git a/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.cs b/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.cs index 0bddfb5f02c..cf534962e02 100644 --- a/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.cs +++ b/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.cs @@ -413,7 +413,7 @@ public static Node GetNode(TreeNode treeNode) } [CanBeNull] - private static T GetNodeSafe([CanBeNull] TreeNode treeNode) where T : class, INode + internal static T GetNodeSafe([CanBeNull] TreeNode treeNode) where T : class, INode { return treeNode?.Tag as T; } diff --git a/GitUI/BranchTreePanel/RepoObjectsTree.cs b/GitUI/BranchTreePanel/RepoObjectsTree.cs index 9a98c74ddc0..612d9775545 100644 --- a/GitUI/BranchTreePanel/RepoObjectsTree.cs +++ b/GitUI/BranchTreePanel/RepoObjectsTree.cs @@ -13,6 +13,7 @@ using GitUI.CommandsDialogs; using GitUI.Properties; using GitUI.UserControls; +using GitUIPluginInterfaces; using JetBrains.Annotations; using ResourceManager; @@ -224,7 +225,7 @@ bool IsOverride(MethodInfo m) } } - public void Initialize([CanBeNull]IAheadBehindDataProvider aheadBehindDataProvider, FilterBranchHelper filterBranchHelper) + public void Initialize([CanBeNull] IAheadBehindDataProvider aheadBehindDataProvider, FilterBranchHelper filterBranchHelper) { _aheadBehindDataProvider = aheadBehindDataProvider; _filterBranchHelper = filterBranchHelper; @@ -236,6 +237,13 @@ public void Initialize([CanBeNull]IAheadBehindDataProvider aheadBehindDataProvid public void SelectionChanged(IReadOnlyList selectedRevisions) { + // If we arrived here through the chain of events after selecting a node in the tree, + // and the selected revision is the one we have selected - do nothing. + if (selectedRevisions.Count == 1 && selectedRevisions[0].ObjectId == GetSelectedNodeObjectId(treeMain.SelectedNode)) + { + return; + } + var cancellationToken = _selectionCancellationTokenSequence.Next(); GitRevision selectedRevision = selectedRevisions.FirstOrDefault(); @@ -264,6 +272,13 @@ public void SelectionChanged(IReadOnlyList selectedRevisions) treeMain.EndUpdate(); } }).FileAndForget(); + + static ObjectId GetSelectedNodeObjectId(TreeNode treeNode) + { + // Local or remote branch nodes or tag nodes + return Node.GetNodeSafe(treeNode)?.ObjectId ?? + Node.GetNodeSafe(treeNode)?.ObjectId; + } } protected override void OnUICommandsSourceSet(IGitUICommandsSource source) diff --git a/GitUI/Strings.cs b/GitUI/Strings.cs index 7829a290f7c..27889c17840 100644 --- a/GitUI/Strings.cs +++ b/GitUI/Strings.cs @@ -61,6 +61,9 @@ internal sealed class Strings : Translate private readonly TranslationString _open = new TranslationString("Open"); private readonly TranslationString _directoryIsNotAValidRepository = new TranslationString("The selected item is not a valid git repository."); + private readonly TranslationString _sortBy = new TranslationString("&Sort by..."); + private readonly TranslationString _sortOrder = new TranslationString("&Sort order..."); + private readonly TranslationString _diffSelectedWithRememberedFile = new TranslationString("Diff with \"{0}\""); private readonly TranslationString _showDiffForAllParentsText = new TranslationString("Show file differences for all parents in browse dialog"); private readonly TranslationString _showDiffForAllParentsTooltip = new TranslationString(@"Show all differences between the selected commits, not limiting to only one difference. @@ -151,6 +154,9 @@ public static void Reinitialize() public static string Open => _instance.Value._open.Text; public static string DirectoryInvalidRepository => _instance.Value._directoryIsNotAValidRepository.Text; + public static string SortBy => _instance.Value._sortBy.Text; + public static string SortOrder => _instance.Value._sortOrder.Text; + public static string DiffSelectedWithRememberedFile => _instance.Value._diffSelectedWithRememberedFile.Text; public static string ShowDiffForAllParentsText => _instance.Value._showDiffForAllParentsText.Text; public static string ShowDiffForAllParentsTooltip => _instance.Value._showDiffForAllParentsTooltip.Text; diff --git a/GitUI/Translation/English.xlf b/GitUI/Translation/English.xlf index c93869f6cc1..4ac4b4e0f4b 100644 --- a/GitUI/Translation/English.xlf +++ b/GitUI/Translation/English.xlf @@ -7936,6 +7936,14 @@ To show all branches, right click the revision grid, select 'view' and then the Show/hide branches/remotes/tags + + &Sort by... + + + + &Sort order... + + Search: @@ -9416,6 +9424,14 @@ Select this commit to populate the full message. - For more than four selected commits, show the difference from the first to the last selected commit. + + &Sort by... + + + + &Sort order... + + Submodules diff --git a/GitUI/UserControls/FileStatusList.cs b/GitUI/UserControls/FileStatusList.cs index d72cb8ce588..6233b45f8f3 100644 --- a/GitUI/UserControls/FileStatusList.cs +++ b/GitUI/UserControls/FileStatusList.cs @@ -33,7 +33,6 @@ public sealed partial class FileStatusList : GitModuleControl private readonly TranslationString _diffBaseToB = new TranslationString("Unique diff BASE with b/"); private readonly TranslationString _diffCommonBase = new TranslationString("Common diff with BASE a/"); private readonly TranslationString _combinedDiff = new TranslationString("Combined diff"); - private readonly IGitRevisionTester _revisionTester; private readonly IFullPathResolver _fullPathResolver; private readonly SortDiffListContextMenuItem _sortByContextMenu; private readonly IReadOnlyList _noItemStatuses; @@ -77,7 +76,11 @@ public FileStatusList() InitializeComponent(); InitialiseFiltering(); CreateOpenSubmoduleMenuItem(); - _sortByContextMenu = CreateSortByContextMenuItem(); + _sortByContextMenu = new SortDiffListContextMenuItem(DiffListSortService.Instance) + { + Name = "sortListByContextMenuItem" + }; + SetupUnifiedDiffListSorting(); lblSplitter.Height = DpiUtil.Scale(1); InitializeComplete(); @@ -98,7 +101,6 @@ public FileStatusList() FilterComboBox.Font = new Font(FilterComboBox.Font, FontStyle.Bold); _fullPathResolver = new FullPathResolver(() => Module.WorkingDir); - _revisionTester = new GitRevisionTester(_fullPathResolver); _noItemStatuses = new[] { new GitItemStatus @@ -202,14 +204,6 @@ private void SetupUnifiedDiffListSorting() .Subscribe(); } - private static SortDiffListContextMenuItem CreateSortByContextMenuItem() - { - return new SortDiffListContextMenuItem(DiffListSortService.Instance) - { - Name = "sortListByContextMenuItem" - }; - } - // Properties [Browsable(false)] diff --git a/GitUI/UserControls/SortDiffListContextMenuItem.cs b/GitUI/UserControls/SortDiffListContextMenuItem.cs index 56df4d6eef3..73f15fabb62 100644 --- a/GitUI/UserControls/SortDiffListContextMenuItem.cs +++ b/GitUI/UserControls/SortDiffListContextMenuItem.cs @@ -1,8 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Windows.Forms; using GitCommands; using GitUI.Properties; @@ -12,7 +9,6 @@ namespace GitUI.UserControls { public class SortDiffListContextMenuItem : ToolStripMenuItem { - private readonly TranslationString _sortByText = new TranslationString("&Sort by..."); private readonly TranslationString _filePathSortText = new TranslationString("File &Path"); private readonly TranslationString _fileExtensionSortText = new TranslationString("File &Extension"); private readonly TranslationString _fileStatusSortText = new TranslationString("File &Status"); @@ -26,7 +22,7 @@ public SortDiffListContextMenuItem(IDiffListSortService sortService) { _sortService = sortService ?? throw new ArgumentNullException(nameof(sortService)); Image = Images.SortBy; - Text = _sortByText.Text; + Text = Strings.SortBy; _filePathSortItem = new ToolStripMenuItem() { @@ -64,8 +60,6 @@ public SortDiffListContextMenuItem(IDiffListSortService sortService) RequerySortingMethod(); } - internal TestAccessor GetTestAccessor() => new TestAccessor(this); - private IReadOnlyList AllItems() { return _allItems; @@ -87,7 +81,9 @@ private void Item_Click(object sender, EventArgs e) _sortService.DiffListSorting = sortingType; } - public struct TestAccessor + internal TestAccessor GetTestAccessor() => new TestAccessor(this); + + internal struct TestAccessor { private readonly SortDiffListContextMenuItem _contextMenuItem; @@ -96,7 +92,7 @@ public TestAccessor(SortDiffListContextMenuItem menuitem) _contextMenuItem = menuitem; } - public void SimulateOpeningEvent() => _contextMenuItem.RequerySortingMethod(); + public void RaiseDropDownOpening() => _contextMenuItem.RequerySortingMethod(); } } } diff --git a/UnitTests/GitUI.Tests/BranchTreePanel/ContextMenu/GitRefsSortByContextMenuItemTests.cs b/UnitTests/GitUI.Tests/BranchTreePanel/ContextMenu/GitRefsSortByContextMenuItemTests.cs new file mode 100644 index 00000000000..548ade0f70a --- /dev/null +++ b/UnitTests/GitUI.Tests/BranchTreePanel/ContextMenu/GitRefsSortByContextMenuItemTests.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows.Forms; +using GitCommands; +using GitCommands.Utils; +using GitUI.BranchTreePanel.ContextMenu; +using GitUIPluginInterfaces; +using NSubstitute; +using NUnit.Framework; + +namespace GitUITests.UserControls +{ + [SetCulture("en-US")] + [SetUICulture("en-US")] + [TestFixture] + public class GitRefsSortByContextMenuItemTests + { + private Action _onSortOrderChanged; + private GitRefsSortByContextMenuItem _itemUnderTest; + + [SetUp] + public void Setup() + { + _onSortOrderChanged = Substitute.For(); + _itemUnderTest = new GitRefsSortByContextMenuItem(_onSortOrderChanged); + } + + [Test] + public void Should_show_all_sort_options() + { + Assert.IsTrue(_itemUnderTest.HasDropDownItems); + Assert.AreEqual(EnumHelper.GetValues().Length, _itemUnderTest.DropDownItems.Count); + } + + private static IEnumerable SortOrderOptions + { + get + { + foreach (GitRefsSortBy order in EnumHelper.GetValues()) + { + yield return new TestCaseData(order); + } + } + } + + [TestCaseSource(nameof(SortOrderOptions))] + public void Only_the_current_sort_option_is_selected(GitRefsSortBy order) + { + GitRefsSortBy original = AppSettings.RefsSortBy; + try + { + AppSettings.RefsSortBy = order; + + // invoke the requery method to reselect the proper sub item + _itemUnderTest.GetTestAccessor().RaiseDropDownOpening(); + + AssertOnlyCheckedItemIs(order); + } + finally + { + AppSettings.RefsSortBy = original; + } + } + + [Test] + public void Clicking_an_item_sets_sort_in_service() + { + GitRefsSortBy original = AppSettings.RefsSortBy; + try + { + // Reset to the default + AppSettings.RefsSortBy = GitRefsSortBy.Default; + + foreach (var item in _itemUnderTest.DropDownItems.Cast()) + { + item.PerformClick(); + _onSortOrderChanged.Received(1).Invoke(); + _onSortOrderChanged.ClearReceivedCalls(); + } + } + finally + { + AppSettings.RefsSortBy = original; + } + } + + private void AssertOnlyCheckedItemIs(GitRefsSortBy sortType) + { + var matchingSubItem = _itemUnderTest.DropDownItems.Cast().Single(i => i.Tag.Equals(sortType)); + Assert.IsTrue(matchingSubItem.Checked); + + foreach (var otherItem in _itemUnderTest.DropDownItems.Cast().Except(new[] { matchingSubItem })) + { + Assert.IsFalse(otherItem.Checked); + } + } + } +} diff --git a/UnitTests/GitUI.Tests/BranchTreePanel/ContextMenu/GitRefsSortOrderContextMenuItemTests.cs b/UnitTests/GitUI.Tests/BranchTreePanel/ContextMenu/GitRefsSortOrderContextMenuItemTests.cs new file mode 100644 index 00000000000..4ce49b8014f --- /dev/null +++ b/UnitTests/GitUI.Tests/BranchTreePanel/ContextMenu/GitRefsSortOrderContextMenuItemTests.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows.Forms; +using GitCommands; +using GitCommands.Utils; +using GitUI.BranchTreePanel.ContextMenu; +using GitUIPluginInterfaces; +using NSubstitute; +using NUnit.Framework; + +namespace GitUITests.UserControls +{ + [SetCulture("en-US")] + [SetUICulture("en-US")] + [TestFixture] + public class GitRefsSortOrderContextMenuItemTests + { + private Action _onSortOrderChanged; + private GitRefsSortOrderContextMenuItem _itemUnderTest; + + [SetUp] + public void Setup() + { + _onSortOrderChanged = Substitute.For(); + _itemUnderTest = new GitRefsSortOrderContextMenuItem(_onSortOrderChanged); + } + + [Test] + public void Should_show_all_sort_options() + { + Assert.IsTrue(_itemUnderTest.HasDropDownItems); + Assert.AreEqual(EnumHelper.GetValues().Length, _itemUnderTest.DropDownItems.Count); + } + + private static IEnumerable SortOrderOptions + { + get + { + foreach (GitRefsSortOrder order in EnumHelper.GetValues()) + { + yield return new TestCaseData(order); + } + } + } + + [TestCaseSource(nameof(SortOrderOptions))] + public void Only_the_current_sort_option_is_selected(GitRefsSortOrder order) + { + GitRefsSortOrder original = AppSettings.RefsSortOrder; + try + { + AppSettings.RefsSortOrder = order; + + // invoke the requery method to reselect the proper sub item + _itemUnderTest.GetTestAccessor().RaiseDropDownOpening(); + + AssertOnlyCheckedItemIs(order); + } + finally + { + AppSettings.RefsSortOrder = original; + } + } + + [Test] + public void Clicking_an_item_sets_sort_in_service() + { + GitRefsSortOrder original = AppSettings.RefsSortOrder; + try + { + // Reset to the default + AppSettings.RefsSortOrder = GitRefsSortOrder.Descending; + + foreach (var item in _itemUnderTest.DropDownItems.Cast()) + { + item.PerformClick(); + _onSortOrderChanged.Received(1).Invoke(); + _onSortOrderChanged.ClearReceivedCalls(); + } + } + finally + { + AppSettings.RefsSortOrder = original; + } + } + + private void AssertOnlyCheckedItemIs(GitRefsSortOrder sortType) + { + var matchingSubItem = _itemUnderTest.DropDownItems.Cast().Single(i => i.Tag.Equals(sortType)); + Assert.IsTrue(matchingSubItem.Checked); + + foreach (var otherItem in _itemUnderTest.DropDownItems.Cast().Except(new[] { matchingSubItem })) + { + Assert.IsFalse(otherItem.Checked); + } + } + } +} diff --git a/UnitTests/GitUI.Tests/UserControls/SortDiffListContextMenuItemTests.cs b/UnitTests/GitUI.Tests/UserControls/SortDiffListContextMenuItemTests.cs index 0b11284b53a..7a4c997ac86 100644 --- a/UnitTests/GitUI.Tests/UserControls/SortDiffListContextMenuItemTests.cs +++ b/UnitTests/GitUI.Tests/UserControls/SortDiffListContextMenuItemTests.cs @@ -50,7 +50,7 @@ public void Only_the_current_sort_option_is_selected(DiffListSortType sortType) _testingSortService.DiffListSorting.Returns(sortType); // invoke the requery method to reselect the proper sub item - _itemUnderTest.GetTestAccessor().SimulateOpeningEvent(); + _itemUnderTest.GetTestAccessor().RaiseDropDownOpening(); AssertOnlyCheckedItemIs(sortType); }