Add support for selecting prompt items

Closes #447
This commit is contained in:
Patrik Svensson 2021-06-27 21:59:58 +02:00 committed by Phil Scott
parent fa553fd72e
commit 865552c3f2
8 changed files with 165 additions and 47 deletions

View File

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
namespace Spectre.Console
{
internal static class StackExtensions
{
public static void PushRange<T>(this Stack<T> stack, IEnumerable<T> source)
{
if (stack is null)
{
throw new ArgumentNullException(nameof(stack));
}
if (source != null)
{
foreach (var item in source)
{
stack.Push(item);
}
}
}
}
}

View File

@ -7,6 +7,11 @@ namespace Spectre.Console
public interface IMultiSelectionItem<T> : ISelectionItem<T> public interface IMultiSelectionItem<T> : ISelectionItem<T>
where T : notnull where T : notnull
{ {
/// <summary>
/// Gets a value indicating whether or not this item is selected.
/// </summary>
bool IsSelected { get; }
/// <summary> /// <summary>
/// Selects the item. /// Selects the item.
/// </summary> /// </summary>

View File

@ -9,7 +9,7 @@ namespace Spectre.Console
public ListPromptItem<T>? Parent { get; } public ListPromptItem<T>? Parent { get; }
public List<ListPromptItem<T>> Children { get; } public List<ListPromptItem<T>> Children { get; }
public int Depth { get; } public int Depth { get; }
public bool Selected { get; set; } public bool IsSelected { get; set; }
public bool IsGroup => Children.Count > 0; public bool IsGroup => Children.Count > 0;
@ -23,7 +23,7 @@ namespace Spectre.Console
public IMultiSelectionItem<T> Select() public IMultiSelectionItem<T> Select()
{ {
Selected = true; IsSelected = true;
return this; return this;
} }

View File

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace Spectre.Console namespace Spectre.Console
@ -6,10 +7,29 @@ namespace Spectre.Console
where T : notnull where T : notnull
{ {
private readonly List<ListPromptItem<T>> _roots; private readonly List<ListPromptItem<T>> _roots;
private readonly IEqualityComparer<T> _comparer;
public ListPromptTree() public ListPromptTree(IEqualityComparer<T> comparer)
{ {
_roots = new List<ListPromptItem<T>>(); _roots = new List<ListPromptItem<T>>();
_comparer = comparer ?? throw new ArgumentNullException(nameof(comparer));
}
public ListPromptItem<T>? Find(T item)
{
var stack = new Stack<ListPromptItem<T>>(_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<T> node) public void Add(ListPromptItem<T> node)

View File

@ -13,8 +13,6 @@ namespace Spectre.Console
public sealed class MultiSelectionPrompt<T> : IPrompt<List<T>>, IListPromptStrategy<T> public sealed class MultiSelectionPrompt<T> : IPrompt<List<T>>, IListPromptStrategy<T>
where T : notnull where T : notnull
{ {
private readonly ListPromptTree<T> _tree;
/// <summary> /// <summary>
/// Gets or sets the title. /// Gets or sets the title.
/// </summary> /// </summary>
@ -59,12 +57,18 @@ namespace Spectre.Console
/// </summary> /// </summary>
public SelectionMode Mode { get; set; } = SelectionMode.Leaf; public SelectionMode Mode { get; set; } = SelectionMode.Leaf;
internal ListPromptTree<T> Tree { get; }
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="MultiSelectionPrompt{T}"/> class. /// Initializes a new instance of the <see cref="MultiSelectionPrompt{T}"/> class.
/// </summary> /// </summary>
public MultiSelectionPrompt() /// <param name="comparer">
/// The <see cref="IEqualityComparer{T}"/> implementation to use when comparing items,
/// or <c>null</c> to use the default <see cref="IEqualityComparer{T}"/> for the type of the item.
/// </param>
public MultiSelectionPrompt(IEqualityComparer<T>? comparer = null)
{ {
_tree = new ListPromptTree<T>(); Tree = new ListPromptTree<T>(comparer ?? EqualityComparer<T>.Default);
} }
/// <summary> /// <summary>
@ -75,7 +79,7 @@ namespace Spectre.Console
public IMultiSelectionItem<T> AddChoice(T item) public IMultiSelectionItem<T> AddChoice(T item)
{ {
var node = new ListPromptItem<T>(item); var node = new ListPromptItem<T>(item);
_tree.Add(node); Tree.Add(node);
return node; return node;
} }
@ -84,18 +88,18 @@ namespace Spectre.Console
{ {
// Create the list prompt // Create the list prompt
var prompt = new ListPrompt<T>(console, this); var prompt = new ListPrompt<T>(console, this);
var result = prompt.Show(_tree, PageSize); var result = prompt.Show(Tree, PageSize);
if (Mode == SelectionMode.Leaf) if (Mode == SelectionMode.Leaf)
{ {
return result.Items return result.Items
.Where(x => x.Selected && x.Children.Count == 0) .Where(x => x.IsSelected && x.Children.Count == 0)
.Select(x => x.Data) .Select(x => x.Data)
.ToList(); .ToList();
} }
return result.Items return result.Items
.Where(x => x.Selected) .Where(x => x.IsSelected)
.Select(x => x.Data) .Select(x => x.Data)
.ToList(); .ToList();
} }
@ -105,7 +109,7 @@ namespace Spectre.Console
{ {
if (key.Key == ConsoleKey.Enter) if (key.Key == ConsoleKey.Enter)
{ {
if (Required && state.Items.None(x => x.Selected)) if (Required && state.Items.None(x => x.IsSelected))
{ {
// Selection not permitted // Selection not permitted
return ListPromptInputResult.None; return ListPromptInputResult.None;
@ -118,14 +122,14 @@ namespace Spectre.Console
if (key.Key == ConsoleKey.Spacebar) if (key.Key == ConsoleKey.Spacebar)
{ {
var current = state.Items[state.Index]; var current = state.Items[state.Index];
var select = !current.Selected; var select = !current.IsSelected;
if (Mode == SelectionMode.Leaf) if (Mode == SelectionMode.Leaf)
{ {
// Select the node and all it's children // Select the node and all it's children
foreach (var item in current.Traverse(includeSelf: true)) foreach (var item in current.Traverse(includeSelf: true))
{ {
item.Selected = select; item.IsSelected = select;
} }
// Visit every parent and evaluate if it's selection // Visit every parent and evaluate if it's selection
@ -133,13 +137,13 @@ namespace Spectre.Console
var parent = current.Parent; var parent = current.Parent;
while (parent != null) 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; parent = parent.Parent;
} }
} }
else else
{ {
current.Selected = !current.Selected; current.IsSelected = !current.IsSelected;
} }
// Refresh the list // Refresh the list
@ -209,7 +213,7 @@ namespace Spectre.Console
text = text.RemoveMarkup(); text = text.RemoveMarkup();
} }
var checkbox = item.Node.Selected var checkbox = item.Node.IsSelected
? (item.Node.IsGroup && Mode == SelectionMode.Leaf ? (item.Node.IsGroup && Mode == SelectionMode.Leaf
? ListPromptConstants.GroupSelectedCheckbox : ListPromptConstants.SelectedCheckbox) ? ListPromptConstants.GroupSelectedCheckbox : ListPromptConstants.SelectedCheckbox)
: ListPromptConstants.Checkbox; : ListPromptConstants.Checkbox;

View File

@ -130,40 +130,19 @@ namespace Spectre.Console
/// </summary> /// </summary>
/// <typeparam name="T">The prompt result type.</typeparam> /// <typeparam name="T">The prompt result type.</typeparam>
/// <param name="obj">The prompt.</param> /// <param name="obj">The prompt.</param>
/// <param name="index">The index of the item to select.</param> /// <param name="item">The item to select.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns> /// <returns>The same instance so that multiple calls can be chained.</returns>
[Obsolete("Selection by index has been made obsolete", error: true)] public static MultiSelectionPrompt<T> Select<T>(this MultiSelectionPrompt<T> obj, T item)
public static MultiSelectionPrompt<T> Select<T>(this MultiSelectionPrompt<T> obj, int index)
where T : notnull where T : notnull
{ {
return obj; if (obj is null)
{
throw new ArgumentNullException(nameof(obj));
} }
/// <summary> var node = obj.Tree.Find(item);
/// Marks multiple items as selected. node?.Select();
/// </summary>
/// <typeparam name="T">The prompt result type.</typeparam>
/// <param name="obj">The prompt.</param>
/// <param name="indices">The indices of the items to select.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
[Obsolete("Selection by index has been made obsolete", error: true)]
public static MultiSelectionPrompt<T> Select<T>(this MultiSelectionPrompt<T> obj, params int[] indices)
where T : notnull
{
return obj;
}
/// <summary>
/// Marks multiple items as selected.
/// </summary>
/// <typeparam name="T">The prompt result type.</typeparam>
/// <param name="obj">The prompt.</param>
/// <param name="indices">The indices of the items to select.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
[Obsolete("Selection by index has been made obsolete", error: true)]
public static MultiSelectionPrompt<T> Select<T>(this MultiSelectionPrompt<T> obj, IEnumerable<int> indices)
where T : notnull
{
return obj; return obj;
} }

View File

@ -57,7 +57,7 @@ namespace Spectre.Console
/// </summary> /// </summary>
public SelectionPrompt() public SelectionPrompt()
{ {
_tree = new ListPromptTree<T>(); _tree = new ListPromptTree<T>(EqualityComparer<T>.Default);
} }
/// <summary> /// <summary>

View File

@ -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<CustomItem>
{
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<int>();
// When
var choice = prompt.AddChoice(32);
// Then
choice.IsSelected.ShouldBeFalse();
}
[Fact]
public void Should_Mark_Item_As_Selected()
{
// Given
var prompt = new MultiSelectionPrompt<int>();
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<CustomItem>();
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<CustomItem>(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();
}
}
}