diff --git a/src/Spectre.Console/Extensions/StackExtensions.cs b/src/Spectre.Console/Extensions/StackExtensions.cs new file mode 100644 index 0000000..6385db4 --- /dev/null +++ b/src/Spectre.Console/Extensions/StackExtensions.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; + +namespace Spectre.Console +{ + internal static class StackExtensions + { + public static void PushRange(this Stack stack, IEnumerable source) + { + if (stack is null) + { + throw new ArgumentNullException(nameof(stack)); + } + + if (source != null) + { + foreach (var item in source) + { + stack.Push(item); + } + } + } + } +} diff --git a/src/Spectre.Console/Widgets/Prompt/IMultiSelectionItem.cs b/src/Spectre.Console/Widgets/Prompt/IMultiSelectionItem.cs index c587adc..83694f2 100644 --- a/src/Spectre.Console/Widgets/Prompt/IMultiSelectionItem.cs +++ b/src/Spectre.Console/Widgets/Prompt/IMultiSelectionItem.cs @@ -7,6 +7,11 @@ namespace Spectre.Console public interface IMultiSelectionItem : ISelectionItem where T : notnull { + /// + /// Gets a value indicating whether or not this item is selected. + /// + bool IsSelected { get; } + /// /// Selects the item. /// diff --git a/src/Spectre.Console/Widgets/Prompt/List/ListPromptItem.cs b/src/Spectre.Console/Widgets/Prompt/List/ListPromptItem.cs index c7e321b..8417c8c 100644 --- a/src/Spectre.Console/Widgets/Prompt/List/ListPromptItem.cs +++ b/src/Spectre.Console/Widgets/Prompt/List/ListPromptItem.cs @@ -9,7 +9,7 @@ namespace Spectre.Console public ListPromptItem? Parent { get; } public List> Children { get; } public int Depth { get; } - public bool Selected { get; set; } + public bool IsSelected { get; set; } public bool IsGroup => Children.Count > 0; @@ -23,7 +23,7 @@ namespace Spectre.Console public IMultiSelectionItem Select() { - Selected = true; + IsSelected = true; return this; } diff --git a/src/Spectre.Console/Widgets/Prompt/List/ListPromptTree.cs b/src/Spectre.Console/Widgets/Prompt/List/ListPromptTree.cs index 82ef5ca..85d97bd 100644 --- a/src/Spectre.Console/Widgets/Prompt/List/ListPromptTree.cs +++ b/src/Spectre.Console/Widgets/Prompt/List/ListPromptTree.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; namespace Spectre.Console @@ -6,10 +7,29 @@ namespace Spectre.Console where T : notnull { private readonly List> _roots; + private readonly IEqualityComparer _comparer; - public ListPromptTree() + public ListPromptTree(IEqualityComparer comparer) { _roots = new List>(); + _comparer = comparer ?? throw new ArgumentNullException(nameof(comparer)); + } + + public ListPromptItem? Find(T item) + { + var stack = new Stack>(_roots); + while (stack.Count > 0) + { + var current = stack.Pop(); + if (_comparer.Equals(item, current.Data)) + { + return current; + } + + stack.PushRange(current.Children); + } + + return null; } public void Add(ListPromptItem node) diff --git a/src/Spectre.Console/Widgets/Prompt/MultiSelectionPrompt.cs b/src/Spectre.Console/Widgets/Prompt/MultiSelectionPrompt.cs index 12e4f23..1d5ba91 100644 --- a/src/Spectre.Console/Widgets/Prompt/MultiSelectionPrompt.cs +++ b/src/Spectre.Console/Widgets/Prompt/MultiSelectionPrompt.cs @@ -13,8 +13,6 @@ namespace Spectre.Console public sealed class MultiSelectionPrompt : IPrompt>, IListPromptStrategy where T : notnull { - private readonly ListPromptTree _tree; - /// /// Gets or sets the title. /// @@ -59,12 +57,18 @@ namespace Spectre.Console /// public SelectionMode Mode { get; set; } = SelectionMode.Leaf; + internal ListPromptTree Tree { get; } + /// /// Initializes a new instance of the class. /// - public MultiSelectionPrompt() + /// + /// The implementation to use when comparing items, + /// or null to use the default for the type of the item. + /// + public MultiSelectionPrompt(IEqualityComparer? comparer = null) { - _tree = new ListPromptTree(); + Tree = new ListPromptTree(comparer ?? EqualityComparer.Default); } /// @@ -75,7 +79,7 @@ namespace Spectre.Console public IMultiSelectionItem AddChoice(T item) { var node = new ListPromptItem(item); - _tree.Add(node); + Tree.Add(node); return node; } @@ -84,18 +88,18 @@ namespace Spectre.Console { // Create the list prompt var prompt = new ListPrompt(console, this); - var result = prompt.Show(_tree, PageSize); + var result = prompt.Show(Tree, PageSize); if (Mode == SelectionMode.Leaf) { return result.Items - .Where(x => x.Selected && x.Children.Count == 0) + .Where(x => x.IsSelected && x.Children.Count == 0) .Select(x => x.Data) .ToList(); } return result.Items - .Where(x => x.Selected) + .Where(x => x.IsSelected) .Select(x => x.Data) .ToList(); } @@ -105,7 +109,7 @@ namespace Spectre.Console { if (key.Key == ConsoleKey.Enter) { - if (Required && state.Items.None(x => x.Selected)) + if (Required && state.Items.None(x => x.IsSelected)) { // Selection not permitted return ListPromptInputResult.None; @@ -118,14 +122,14 @@ namespace Spectre.Console if (key.Key == ConsoleKey.Spacebar) { var current = state.Items[state.Index]; - var select = !current.Selected; + var select = !current.IsSelected; if (Mode == SelectionMode.Leaf) { // Select the node and all it's children foreach (var item in current.Traverse(includeSelf: true)) { - item.Selected = select; + item.IsSelected = select; } // Visit every parent and evaluate if it's selection @@ -133,13 +137,13 @@ namespace Spectre.Console var parent = current.Parent; while (parent != null) { - parent.Selected = parent.Traverse(includeSelf: false).All(x => x.Selected); + parent.IsSelected = parent.Traverse(includeSelf: false).All(x => x.IsSelected); parent = parent.Parent; } } else { - current.Selected = !current.Selected; + current.IsSelected = !current.IsSelected; } // Refresh the list @@ -209,7 +213,7 @@ namespace Spectre.Console text = text.RemoveMarkup(); } - var checkbox = item.Node.Selected + var checkbox = item.Node.IsSelected ? (item.Node.IsGroup && Mode == SelectionMode.Leaf ? ListPromptConstants.GroupSelectedCheckbox : ListPromptConstants.SelectedCheckbox) : ListPromptConstants.Checkbox; diff --git a/src/Spectre.Console/Widgets/Prompt/MultiSelectionPromptExtensions.cs b/src/Spectre.Console/Widgets/Prompt/MultiSelectionPromptExtensions.cs index 4575ba6..d1a3bf4 100644 --- a/src/Spectre.Console/Widgets/Prompt/MultiSelectionPromptExtensions.cs +++ b/src/Spectre.Console/Widgets/Prompt/MultiSelectionPromptExtensions.cs @@ -130,40 +130,19 @@ namespace Spectre.Console /// /// The prompt result type. /// The prompt. - /// The index of the item to select. + /// The item to select. /// The same instance so that multiple calls can be chained. - [Obsolete("Selection by index has been made obsolete", error: true)] - public static MultiSelectionPrompt Select(this MultiSelectionPrompt obj, int index) + public static MultiSelectionPrompt Select(this MultiSelectionPrompt obj, T item) where T : notnull { - return obj; - } + if (obj is null) + { + throw new ArgumentNullException(nameof(obj)); + } - /// - /// Marks multiple items as selected. - /// - /// The prompt result type. - /// The prompt. - /// The indices of the items to select. - /// The same instance so that multiple calls can be chained. - [Obsolete("Selection by index has been made obsolete", error: true)] - public static MultiSelectionPrompt Select(this MultiSelectionPrompt obj, params int[] indices) - where T : notnull - { - return obj; - } + var node = obj.Tree.Find(item); + node?.Select(); - /// - /// Marks multiple items as selected. - /// - /// The prompt result type. - /// The prompt. - /// The indices of the items to select. - /// The same instance so that multiple calls can be chained. - [Obsolete("Selection by index has been made obsolete", error: true)] - public static MultiSelectionPrompt Select(this MultiSelectionPrompt obj, IEnumerable indices) - where T : notnull - { return obj; } diff --git a/src/Spectre.Console/Widgets/Prompt/SelectionPrompt.cs b/src/Spectre.Console/Widgets/Prompt/SelectionPrompt.cs index d8b3890..24b71f4 100644 --- a/src/Spectre.Console/Widgets/Prompt/SelectionPrompt.cs +++ b/src/Spectre.Console/Widgets/Prompt/SelectionPrompt.cs @@ -57,7 +57,7 @@ namespace Spectre.Console /// public SelectionPrompt() { - _tree = new ListPromptTree(); + _tree = new ListPromptTree(EqualityComparer.Default); } /// diff --git a/test/Spectre.Console.Tests/Unit/Prompt/MultiSelectionPromptTests.cs b/test/Spectre.Console.Tests/Unit/Prompt/MultiSelectionPromptTests.cs new file mode 100644 index 0000000..b9bdcb5 --- /dev/null +++ b/test/Spectre.Console.Tests/Unit/Prompt/MultiSelectionPromptTests.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Shouldly; +using Xunit; + +namespace Spectre.Console.Tests.Unit +{ + public sealed class MultiSelectionPromptTests + { + private class CustomItem + { + public int X { get; set; } + public int Y { get; set; } + + public class Comparer : IEqualityComparer + { + public bool Equals(CustomItem x, CustomItem y) + { + return x.X == y.X && x.Y == y.Y; + } + + public int GetHashCode([DisallowNull] CustomItem obj) + { + throw new NotImplementedException(); + } + } + } + + [Fact] + public void Should_Not_Mark_Item_As_Selected_By_Default() + { + // Given + var prompt = new MultiSelectionPrompt(); + + // When + var choice = prompt.AddChoice(32); + + // Then + choice.IsSelected.ShouldBeFalse(); + } + + [Fact] + public void Should_Mark_Item_As_Selected() + { + // Given + var prompt = new MultiSelectionPrompt(); + var choice = prompt.AddChoice(32); + + // When + prompt.Select(32); + + // Then + choice.IsSelected.ShouldBeTrue(); + } + + [Fact] + public void Should_Mark_Custom_Item_As_Selected_If_The_Same_Reference_Is_Used() + { + // Given + var prompt = new MultiSelectionPrompt(); + var item = new CustomItem { X = 18, Y = 32 }; + var choice = prompt.AddChoice(item); + + // When + prompt.Select(item); + + // Then + choice.IsSelected.ShouldBeTrue(); + } + + [Fact] + public void Should_Mark_Custom_Item_As_Selected_If_A_Comparer_Is_Provided() + { + // Given + var prompt = new MultiSelectionPrompt(new CustomItem.Comparer()); + var choice = prompt.AddChoice(new CustomItem { X = 18, Y = 32 }); + + // When + prompt.Select(new CustomItem { X = 18, Y = 32 }); + + // Then + choice.IsSelected.ShouldBeTrue(); + } + } +}