using System.Collections;
using static Terminal.Gui.SpinnerStyle;
namespace Terminal.Gui;
/// Implement to provide custom rendering for a .
public interface IListDataSource
{
/// Returns the number of elements to display
int Count { get; }
/// Returns the maximum length of elements to display
int Length { get; }
/// Should return whether the specified item is currently marked.
/// , if marked, otherwise.
/// Item index.
bool IsMarked (int item);
/// This method is invoked to render a specified item, the method should cover the entire provided width.
/// The render.
/// The list view to render.
/// The console driver to render.
/// Describes whether the item being rendered is currently selected by the user.
/// The index of the item to render, zero for the first item and so on.
/// The column where the rendering will start
/// The line where the rendering will be done.
/// The width that must be filled out.
/// The index of the string to be displayed.
///
/// The default color will be set before this method is invoked, and will be based on whether the item is selected
/// or not.
///
void Render (
ListView container,
ConsoleDriver driver,
bool selected,
int item,
int col,
int line,
int width,
int start = 0
);
/// Flags the item as marked.
/// Item index.
/// If set to value.
void SetMark (int item, bool value);
/// Return the source as IList.
///
IList ToList ();
}
///
/// ListView renders a scrollable list of data where each item can be activated to perform an
/// action.
///
///
///
/// The displays lists of data and allows the user to scroll through the data. Items in
/// the can be activated firing an event (with the ENTER key or a mouse double-click). If the
/// property is true, elements of the list can be marked by the user.
///
///
/// By default uses to render the items of any
/// object (e.g. arrays, , and other collections). Alternatively, an
/// object that implements can be provided giving full control of what is rendered.
///
///
/// can display any object that implements the interface.
/// values are converted into values before rendering, and other values
/// are converted into by calling and then converting to
/// .
///
///
/// To change the contents of the ListView, set the property (when providing custom
/// rendering via ) or call an is being
/// used.
///
///
/// When is set to true the rendering will prefix the rendered items with [x] or [ ]
/// and bind the SPACE key to toggle the selection. To implement a different marking style set
/// to false and implement custom rendering.
///
///
/// Searching the ListView with the keyboard is supported. Users type the first characters of an item, and the
/// first item that starts with what the user types will be selected.
///
///
public class ListView : View
{
private bool _allowsMarking;
private bool _allowsMultipleSelection = true;
private int _lastSelectedItem = -1;
private int _selected = -1;
private IListDataSource _source;
// TODO: ListView has been upgraded to use Viewport and ContentSize instead of the
// TODO: bespoke _top and _left. It was a quick & dirty port. There is now duplicate logic
// TODO: that could be removed.
//private int _top, _left;
///
/// Initializes a new instance of . Set the property to display
/// something.
///
public ListView ()
{
CanFocus = true;
// Things this view knows how to do
AddCommand (Command.LineUp, () => MoveUp ());
AddCommand (Command.LineDown, () => MoveDown ());
AddCommand (Command.ScrollUp, () => ScrollVertical (-1));
AddCommand (Command.ScrollDown, () => ScrollVertical (1));
AddCommand (Command.PageUp, () => MovePageUp ());
AddCommand (Command.PageDown, () => MovePageDown ());
AddCommand (Command.TopHome, () => MoveHome ());
AddCommand (Command.BottomEnd, () => MoveEnd ());
AddCommand (Command.Accept, () => OnOpenSelectedItem ());
AddCommand (Command.OpenSelectedItem, () => OnOpenSelectedItem ());
AddCommand (Command.Select, () => MarkUnmarkRow ());
AddCommand (Command.ScrollLeft, () => ScrollHorizontal (-1));
AddCommand (Command.ScrollRight, () => ScrollHorizontal (1));
// Default keybindings for all ListViews
KeyBindings.Add (Key.CursorUp, Command.LineUp);
KeyBindings.Add (Key.P.WithCtrl, Command.LineUp);
KeyBindings.Add (Key.CursorDown, Command.LineDown);
KeyBindings.Add (Key.N.WithCtrl, Command.LineDown);
KeyBindings.Add (Key.PageUp, Command.PageUp);
KeyBindings.Add (Key.PageDown, Command.PageDown);
KeyBindings.Add (Key.V.WithCtrl, Command.PageDown);
KeyBindings.Add (Key.Home, Command.TopHome);
KeyBindings.Add (Key.End, Command.BottomEnd);
KeyBindings.Add (Key.Enter, Command.OpenSelectedItem);
}
/// Gets or sets whether this allows items to be marked.
/// Set to to allow marking elements of the list.
///
/// If set to , will render items marked items with "[x]", and
/// unmarked items with "[ ]" spaces. SPACE key will toggle marking. The default is .
///
public bool AllowsMarking
{
get => _allowsMarking;
set
{
_allowsMarking = value;
if (_allowsMarking)
{
KeyBindings.Add (Key.Space, Command.Select);
}
else
{
KeyBindings.Remove (Key.Space);
}
SetNeedsDisplay ();
}
}
///
/// If set to more than one item can be selected. If selecting an
/// item will cause all others to be un-selected. The default is .
///
public bool AllowsMultipleSelection
{
get => _allowsMultipleSelection;
set
{
_allowsMultipleSelection = value;
if (Source is { } && !_allowsMultipleSelection)
{
// Clear all selections except selected
for (var i = 0; i < Source.Count; i++)
{
if (Source.IsMarked (i) && i != _selected)
{
Source.SetMark (i, false);
}
}
}
SetNeedsDisplay ();
}
}
///
/// Gets the that searches the collection as the
/// user types.
///
public CollectionNavigator KeystrokeNavigator { get; } = new ();
/// Gets or sets the leftmost column that is currently visible (when scrolling horizontally).
/// The left position.
public int LeftItem
{
get => Viewport.X;
set
{
if (_source is null)
{
return;
}
if (value < 0 || (MaxLength > 0 && value >= MaxLength))
{
throw new ArgumentException ("value");
}
Viewport = Viewport with { X = value };
SetNeedsDisplay ();
}
}
/// Gets the widest item in the list.
public int MaxLength => _source?.Length ?? 0;
/// Gets or sets the index of the currently selected item.
/// The selected item.
public int SelectedItem
{
get => _selected;
set
{
if (_source is null || _source.Count == 0)
{
return;
}
if (value < -1 || value >= _source.Count)
{
throw new ArgumentException ("value");
}
_selected = value;
OnSelectedChanged ();
}
}
/// Gets or sets the backing this , enabling custom rendering.
/// The source.
/// Use to set a new source.
public IListDataSource Source
{
get => _source;
set
{
if (_source == value)
{
return;
}
_source = value;
SetContentSize (new Size (_source?.Length ?? Viewport.Width, _source?.Count ?? Viewport.Width));
if (IsInitialized)
{
Viewport = Viewport with { Y = 0 };
}
KeystrokeNavigator.Collection = _source?.ToList ();
_selected = -1;
_lastSelectedItem = -1;
SetNeedsDisplay ();
}
}
/// Gets or sets the index of the item that will appear at the top of the .
///
/// This a helper property for accessing listView.Viewport.Y.
///
/// The top item.
public int TopItem
{
get => Viewport.Y;
set
{
if (_source is null)
{
return;
}
Viewport = Viewport with { Y = value };
}
}
///
/// If and are both ,
/// unmarks all marked items other than the currently selected.
///
/// if unmarking was successful.
public virtual bool AllowsAll ()
{
if (!_allowsMarking)
{
return false;
}
if (!AllowsMultipleSelection)
{
for (var i = 0; i < Source.Count; i++)
{
if (Source.IsMarked (i) && i != _selected)
{
Source.SetMark (i, false);
return true;
}
}
}
return true;
}
/// Ensures the selected item is always visible on the screen.
public void EnsureSelectedItemVisible ()
{
if (SuperView?.IsInitialized == true)
{
if (_selected < Viewport.Y)
{
// TODO: The Max check here is not needed because, by default, Viewport enforces staying w/in ContentArea (View.ScrollSettings).
Viewport = Viewport with { Y = _selected };
}
else if (Viewport.Height > 0 && _selected >= Viewport.Y + Viewport.Height)
{
Viewport = Viewport with { Y = _selected - Viewport.Height + 1 };
}
LayoutStarted -= ListView_LayoutStarted;
}
else
{
LayoutStarted += ListView_LayoutStarted;
}
}
/// Marks the if it is not already marked.
/// if the was marked.
public virtual bool MarkUnmarkRow ()
{
if (AllowsAll ())
{
Source.SetMark (SelectedItem, !Source.IsMarked (SelectedItem));
SetNeedsDisplay ();
return true;
}
return false;
}
///
protected internal override bool OnMouseEvent (MouseEvent me)
{
if (!me.Flags.HasFlag (MouseFlags.Button1Clicked)
&& !me.Flags.HasFlag (MouseFlags.Button1DoubleClicked)
&& me.Flags != MouseFlags.WheeledDown
&& me.Flags != MouseFlags.WheeledUp
&& me.Flags != MouseFlags.WheeledRight
&& me.Flags != MouseFlags.WheeledLeft)
{
return false;
}
if (!HasFocus && CanFocus)
{
SetFocus ();
}
if (_source is null)
{
return false;
}
if (me.Flags == MouseFlags.WheeledDown)
{
ScrollVertical (1);
return true;
}
if (me.Flags == MouseFlags.WheeledUp)
{
ScrollVertical (-1);
return true;
}
if (me.Flags == MouseFlags.WheeledRight)
{
ScrollHorizontal (1);
return true;
}
if (me.Flags == MouseFlags.WheeledLeft)
{
ScrollHorizontal (-1);
return true;
}
if (me.Position.Y + Viewport.Y >= _source.Count
|| me.Position.Y + Viewport.Y < 0
|| me.Position.Y + Viewport.Y > Viewport.Y + Viewport.Height)
{
return true;
}
_selected = Viewport.Y + me.Position.Y;
if (AllowsAll ())
{
Source.SetMark (SelectedItem, !Source.IsMarked (SelectedItem));
SetNeedsDisplay ();
return true;
}
OnSelectedChanged ();
SetNeedsDisplay ();
if (me.Flags == MouseFlags.Button1DoubleClicked)
{
OnOpenSelectedItem ();
}
return true;
}
/// Changes the to the next item in the list, scrolling the list if needed.
///
public virtual bool MoveDown ()
{
if (_source is null || _source.Count == 0)
{
// Do we set lastSelectedItem to -1 here?
return false; //Nothing for us to move to
}
if (_selected >= _source.Count)
{
// If for some reason we are currently outside of the
// valid values range, we should select the bottommost valid value.
// This can occur if the backing data source changes.
_selected = _source.Count - 1;
OnSelectedChanged ();
SetNeedsDisplay ();
}
else if (_selected + 1 < _source.Count)
{
//can move by down by one.
_selected++;
if (_selected >= Viewport.Y + Viewport.Height)
{
Viewport = Viewport with { Y = Viewport.Y + 1 };
}
else if (_selected < Viewport.Y)
{
Viewport = Viewport with { Y = _selected };
}
OnSelectedChanged ();
SetNeedsDisplay ();
}
else if (_selected == 0)
{
OnSelectedChanged ();
SetNeedsDisplay ();
}
else if (_selected >= Viewport.Y + Viewport.Height)
{
Viewport = Viewport with { Y = _source.Count - Viewport.Height };
SetNeedsDisplay ();
}
return true;
}
/// Changes the to last item in the list, scrolling the list if needed.
///
public virtual bool MoveEnd ()
{
if (_source is { Count: > 0 } && _selected != _source.Count - 1)
{
_selected = _source.Count - 1;
if (Viewport.Y + _selected > Viewport.Height - 1)
{
Viewport = Viewport with { Y = _selected };
}
OnSelectedChanged ();
SetNeedsDisplay ();
}
return true;
}
/// Changes the to the first item in the list, scrolling the list if needed.
///
public virtual bool MoveHome ()
{
if (_selected != 0)
{
_selected = 0;
Viewport = Viewport with { Y = _selected };
OnSelectedChanged ();
SetNeedsDisplay ();
}
return true;
}
///
/// Changes the to the item just below the bottom of the visible list, scrolling if
/// needed.
///
///
public virtual bool MovePageDown ()
{
if (_source is null)
{
return true;
}
int n = _selected + Viewport.Height;
if (n >= _source.Count)
{
n = _source.Count - 1;
}
if (n != _selected)
{
_selected = n;
if (_source.Count >= Viewport.Height)
{
Viewport = Viewport with { Y = _selected };
}
else
{
Viewport = Viewport with { Y = 0 };
}
OnSelectedChanged ();
SetNeedsDisplay ();
}
return true;
}
/// Changes the to the item at the top of the visible list.
///
public virtual bool MovePageUp ()
{
int n = _selected - Viewport.Height;
if (n < 0)
{
n = 0;
}
if (n != _selected)
{
_selected = n;
Viewport = Viewport with { Y = _selected };
OnSelectedChanged ();
SetNeedsDisplay ();
}
return true;
}
/// Changes the to the previous item in the list, scrolling the list if needed.
///
public virtual bool MoveUp ()
{
if (_source is null || _source.Count == 0)
{
// Do we set lastSelectedItem to -1 here?
return false; //Nothing for us to move to
}
if (_selected >= _source.Count)
{
// If for some reason we are currently outside of the
// valid values range, we should select the bottommost valid value.
// This can occur if the backing data source changes.
_selected = _source.Count - 1;
OnSelectedChanged ();
SetNeedsDisplay ();
}
else if (_selected > 0)
{
_selected--;
if (_selected > Source.Count)
{
_selected = Source.Count - 1;
}
if (_selected < Viewport.Y)
{
Viewport = Viewport with { Y = _selected };
}
else if (_selected > Viewport.Y + Viewport.Height)
{
Viewport = Viewport with { Y = _selected - Viewport.Height + 1 };
}
OnSelectedChanged ();
SetNeedsDisplay ();
}
else if (_selected < Viewport.Y)
{
Viewport = Viewport with { Y = _selected };
SetNeedsDisplay ();
}
return true;
}
///
public override void OnDrawContent (Rectangle viewport)
{
base.OnDrawContent (viewport);
Attribute current = ColorScheme.Focus;
Driver.SetAttribute (current);
Move (0, 0);
Rectangle f = Viewport;
int item = Viewport.Y;
bool focused = HasFocus;
int col = _allowsMarking ? 2 : 0;
int start = Viewport.X;
for (var row = 0; row < f.Height; row++, item++)
{
bool isSelected = item == _selected;
Attribute newcolor = focused ? isSelected ? ColorScheme.Focus : GetNormalColor () :
isSelected ? ColorScheme.HotNormal : GetNormalColor ();
if (newcolor != current)
{
Driver.SetAttribute (newcolor);
current = newcolor;
}
Move (0, row);
if (_source is null || item >= _source.Count)
{
for (var c = 0; c < f.Width; c++)
{
Driver.AddRune ((Rune)' ');
}
}
else
{
var rowEventArgs = new ListViewRowEventArgs (item);
OnRowRender (rowEventArgs);
if (rowEventArgs.RowAttribute is { } && current != rowEventArgs.RowAttribute)
{
current = (Attribute)rowEventArgs.RowAttribute;
Driver.SetAttribute (current);
}
if (_allowsMarking)
{
Driver.AddRune (
_source.IsMarked (item) ? AllowsMultipleSelection ? Glyphs.Checked : Glyphs.Selected :
AllowsMultipleSelection ? Glyphs.UnChecked : Glyphs.UnSelected
);
Driver.AddRune ((Rune)' ');
}
Source.Render (this, Driver, isSelected, item, col, row, f.Width - col, start);
}
}
}
///
public override bool OnEnter (View view)
{
if (_lastSelectedItem != _selected)
{
EnsureSelectedItemVisible ();
}
return base.OnEnter (view);
}
// TODO: This should be cancelable
/// Invokes the event if it is defined.
/// if the event was fired.
public bool OnOpenSelectedItem ()
{
if (_source is null || _source.Count <= _selected || _selected < 0 || OpenSelectedItem is null)
{
return false;
}
object value = _source.ToList () [_selected];
// By default, Command.Accept calls OnAccept, so we need to call it here to ensure that the event is fired.
if (OnAccept () == true)
{
return true;
}
OpenSelectedItem?.Invoke (this, new ListViewItemEventArgs (_selected, value));
return true;
}
///
public override bool OnProcessKeyDown (Key a)
{
// Enable user to find & select an item by typing text
if (CollectionNavigatorBase.IsCompatibleKey (a))
{
int? newItem = KeystrokeNavigator?.GetNextMatchingItem (SelectedItem, (char)a);
if (newItem is int && newItem != -1)
{
SelectedItem = (int)newItem;
EnsureSelectedItemVisible ();
SetNeedsDisplay ();
return true;
}
}
return false;
}
/// Virtual method that will invoke the .
///
public virtual void OnRowRender (ListViewRowEventArgs rowEventArgs) { RowRender?.Invoke (this, rowEventArgs); }
/// Invokes the event if it is defined.
///
public virtual bool OnSelectedChanged ()
{
if (_selected != _lastSelectedItem)
{
object value = _source?.Count > 0 ? _source.ToList () [_selected] : null;
SelectedItemChanged?.Invoke (this, new ListViewItemEventArgs (_selected, value));
_lastSelectedItem = _selected;
EnsureSelectedItemVisible ();
return true;
}
return false;
}
/// This event is raised when the user Double Clicks on an item or presses ENTER to open the selected item.
public event EventHandler OpenSelectedItem;
///
public override Point? PositionCursor ()
{
int x = 0;
int y = _selected - Viewport.Y;
if (!_allowsMarking)
{
x = Viewport.Width - 1;
}
Move (x, y);
return null; // Don't show the cursor
}
/// This event is invoked when this is being drawn before rendering.
public event EventHandler RowRender;
/// This event is raised when the selected item in the has changed.
public event EventHandler SelectedItemChanged;
/// Sets the source of the to an .
/// An object implementing the IList interface.
///
/// Use the property to set a new source and use custome
/// rendering.
///
public void SetSource (IList source)
{
if (source is null && (Source is null || !(Source is ListWrapper)))
{
Source = null;
}
else
{
Source = new ListWrapper (source);
}
}
/// Sets the source to an value asynchronously.
/// An item implementing the IList interface.
///
/// Use the property to set a new source and use custom
/// rendering.
///
public Task SetSourceAsync (IList source)
{
return Task.Factory.StartNew (
() =>
{
if (source is null && (Source is null || !(Source is ListWrapper)))
{
Source = null;
}
else
{
Source = new ListWrapper (source);
}
return source;
},
CancellationToken.None,
TaskCreationOptions.DenyChildAttach,
TaskScheduler.Default
);
}
private void ListView_LayoutStarted (object sender, LayoutEventArgs e) { EnsureSelectedItemVisible (); }
}
///
/// Provides a default implementation of that renders items
/// using .
///
public class ListWrapper : IListDataSource
{
private readonly int _count;
private readonly BitArray _marks;
private readonly IList _source;
///
public ListWrapper (IList source)
{
if (source is { })
{
_count = source.Count;
_marks = new BitArray (_count);
_source = source;
Length = GetMaxLengthItem ();
}
}
///
public int Count => _source is { } ? _source.Count : 0;
///
public int Length { get; }
///
public void Render (
ListView container,
ConsoleDriver driver,
bool marked,
int item,
int col,
int line,
int width,
int start = 0
)
{
container.Move (Math.Max (col - start, 0), line);
object t = _source? [item];
if (t is null)
{
RenderUstr (driver, "", col, line, width);
}
else
{
if (t is string u)
{
RenderUstr (driver, u, col, line, width, start);
}
else if (t is string s)
{
RenderUstr (driver, s, col, line, width, start);
}
else
{
RenderUstr (driver, t.ToString (), col, line, width, start);
}
}
}
///
public bool IsMarked (int item)
{
if (item >= 0 && item < _count)
{
return _marks [item];
}
return false;
}
///
public void SetMark (int item, bool value)
{
if (item >= 0 && item < _count)
{
_marks [item] = value;
}
}
///
public IList ToList () { return _source; }
///
public int StartsWith (string search)
{
if (_source is null || _source?.Count == 0)
{
return -1;
}
for (var i = 0; i < _source.Count; i++)
{
object t = _source [i];
if (t is string u)
{
if (u.ToUpper ().StartsWith (search.ToUpperInvariant ()))
{
return i;
}
}
else if (t is string s)
{
if (s.StartsWith (search, StringComparison.InvariantCultureIgnoreCase))
{
return i;
}
}
}
return -1;
}
private int GetMaxLengthItem ()
{
if (_source is null || _source?.Count == 0)
{
return 0;
}
var maxLength = 0;
for (var i = 0; i < _source.Count; i++)
{
object t = _source [i];
int l;
if (t is string u)
{
l = u.GetColumns ();
}
else if (t is string s)
{
l = s.Length;
}
else
{
l = t.ToString ().Length;
}
if (l > maxLength)
{
maxLength = l;
}
}
return maxLength;
}
private void RenderUstr (ConsoleDriver driver, string ustr, int col, int line, int width, int start = 0)
{
string str = start > ustr.GetColumns () ? string.Empty : ustr.Substring (Math.Min (start, ustr.ToRunes ().Length - 1));
string u = TextFormatter.ClipAndJustify (str, width, Alignment.Start);
driver.AddStr (u);
width -= u.GetColumns ();
while (width-- > 0)
{
driver.AddRune ((Rune)' ');
}
}
}