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>
where T : notnull
{
/// <summary>
/// Gets a value indicating whether or not this item is selected.
/// </summary>
bool IsSelected { get; }
/// <summary>
/// Selects the item.
/// </summary>

View File

@ -9,7 +9,7 @@ namespace Spectre.Console
public ListPromptItem<T>? Parent { get; }
public List<ListPromptItem<T>> 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<T> Select()
{
Selected = true;
IsSelected = true;
return this;
}

View File

@ -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<ListPromptItem<T>> _roots;
private readonly IEqualityComparer<T> _comparer;
public ListPromptTree()
public ListPromptTree(IEqualityComparer<T> comparer)
{
_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)

View File

@ -13,8 +13,6 @@ namespace Spectre.Console
public sealed class MultiSelectionPrompt<T> : IPrompt<List<T>>, IListPromptStrategy<T>
where T : notnull
{
private readonly ListPromptTree<T> _tree;
/// <summary>
/// Gets or sets the title.
/// </summary>
@ -59,12 +57,18 @@ namespace Spectre.Console
/// </summary>
public SelectionMode Mode { get; set; } = SelectionMode.Leaf;
internal ListPromptTree<T> Tree { get; }
/// <summary>
/// Initializes a new instance of the <see cref="MultiSelectionPrompt{T}"/> class.
/// </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>
@ -75,7 +79,7 @@ namespace Spectre.Console
public IMultiSelectionItem<T> AddChoice(T item)
{
var node = new ListPromptItem<T>(item);
_tree.Add(node);
Tree.Add(node);
return node;
}
@ -84,18 +88,18 @@ namespace Spectre.Console
{
// Create the list prompt
var prompt = new ListPrompt<T>(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;

View File

@ -130,40 +130,19 @@ namespace Spectre.Console
/// </summary>
/// <typeparam name="T">The prompt result type.</typeparam>
/// <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>
[Obsolete("Selection by index has been made obsolete", error: true)]
public static MultiSelectionPrompt<T> Select<T>(this MultiSelectionPrompt<T> obj, int index)
public static MultiSelectionPrompt<T> Select<T>(this MultiSelectionPrompt<T> obj, T item)
where T : notnull
{
return obj;
}
if (obj is null)
{
throw new ArgumentNullException(nameof(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, params int[] indices)
where T : notnull
{
return obj;
}
var node = obj.Tree.Find(item);
node?.Select();
/// <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;
}

View File

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