using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NStack;
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; }
///
/// 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);
///
/// Should return whether the specified item is currently marked.
///
/// , if marked, otherwise.
/// Item index.
bool IsMarked (int item);
///
/// 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 {
int top, left;
int selected = -1;
IListDataSource source;
///
/// Gets or sets the backing this , enabling custom rendering.
///
/// The source.
///
/// Use to set a new source.
///
public IListDataSource Source {
get => source;
set {
source = value;
KeystrokeNavigator.Collection = source?.ToList ()?.Cast ();
top = 0;
selected = -1;
lastSelectedItem = -1;
SetNeedsDisplay ();
}
}
///
/// 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 == null && (Source == null || !(Source is ListWrapper)))
Source = null;
else {
Source = MakeWrapper (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 == null && (Source == null || !(Source is ListWrapper)))
Source = null;
else
Source = MakeWrapper (source);
return source;
}, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
}
bool allowsMarking;
///
/// 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) {
AddKeyBinding (Key.Space, Command.ToggleChecked);
} else {
ClearKeybinding (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 != null && !allowsMultipleSelection) {
// Clear all selections except selected
for (int i = 0; i < Source.Count; i++) {
if (Source.IsMarked (i) && i != selected) {
Source.SetMark (i, false);
}
}
}
SetNeedsDisplay ();
}
}
///
/// Gets or sets the item that is displayed at the top of the .
///
/// The top item.
public int TopItem {
get => top;
set {
if (source == null)
return;
if (value < 0 || (source.Count > 0 && value >= source.Count))
throw new ArgumentException ("value");
top = Math.Max (value, 0);
SetNeedsDisplay ();
}
}
///
/// Gets or sets the leftmost column that is currently visible (when scrolling horizontally).
///
/// The left position.
public int LeftItem {
get => left;
set {
if (source == null)
return;
if (value < 0 || (Maxlength > 0 && value >= Maxlength))
throw new ArgumentException ("value");
left = 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 == null || source.Count == 0) {
return;
}
if (value < -1 || value >= source.Count) {
throw new ArgumentException ("value");
}
selected = value;
OnSelectedChanged ();
}
}
static IListDataSource MakeWrapper (IList source)
{
return new ListWrapper (source);
}
///
/// Initializes a new instance of that will display the
/// contents of the object implementing the interface,
/// with relative positioning.
///
/// An data source, if the elements are strings or ustrings,
/// the string is rendered, otherwise the ToString() method is invoked on the result.
public ListView (IList source) : this (MakeWrapper (source))
{
}
///
/// Initializes a new instance of that will display the provided data source, using relative positioning.
///
/// object that provides a mechanism to render the data.
/// The number of elements on the collection should not change, if you must change, set
/// the "Source" property to reset the internal settings of the ListView.
public ListView (IListDataSource source) : base ()
{
this.source = source;
Initialize ();
}
///
/// Initializes a new instance of . Set the property to display something.
///
public ListView () : base ()
{
Initialize ();
}
///
/// Initializes a new instance of that will display the contents of the object implementing the interface with an absolute position.
///
/// Frame for the listview.
/// An IList data source, if the elements of the IList are strings or ustrings,
/// the string is rendered, otherwise the ToString() method is invoked on the result.
public ListView (Rect rect, IList source) : this (rect, MakeWrapper (source))
{
Initialize ();
}
///
/// Initializes a new instance of with the provided data source and an absolute position
///
/// Frame for the listview.
/// IListDataSource object that provides a mechanism to render the data.
/// The number of elements on the collection should not change, if you must change,
/// set the "Source" property to reset the internal settings of the ListView.
public ListView (Rect rect, IListDataSource source) : base (rect)
{
this.source = source;
Initialize ();
}
void Initialize ()
{
Source = source;
CanFocus = true;
// Things this view knows how to do
AddCommand (Command.LineUp, () => MoveUp ());
AddCommand (Command.LineDown, () => MoveDown ());
AddCommand (Command.ScrollUp, () => ScrollUp (1));
AddCommand (Command.ScrollDown, () => ScrollDown (1));
AddCommand (Command.PageUp, () => MovePageUp ());
AddCommand (Command.PageDown, () => MovePageDown ());
AddCommand (Command.TopHome, () => MoveHome ());
AddCommand (Command.BottomEnd, () => MoveEnd ());
AddCommand (Command.OpenSelectedItem, () => OnOpenSelectedItem ());
AddCommand (Command.ToggleChecked, () => MarkUnmarkRow ());
// Default keybindings for all ListViews
AddKeyBinding (Key.CursorUp, Command.LineUp);
AddKeyBinding (Key.P | Key.CtrlMask, Command.LineUp);
AddKeyBinding (Key.CursorDown, Command.LineDown);
AddKeyBinding (Key.N | Key.CtrlMask, Command.LineDown);
AddKeyBinding (Key.PageUp, Command.PageUp);
AddKeyBinding (Key.PageDown, Command.PageDown);
AddKeyBinding (Key.V | Key.CtrlMask, Command.PageDown);
AddKeyBinding (Key.Home, Command.TopHome);
AddKeyBinding (Key.End, Command.BottomEnd);
AddKeyBinding (Key.Enter, Command.OpenSelectedItem);
}
///
public override void Redraw (Rect bounds)
{
var current = ColorScheme.Focus;
Driver.SetAttribute (current);
Move (0, 0);
var f = Frame;
var item = top;
bool focused = HasFocus;
int col = allowsMarking ? 2 : 0;
int start = left;
for (int row = 0; row < f.Height; row++, item++) {
bool isSelected = item == selected;
var newcolor = focused ? (isSelected ? ColorScheme.Focus : GetNormalColor ())
: (isSelected ? ColorScheme.HotNormal : GetNormalColor ());
if (newcolor != current) {
Driver.SetAttribute (newcolor);
current = newcolor;
}
Move (0, row);
if (source == null || item >= source.Count) {
for (int c = 0; c < f.Width; c++)
Driver.AddRune (' ');
} else {
var rowEventArgs = new ListViewRowEventArgs (item);
OnRowRender (rowEventArgs);
if (rowEventArgs.RowAttribute != null && current != rowEventArgs.RowAttribute) {
current = (Attribute)rowEventArgs.RowAttribute;
Driver.SetAttribute (current);
}
if (allowsMarking) {
Driver.AddRune (source.IsMarked (item) ? (AllowsMultipleSelection ? Driver.Checked : Driver.Selected) :
(AllowsMultipleSelection ? Driver.UnChecked : Driver.UnSelected));
Driver.AddRune (' ');
}
Source.Render (this, Driver, isSelected, item, col, row, f.Width - col, start);
}
}
}
///
/// This event is raised when the selected item in the has changed.
///
public event EventHandler SelectedItemChanged;
///
/// This event is raised when the user Double Clicks on an item or presses ENTER to open the selected item.
///
public event EventHandler OpenSelectedItem;
///
/// This event is invoked when this is being drawn before rendering.
///
public event EventHandler RowRender;
///
/// Gets the that searches the collection as
/// the user types.
///
public CollectionNavigator KeystrokeNavigator { get; private set; } = new CollectionNavigator ();
///
public override bool ProcessKey (KeyEvent kb)
{
if (source == null) {
return base.ProcessKey (kb);
}
var result = InvokeKeybindings (kb);
if (result != null) {
return (bool)result;
}
// Enable user to find & select an item by typing text
if (CollectionNavigator.IsCompatibleKey (kb)) {
var newItem = KeystrokeNavigator?.GetNextMatchingItem (SelectedItem, (char)kb.KeyValue);
if (newItem is int && newItem != -1) {
SelectedItem = (int)newItem;
EnsureSelectedItemVisible ();
SetNeedsDisplay ();
return true;
}
}
return false;
}
///
/// 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 (int i = 0; i < Source.Count; i++) {
if (Source.IsMarked (i) && i != selected) {
Source.SetMark (i, false);
return true;
}
}
}
return true;
}
///
/// 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;
}
///
/// Changes the to the item at the top of the visible list.
///
///
public virtual bool MovePageUp ()
{
int n = (selected - Frame.Height);
if (n < 0)
n = 0;
if (n != selected) {
selected = n;
top = Math.Max (selected, 0);
OnSelectedChanged ();
SetNeedsDisplay ();
}
return true;
}
///
/// Changes the to the item just below the bottom
/// of the visible list, scrolling if needed.
///
///
public virtual bool MovePageDown ()
{
var n = (selected + Frame.Height);
if (n >= source.Count)
n = source.Count - 1;
if (n != selected) {
selected = n;
if (source.Count >= Frame.Height)
top = Math.Max (selected, 0);
else
top = 0;
OnSelectedChanged ();
SetNeedsDisplay ();
}
return true;
}
///
/// Changes the to the next item in the list,
/// scrolling the list if needed.
///
///
public virtual bool MoveDown ()
{
if (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 >= top + Frame.Height) {
top++;
} else if (selected < top) {
top = Math.Max (selected, 0);
}
OnSelectedChanged ();
SetNeedsDisplay ();
} else if (selected == 0) {
OnSelectedChanged ();
SetNeedsDisplay ();
} else if (selected >= top + Frame.Height) {
top = Math.Max (source.Count - Frame.Height, 0);
SetNeedsDisplay ();
}
return true;
}
///
/// Changes the to the previous item in the list,
/// scrolling the list if needed.
///
///
public virtual bool MoveUp ()
{
if (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 < top) {
top = Math.Max (selected, 0);
} else if (selected > top + Frame.Height) {
top = Math.Max (selected - Frame.Height + 1, 0);
}
OnSelectedChanged ();
SetNeedsDisplay ();
} else if (selected < top) {
top = Math.Max (selected, 0);
SetNeedsDisplay ();
}
return true;
}
///
/// Changes the to last item in the list,
/// scrolling the list if needed.
///
///
public virtual bool MoveEnd ()
{
if (source.Count > 0 && selected != source.Count - 1) {
selected = source.Count - 1;
if (top + selected > Frame.Height - 1) {
top = Math.Max (selected, 0);
}
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;
top = Math.Max (selected, 0);
OnSelectedChanged ();
SetNeedsDisplay ();
}
return true;
}
///
/// Scrolls the view down by items.
///
/// Number of items to scroll down.
public virtual bool ScrollDown (int items)
{
top = Math.Max (Math.Min (top + items, source.Count - 1), 0);
SetNeedsDisplay ();
return true;
}
///
/// Scrolls the view up by items.
///
/// Number of items to scroll up.
public virtual bool ScrollUp (int items)
{
top = Math.Max (top - items, 0);
SetNeedsDisplay ();
return true;
}
///
/// Scrolls the view right.
///
/// Number of columns to scroll right.
public virtual bool ScrollRight (int cols)
{
left = Math.Max (Math.Min (left + cols, Maxlength - 1), 0);
SetNeedsDisplay ();
return true;
}
///
/// Scrolls the view left.
///
/// Number of columns to scroll left.
public virtual bool ScrollLeft (int cols)
{
left = Math.Max (left - cols, 0);
SetNeedsDisplay ();
return true;
}
int lastSelectedItem = -1;
private bool allowsMultipleSelection = true;
///
/// Invokes the event if it is defined.
///
///
public virtual bool OnSelectedChanged ()
{
if (selected != lastSelectedItem) {
var value = source?.Count > 0 ? source.ToList () [selected] : null;
SelectedItemChanged?.Invoke (this, new ListViewItemEventArgs (selected, value));
lastSelectedItem = selected;
EnsureSelectedItemVisible ();
return true;
}
return false;
}
///
/// Invokes the event if it is defined.
///
///
public virtual bool OnOpenSelectedItem ()
{
if (source.Count <= selected || selected < 0 || OpenSelectedItem == null) {
return false;
}
var value = source.ToList () [selected];
OpenSelectedItem?.Invoke (this, new ListViewItemEventArgs (selected, value));
return true;
}
///
/// Virtual method that will invoke the .
///
///
public virtual void OnRowRender (ListViewRowEventArgs rowEventArgs)
{
RowRender?.Invoke (this, rowEventArgs);
}
///
public override bool OnEnter (View view)
{
if (IsInitialized) {
Application.Driver.SetCursorVisibility (CursorVisibility.Invisible);
}
if (lastSelectedItem != selected) {
EnsureSelectedItemVisible ();
}
return base.OnEnter (view);
}
///
/// Ensures the selected item is always visible on the screen.
///
public void EnsureSelectedItemVisible ()
{
SuperView?.LayoutSubviews ();
if (selected < top) {
top = Math.Max (selected, 0);
} else if (Frame.Height > 0 && selected >= top + Frame.Height) {
top = Math.Max (selected - Frame.Height + 1, 0);
}
}
///
public override void PositionCursor ()
{
if (allowsMarking)
Move (0, selected - top);
else
Move (Bounds.Width - 1, selected - top);
}
///
public override bool MouseEvent (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 == null) {
return false;
}
if (me.Flags == MouseFlags.WheeledDown) {
ScrollDown (1);
return true;
} else if (me.Flags == MouseFlags.WheeledUp) {
ScrollUp (1);
return true;
} else if (me.Flags == MouseFlags.WheeledRight) {
ScrollRight (1);
return true;
} else if (me.Flags == MouseFlags.WheeledLeft) {
ScrollLeft (1);
return true;
}
if (me.Y + top >= source.Count) {
return true;
}
selected = top + me.Y;
if (AllowsAll ()) {
Source.SetMark (SelectedItem, !Source.IsMarked (SelectedItem));
SetNeedsDisplay ();
return true;
}
OnSelectedChanged ();
SetNeedsDisplay ();
if (me.Flags == MouseFlags.Button1DoubleClicked) {
OnOpenSelectedItem ();
}
return true;
}
}
///
/// Provides a default implementation of that renders
/// items using .
///
public class ListWrapper : IListDataSource {
IList src;
BitArray marks;
int count, len;
///
public ListWrapper (IList source)
{
if (source != null) {
count = source.Count;
marks = new BitArray (count);
src = source;
len = GetMaxLengthItem ();
}
}
///
public int Count => src != null ? src.Count : 0;
///
public int Length => len;
int GetMaxLengthItem ()
{
if (src == null || src?.Count == 0) {
return 0;
}
int maxLength = 0;
for (int i = 0; i < src.Count; i++) {
var t = src [i];
int l;
if (t is ustring u) {
l = TextFormatter.GetTextWidth (u);
} else if (t is string s) {
l = s.Length;
} else {
l = t.ToString ().Length;
}
if (l > maxLength) {
maxLength = l;
}
}
return maxLength;
}
void RenderUstr (ConsoleDriver driver, ustring ustr, int col, int line, int width, int start = 0)
{
var u = TextFormatter.ClipAndJustify (ustr, width, TextAlignment.Left);
driver.AddStr (u);
width -= TextFormatter.GetTextWidth (u);
while (width-- > 0) {
driver.AddRune (' ');
}
}
///
public void Render (ListView container, ConsoleDriver driver, bool marked, int item, int col, int line, int width, int start = 0)
{
container.Move (col, line);
var t = src? [item];
if (t == null) {
RenderUstr (driver, ustring.Make (""), col, line, width);
} else {
if (t is ustring 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 src;
}
///
public int StartsWith (string search)
{
if (src == null || src?.Count == 0) {
return -1;
}
for (int i = 0; i < src.Count; i++) {
var t = src [i];
if (t is ustring 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;
}
}
}