1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009 |
- using System;
- using System.Collections;
- using System.Collections.Generic;
- using System.Linq;
- using System.Threading;
- using System.Threading.Tasks;
- using NStack;
- namespace Terminal.Gui {
- /// <summary>
- /// Implement <see cref="IListDataSource"/> to provide custom rendering for a <see cref="ListView"/>.
- /// </summary>
- public interface IListDataSource {
- /// <summary>
- /// Returns the number of elements to display
- /// </summary>
- int Count { get; }
- /// <summary>
- /// Returns the maximum length of elements to display
- /// </summary>
- int Length { get; }
- /// <summary>
- /// This method is invoked to render a specified item, the method should cover the entire provided width.
- /// </summary>
- /// <returns>The render.</returns>
- /// <param name="container">The list view to render.</param>
- /// <param name="driver">The console driver to render.</param>
- /// <param name="selected">Describes whether the item being rendered is currently selected by the user.</param>
- /// <param name="item">The index of the item to render, zero for the first item and so on.</param>
- /// <param name="col">The column where the rendering will start</param>
- /// <param name="line">The line where the rendering will be done.</param>
- /// <param name="width">The width that must be filled out.</param>
- /// <param name="start">The index of the string to be displayed.</param>
- /// <remarks>
- /// The default color will be set before this method is invoked, and will be based on whether the item is selected or not.
- /// </remarks>
- void Render (ListView container, ConsoleDriver driver, bool selected, int item, int col, int line, int width, int start = 0);
- /// <summary>
- /// Should return whether the specified item is currently marked.
- /// </summary>
- /// <returns><see langword="true"/>, if marked, <see langword="false"/> otherwise.</returns>
- /// <param name="item">Item index.</param>
- bool IsMarked (int item);
- /// <summary>
- /// Flags the item as marked.
- /// </summary>
- /// <param name="item">Item index.</param>
- /// <param name="value">If set to <see langword="true"/> value.</param>
- void SetMark (int item, bool value);
- /// <summary>
- /// Return the source as IList.
- /// </summary>
- /// <returns></returns>
- IList ToList ();
- }
- /// <summary>
- /// Implement <see cref="IListDataSourceSearchable"/> to provide custom rendering for a <see cref="ListView"/> that
- /// supports searching for items.
- /// </summary>
- public interface IListDataSourceSearchable : IListDataSource {
- /// <summary>
- /// Finds the first item that starts with the specified search string. Used by the default implementation
- /// to support typing the first characters of an item to find it and move the selection to i.
- /// </summary>
- /// <param name="search">Text to search for.</param>
- /// <returns>The index of the first <see cref="ListView"/> item that starts with <paramref name="search"/>.
- /// Returns <see langword="-1"/> if <paramref name="search"/> was not found.</returns>
- int StartsWith (string search);
- }
- /// <summary>
- /// ListView <see cref="View"/> renders a scrollable list of data where each item can be activated to perform an action.
- /// </summary>
- /// <remarks>
- /// <para>
- /// The <see cref="ListView"/> 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 <see cref="AllowsMarking"/> property is true, elements of the list can be marked by the user.
- /// </para>
- /// <para>
- /// By default <see cref="ListView"/> uses <see cref="object.ToString"/> to render the items of any
- /// <see cref="IList"/> object (e.g. arrays, <see cref="List{T}"/>,
- /// and other collections). Alternatively, an object that implements <see cref="IListDataSource"/>
- /// or <see cref="IListDataSourceSearchable"/> can be provided giving full control of what is rendered.
- /// </para>
- /// <para>
- /// <see cref="ListView"/> can display any object that implements the <see cref="IList"/> interface.
- /// <see cref="string"/> values are converted into <see cref="ustring"/> values before rendering, and other values are
- /// converted into <see cref="string"/> by calling <see cref="object.ToString"/> and then converting to <see cref="ustring"/> .
- /// </para>
- /// <para>
- /// To change the contents of the ListView, set the <see cref="Source"/> property (when
- /// providing custom rendering via <see cref="IListDataSource"/>) or call <see cref="SetSource"/>
- /// an <see cref="IList"/> is being used.
- /// </para>
- /// <para>
- /// When <see cref="AllowsMarking"/> 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 <see cref="AllowsMarking"/> to false and implement custom rendering.
- /// </para>
- /// <para>
- /// By default or if <see cref="Source"/> is set to an object that implements
- /// <see cref="IListDataSourceSearchable"/>, 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.
- /// </para>
- /// </remarks>
- public class ListView : View {
- int top, left;
- int selected;
- IListDataSource source;
- /// <summary>
- /// Gets or sets the <see cref="IListDataSource"/> backing this <see cref="ListView"/>, enabling custom rendering.
- /// </summary>
- /// <value>The source.</value>
- /// <remarks>
- /// Use <see cref="SetSource"/> to set a new <see cref="IList"/> source.
- /// </remarks>
- public IListDataSource Source {
- get => source;
- set {
- source = value;
- navigator = null;
- top = 0;
- selected = 0;
- lastSelectedItem = -1;
- SetNeedsDisplay ();
- }
- }
- /// <summary>
- /// Sets the source of the <see cref="ListView"/> to an <see cref="IList"/>.
- /// </summary>
- /// <value>An object implementing the IList interface.</value>
- /// <remarks>
- /// Use the <see cref="Source"/> property to set a new <see cref="IListDataSource"/> source and use custome rendering.
- /// </remarks>
- public void SetSource (IList source)
- {
- if (source == null && (Source == null || !(Source is ListWrapper)))
- Source = null;
- else {
- Source = MakeWrapper (source);
- }
- }
- /// <summary>
- /// Sets the source to an <see cref="IList"/> value asynchronously.
- /// </summary>
- /// <value>An item implementing the IList interface.</value>
- /// <remarks>
- /// Use the <see cref="Source"/> property to set a new <see cref="IListDataSource"/> source and use custom rendering.
- /// </remarks>
- 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;
- /// <summary>
- /// Gets or sets whether this <see cref="ListView"/> allows items to be marked.
- /// </summary>
- /// <value>Set to <see langword="true"/> to allow marking elements of the list.</value>
- /// <remarks>
- /// If set to <see langword="true"/>, <see cref="ListView"/> will render items marked items with "[x]", and unmarked items with "[ ]"
- /// spaces. SPACE key will toggle marking. The default is <see langword="false"/>.
- /// </remarks>
- public bool AllowsMarking {
- get => allowsMarking;
- set {
- allowsMarking = value;
- if (allowsMarking) {
- AddKeyBinding (Key.Space, Command.ToggleChecked);
- } else {
- ClearKeybinding (Key.Space);
- }
- SetNeedsDisplay ();
- }
- }
- /// <summary>
- /// If set to <see langword="true"/> more than one item can be selected. If <see langword="false"/> selecting
- /// an item will cause all others to be un-selected. The default is <see langword="false"/>.
- /// </summary>
- 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 ();
- }
- }
- /// <summary>
- /// Gets or sets the item that is displayed at the top of the <see cref="ListView"/>.
- /// </summary>
- /// <value>The top item.</value>
- public int TopItem {
- get => top;
- set {
- if (source == null)
- return;
- if (value < 0 || (source.Count > 0 && value >= source.Count))
- throw new ArgumentException ("value");
- top = value;
- SetNeedsDisplay ();
- }
- }
- /// <summary>
- /// Gets or sets the leftmost column that is currently visible (when scrolling horizontally).
- /// </summary>
- /// <value>The left position.</value>
- public int LeftItem {
- get => left;
- set {
- if (source == null)
- return;
- if (value < 0 || (Maxlength > 0 && value >= Maxlength))
- throw new ArgumentException ("value");
- left = value;
- SetNeedsDisplay ();
- }
- }
- /// <summary>
- /// Gets the widest item in the list.
- /// </summary>
- public int Maxlength => (source?.Length) ?? 0;
- /// <summary>
- /// Gets or sets the index of the currently selected item.
- /// </summary>
- /// <value>The selected item.</value>
- public int SelectedItem {
- get => selected;
- set {
- if (source == null || source.Count == 0) {
- return;
- }
- if (value < 0 || value >= source.Count) {
- throw new ArgumentException ("value");
- }
- selected = value;
- OnSelectedChanged ();
- }
- }
- static IListDataSource MakeWrapper (IList source)
- {
- return new ListWrapper (source);
- }
- /// <summary>
- /// Initializes a new instance of <see cref="ListView"/> that will display the
- /// contents of the object implementing the <see cref="IList"/> interface,
- /// with relative positioning.
- /// </summary>
- /// <param name="source">An <see cref="IList"/> data source, if the elements are strings or ustrings,
- /// the string is rendered, otherwise the ToString() method is invoked on the result.</param>
- public ListView (IList source) : this (MakeWrapper (source))
- {
- }
- /// <summary>
- /// Initializes a new instance of <see cref="ListView"/> that will display the provided data source, using relative positioning.
- /// </summary>
- /// <param name="source"><see cref="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.</param>
- public ListView (IListDataSource source) : base ()
- {
- this.source = source;
- Initialize ();
- }
- /// <summary>
- /// Initializes a new instance of <see cref="ListView"/>. Set the <see cref="Source"/> property to display something.
- /// </summary>
- public ListView () : base ()
- {
- Initialize ();
- }
- /// <summary>
- /// Initializes a new instance of <see cref="ListView"/> that will display the contents of the object implementing the <see cref="IList"/> interface with an absolute position.
- /// </summary>
- /// <param name="rect">Frame for the listview.</param>
- /// <param name="source">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.</param>
- public ListView (Rect rect, IList source) : this (rect, MakeWrapper (source))
- {
- Initialize ();
- }
- /// <summary>
- /// Initializes a new instance of <see cref="ListView"/> with the provided data source and an absolute position
- /// </summary>
- /// <param name="rect">Frame for the listview.</param>
- /// <param name="source">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.</param>
- 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);
- }
- ///<inheritdoc/>
- 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);
- }
- }
- }
- /// <summary>
- /// This event is raised when the selected item in the <see cref="ListView"/> has changed.
- /// </summary>
- public event Action<ListViewItemEventArgs> SelectedItemChanged;
- /// <summary>
- /// This event is raised when the user Double Clicks on an item or presses ENTER to open the selected item.
- /// </summary>
- public event Action<ListViewItemEventArgs> OpenSelectedItem;
- /// <summary>
- /// This event is invoked when this <see cref="ListView"/> is being drawn before rendering.
- /// </summary>
- public event Action<ListViewRowEventArgs> RowRender;
- private SearchCollectionNavigator navigator;
- ///<inheritdoc/>
- 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 (SearchCollectionNavigator.IsCompatibleKey(kb)) {
- if (navigator == null) {
- navigator = new SearchCollectionNavigator (source.ToList ().Cast<object> ());
- }
- var newItem = navigator.CalculateNewIndex (SelectedItem, (char)kb.KeyValue);
- if (newItem != SelectedItem) {
- SelectedItem = newItem;
- EnsuresVisibilitySelectedItem ();
- SetNeedsDisplay ();
- return true;
- }
- }
- return false;
- }
- /// <summary>
- /// If <see cref="AllowsMarking"/> and <see cref="AllowsMultipleSelection"/> are both <see langword="true"/>,
- /// unmarks all marked items other than the currently selected.
- /// </summary>
- /// <returns><see langword="true"/> if unmarking was successful.</returns>
- 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;
- }
- /// <summary>
- /// Marks the <see cref="SelectedItem"/> if it is not already marked.
- /// </summary>
- /// <returns><see langword="true"/> if the <see cref="SelectedItem"/> was marked.</returns>
- public virtual bool MarkUnmarkRow ()
- {
- if (AllowsAll ()) {
- Source.SetMark (SelectedItem, !Source.IsMarked (SelectedItem));
- SetNeedsDisplay ();
- return true;
- }
- return false;
- }
- /// <summary>
- /// Changes the <see cref="SelectedItem"/> to the item at the top of the visible list.
- /// </summary>
- /// <returns></returns>
- public virtual bool MovePageUp ()
- {
- int n = (selected - Frame.Height);
- if (n < 0)
- n = 0;
- if (n != selected) {
- selected = n;
- top = selected;
- OnSelectedChanged ();
- SetNeedsDisplay ();
- }
- return true;
- }
- /// <summary>
- /// Changes the <see cref="SelectedItem"/> to the item just below the bottom
- /// of the visible list, scrolling if needed.
- /// </summary>
- /// <returns></returns>
- 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 = selected;
- else
- top = 0;
- OnSelectedChanged ();
- SetNeedsDisplay ();
- }
- return true;
- }
- /// <summary>
- /// Changes the <see cref="SelectedItem"/> to the next item in the list,
- /// scrolling the list if needed.
- /// </summary>
- /// <returns></returns>
- 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 = selected;
- } else if (selected < top) {
- top = selected;
- }
- OnSelectedChanged ();
- SetNeedsDisplay ();
- } else if (selected == 0) {
- OnSelectedChanged ();
- SetNeedsDisplay ();
- } else if (selected >= top + Frame.Height) {
- top = source.Count - Frame.Height;
- SetNeedsDisplay ();
- }
- return true;
- }
- /// <summary>
- /// Changes the <see cref="SelectedItem"/> to the previous item in the list,
- /// scrolling the list if needed.
- /// </summary>
- /// <returns></returns>
- 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 = selected;
- } else if (selected > top + Frame.Height) {
- top = Math.Max (selected - Frame.Height + 1, 0);
- }
- OnSelectedChanged ();
- SetNeedsDisplay ();
- } else if (selected < top) {
- top = selected;
- SetNeedsDisplay ();
- }
- return true;
- }
- /// <summary>
- /// Changes the <see cref="SelectedItem"/> to last item in the list,
- /// scrolling the list if needed.
- /// </summary>
- /// <returns></returns>
- public virtual bool MoveEnd ()
- {
- if (source.Count > 0 && selected != source.Count - 1) {
- selected = source.Count - 1;
- if (top + selected > Frame.Height - 1) {
- top = selected;
- }
- OnSelectedChanged ();
- SetNeedsDisplay ();
- }
- return true;
- }
- /// <summary>
- /// Changes the <see cref="SelectedItem"/> to the first item in the list,
- /// scrolling the list if needed.
- /// </summary>
- /// <returns></returns>
- public virtual bool MoveHome ()
- {
- if (selected != 0) {
- selected = 0;
- top = selected;
- OnSelectedChanged ();
- SetNeedsDisplay ();
- }
- return true;
- }
- /// <summary>
- /// Scrolls the view down by <paramref name="items"/> items.
- /// </summary>
- /// <param name="items">Number of items to scroll down.</param>
- public virtual bool ScrollDown (int items)
- {
- top = Math.Max (Math.Min (top + items, source.Count - 1), 0);
- SetNeedsDisplay ();
- return true;
- }
- /// <summary>
- /// Scrolls the view up by <paramref name="items"/> items.
- /// </summary>
- /// <param name="items">Number of items to scroll up.</param>
- public virtual bool ScrollUp (int items)
- {
- top = Math.Max (top - items, 0);
- SetNeedsDisplay ();
- return true;
- }
- /// <summary>
- /// Scrolls the view right.
- /// </summary>
- /// <param name="cols">Number of columns to scroll right.</param>
- public virtual bool ScrollRight (int cols)
- {
- left = Math.Max (Math.Min (left + cols, Maxlength - 1), 0);
- SetNeedsDisplay ();
- return true;
- }
- /// <summary>
- /// Scrolls the view left.
- /// </summary>
- /// <param name="cols">Number of columns to scroll left.</param>
- public virtual bool ScrollLeft (int cols)
- {
- left = Math.Max (left - cols, 0);
- SetNeedsDisplay ();
- return true;
- }
- int lastSelectedItem = -1;
- private bool allowsMultipleSelection = true;
- private System.Timers.Timer searchTimer;
- /// <summary>
- /// Invokes the <see cref="SelectedItemChanged"/> event if it is defined.
- /// </summary>
- /// <returns></returns>
- public virtual bool OnSelectedChanged ()
- {
- if (selected != lastSelectedItem) {
- var value = source?.Count > 0 ? source.ToList () [selected] : null;
- SelectedItemChanged?.Invoke (new ListViewItemEventArgs (selected, value));
- if (HasFocus) {
- lastSelectedItem = selected;
- }
- return true;
- }
- return false;
- }
- /// <summary>
- /// Invokes the <see cref="OpenSelectedItem"/> event if it is defined.
- /// </summary>
- /// <returns></returns>
- public virtual bool OnOpenSelectedItem ()
- {
- if (source.Count <= selected || selected < 0 || OpenSelectedItem == null) {
- return false;
- }
- var value = source.ToList () [selected];
- OpenSelectedItem?.Invoke (new ListViewItemEventArgs (selected, value));
- return true;
- }
- /// <summary>
- /// Virtual method that will invoke the <see cref="RowRender"/>.
- /// </summary>
- /// <param name="rowEventArgs"></param>
- public virtual void OnRowRender (ListViewRowEventArgs rowEventArgs)
- {
- RowRender?.Invoke (rowEventArgs);
- }
- ///<inheritdoc/>
- public override bool OnEnter (View view)
- {
- Application.Driver.SetCursorVisibility (CursorVisibility.Invisible);
- if (lastSelectedItem == -1) {
- EnsuresVisibilitySelectedItem ();
- OnSelectedChanged ();
- }
- return base.OnEnter (view);
- }
- ///<inheritdoc/>
- public override bool OnLeave (View view)
- {
- if (lastSelectedItem > -1) {
- lastSelectedItem = -1;
- }
- return base.OnLeave (view);
- }
- void EnsuresVisibilitySelectedItem ()
- {
- SuperView?.LayoutSubviews ();
- if (selected < top) {
- top = selected;
- } else if (Frame.Height > 0 && selected >= top + Frame.Height) {
- top = Math.Max (selected - Frame.Height + 1, 0);
- }
- }
- ///<inheritdoc/>
- public override void PositionCursor ()
- {
- if (allowsMarking)
- Move (0, selected - top);
- else
- Move (Bounds.Width - 1, selected - top);
- }
- ///<inheritdoc/>
- 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;
- }
- }
- /// <inheritdoc/>
- public class ListWrapper : IListDataSourceSearchable {
- IList src;
- BitArray marks;
- int count, len;
- /// <inheritdoc/>
- public ListWrapper (IList source)
- {
- if (source != null) {
- count = source.Count;
- marks = new BitArray (count);
- src = source;
- len = GetMaxLengthItem ();
- }
- }
- /// <inheritdoc/>
- public int Count => src != null ? src.Count : 0;
- /// <inheritdoc/>
- 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 = u.RuneCount;
- } 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)
- {
- int byteLen = ustr.Length;
- int used = 0;
- for (int i = start; i < byteLen;) {
- (var rune, var size) = Utf8.DecodeRune (ustr, i, i - byteLen);
- var count = Rune.ColumnWidth (rune);
- if (used + count > width)
- break;
- driver.AddRune (rune);
- used += count;
- i += size;
- }
- for (; used < width; used++) {
- driver.AddRune (' ');
- }
- }
- /// <inheritdoc/>
- 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);
- }
- }
- }
- /// <inheritdoc/>
- public bool IsMarked (int item)
- {
- if (item >= 0 && item < count)
- return marks [item];
- return false;
- }
- /// <inheritdoc/>
- public void SetMark (int item, bool value)
- {
- if (item >= 0 && item < count)
- marks [item] = value;
- }
- /// <inheritdoc/>
- public IList ToList ()
- {
- return src;
- }
- /// <inheritdoc/>
- 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.ToUpperInvariant().StartsWith (search.ToUpperInvariant())) {
- return i;
- }
- }
- }
- return -1;
- }
- }
- /// <summary>
- /// <see cref="EventArgs"/> for <see cref="ListView"/> events.
- /// </summary>
- public class ListViewItemEventArgs : EventArgs {
- /// <summary>
- /// The index of the <see cref="ListView"/> item.
- /// </summary>
- public int Item { get; }
- /// <summary>
- /// The <see cref="ListView"/> item.
- /// </summary>
- public object Value { get; }
- /// <summary>
- /// Initializes a new instance of <see cref="ListViewItemEventArgs"/>
- /// </summary>
- /// <param name="item">The index of the <see cref="ListView"/> item.</param>
- /// <param name="value">The <see cref="ListView"/> item</param>
- public ListViewItemEventArgs (int item, object value)
- {
- Item = item;
- Value = value;
- }
- }
- /// <summary>
- /// <see cref="EventArgs"/> used by the <see cref="ListView.RowRender"/> event.
- /// </summary>
- public class ListViewRowEventArgs : EventArgs {
- /// <summary>
- /// The current row being rendered.
- /// </summary>
- public int Row { get; }
- /// <summary>
- /// The <see cref="Attribute"/> used by current row or
- /// null to maintain the current attribute.
- /// </summary>
- public Attribute? RowAttribute { get; set; }
- /// <summary>
- /// Initializes with the current row.
- /// </summary>
- /// <param name="row"></param>
- public ListViewRowEventArgs (int row)
- {
- Row = row;
- }
- }
- }
|