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)' '); } } }