namespace Spectre.Console; /// /// Represents a multi selection list prompt. /// /// The prompt result type. public sealed class MultiSelectionPrompt : IPrompt>, IListPromptStrategy where T : notnull { /// /// Gets or sets the title. /// public string? Title { get; set; } /// /// Gets or sets the page size. /// Defaults to 10. /// public int PageSize { get; set; } = 10; /// /// Gets or sets a value indicating whether the selection should wrap around when reaching the edge. /// Defaults to false. /// public bool WrapAround { get; set; } = false; /// /// Gets or sets the highlight style of the selected choice. /// public Style? HighlightStyle { get; set; } /// /// Gets or sets the converter to get the display string for a choice. By default /// the corresponding is used. /// public Func? Converter { get; set; } /// /// Gets or sets a value indicating whether or not /// at least one selection is required. /// public bool Required { get; set; } = true; /// /// Gets or sets the text that will be displayed if there are more choices to show. /// public string? MoreChoicesText { get; set; } /// /// Gets or sets the text that instructs the user of how to select items. /// public string? InstructionsText { get; set; } /// /// Gets or sets the selection mode. /// Defaults to . /// public SelectionMode Mode { get; set; } = SelectionMode.Leaf; internal ListPromptTree Tree { get; } /// /// Initializes a new instance of the class. /// /// /// 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(comparer ?? EqualityComparer.Default); } /// /// Adds a choice. /// /// The item to add. /// A so that multiple calls can be chained. public IMultiSelectionItem AddChoice(T item) { var node = new ListPromptItem(item); Tree.Add(node); return node; } /// public List Show(IAnsiConsole console) { return ShowAsync(console, CancellationToken.None).GetAwaiter().GetResult(); } /// public async Task> ShowAsync(IAnsiConsole console, CancellationToken cancellationToken) { // Create the list prompt var prompt = new ListPrompt(console, this); var result = await prompt.Show(Tree, cancellationToken, PageSize, WrapAround).ConfigureAwait(false); if (Mode == SelectionMode.Leaf) { return result.Items .Where(x => x.IsSelected && x.Children.Count == 0) .Select(x => x.Data) .ToList(); } return result.Items .Where(x => x.IsSelected) .Select(x => x.Data) .ToList(); } /// /// Returns all parent items of the given . /// /// The item for which to find the parents. /// The parent items, or an empty list, if the given item has no parents. public IEnumerable GetParents(T item) { var promptItem = Tree.Find(item); if (promptItem == null) { throw new ArgumentOutOfRangeException(nameof(item), "Item not found in tree."); } var parents = new List>(); while (promptItem.Parent != null) { promptItem = promptItem.Parent; parents.Add(promptItem); } return parents .ReverseEnumerable() .Select(x => x.Data); } /// /// Returns the parent item of the given . /// /// The item for which to find the parent. /// The parent item, or null if the given item has no parent. public T? GetParent(T item) { return GetParents(item).LastOrDefault(); } /// ListPromptInputResult IListPromptStrategy.HandleInput(ConsoleKeyInfo key, ListPromptState state) { if (key.Key == ConsoleKey.Enter) { if (Required && state.Items.None(x => x.IsSelected)) { // Selection not permitted return ListPromptInputResult.None; } // Submit return ListPromptInputResult.Submit; } if (key.Key == ConsoleKey.Spacebar || key.Key == ConsoleKey.Packet) { var current = state.Items[state.Index]; var select = !current.IsSelected; if (Mode == SelectionMode.Leaf) { // Select the node and all its children foreach (var item in current.Traverse(includeSelf: true)) { item.IsSelected = select; } // Visit every parent and evaluate if its selection // status need to be updated var parent = current.Parent; while (parent != null) { parent.IsSelected = parent.Traverse(includeSelf: false).All(x => x.IsSelected); parent = parent.Parent; } } else { current.IsSelected = !current.IsSelected; } // Refresh the list return ListPromptInputResult.Refresh; } return ListPromptInputResult.None; } /// int IListPromptStrategy.CalculatePageSize(IAnsiConsole console, int totalItemCount, int requestedPageSize) { // The instructions take up two rows including a blank line var extra = 2; if (Title != null) { // Title takes up two rows including a blank line extra += 2; } // Scrolling? if (totalItemCount > requestedPageSize) { // The scrolling instructions takes up one row extra++; } var pageSize = requestedPageSize; if (pageSize > console.Profile.Height - extra) { pageSize = console.Profile.Height - extra; } return pageSize; } /// IRenderable IListPromptStrategy.Render(IAnsiConsole console, bool scrollable, int cursorIndex, IEnumerable<(int Index, ListPromptItem Node)> items) { var list = new List(); var highlightStyle = HighlightStyle ?? Color.Blue; if (Title != null) { list.Add(new Markup(Title)); } var grid = new Grid(); grid.AddColumn(new GridColumn().Padding(0, 0, 1, 0).NoWrap()); if (Title != null) { grid.AddEmptyRow(); } foreach (var item in items) { var current = item.Index == cursorIndex; var style = current ? highlightStyle : Style.Plain; var indent = new string(' ', item.Node.Depth * 2); var prompt = item.Index == cursorIndex ? ListPromptConstants.Arrow : new string(' ', ListPromptConstants.Arrow.Length); var text = (Converter ?? TypeConverterHelper.ConvertToString)?.Invoke(item.Node.Data) ?? item.Node.Data.ToString() ?? "?"; if (current) { text = text.RemoveMarkup().EscapeMarkup(); } var checkbox = item.Node.IsSelected ? (item.Node.IsGroup && Mode == SelectionMode.Leaf ? ListPromptConstants.GroupSelectedCheckbox : ListPromptConstants.SelectedCheckbox) : ListPromptConstants.Checkbox; grid.AddRow(new Markup(indent + prompt + " " + checkbox + " " + text, style)); } list.Add(grid); list.Add(Text.Empty); if (scrollable) { // There are more choices list.Add(new Markup(MoreChoicesText ?? ListPromptConstants.MoreChoicesMarkup)); } // Instructions list.Add(new Markup(InstructionsText ?? ListPromptConstants.InstructionsMarkup)); // Combine all items return new Rows(list); } }