From b88dbb672b43a46e83482ca9ff3b95a6d47be1f9 Mon Sep 17 00:00:00 2001 From: BDisp Date: Fri, 15 Jan 2021 01:45:40 +0000 Subject: [PATCH] Added right and left scrolling feature to the ListView. --- Terminal.Gui/Core/View.cs | 2 +- Terminal.Gui/Views/ListView.cs | 127 ++++++++++++++++--- Terminal.Gui/Views/ScrollBarView.cs | 3 + UICatalog/Scenarios/ListViewWithSelection.cs | 56 ++++++-- UICatalog/Scenarios/ListsAndCombos.cs | 35 +++++ UICatalog/UICatalog.cs | 37 +++++- 6 files changed, 226 insertions(+), 34 deletions(-) diff --git a/Terminal.Gui/Core/View.cs b/Terminal.Gui/Core/View.cs index 66d8377e8..b4ccd5f0d 100644 --- a/Terminal.Gui/Core/View.cs +++ b/Terminal.Gui/Core/View.cs @@ -1342,8 +1342,8 @@ namespace Terminal.Gui { // Draw the subview // Use the view's bounds (view-relative; Location will always be (0,0) if (view.Visible && view.Frame.Width > 0 && view.Frame.Height > 0) { - view.Redraw (view.Bounds); view.OnDrawContent (view.Bounds); + view.Redraw (view.Bounds); } } view.NeedDisplay = Rect.Empty; diff --git a/Terminal.Gui/Views/ListView.cs b/Terminal.Gui/Views/ListView.cs index 41b1ae856..c009146df 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -34,6 +34,11 @@ namespace Terminal.Gui { /// 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. /// @@ -45,10 +50,11 @@ namespace Terminal.Gui { /// 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); + 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. @@ -103,7 +109,7 @@ namespace Terminal.Gui { /// /// public class ListView : View { - int top; + int top, left; int selected; IListDataSource source; @@ -146,7 +152,7 @@ namespace Terminal.Gui { /// /// An item implementing the IList interface. /// - /// Use the property to set a new source and use custome rendering. + /// Use the property to set a new source and use custom rendering. /// public Task SetSourceAsync (IList source) { @@ -211,6 +217,28 @@ namespace Terminal.Gui { } } + /// + /// Gets or sets the left column where the item start to be displayed at on the . + /// + /// The left position. + public int LeftItem { + get => left; + set { + if (source == null) + return; + + if (left < 0 || top >= source.Count) + throw new ArgumentException ("value"); + left = value; + SetNeedsDisplay (); + } + } + + /// + /// Gets the widest item. + /// + public int Maxlength => (source?.Length) ?? 0; + /// /// Gets or sets the index of the currently selected item. /// @@ -229,7 +257,6 @@ namespace Terminal.Gui { } } - static IListDataSource MakeWrapper (IList source) { return new ListWrapper (source); @@ -301,6 +328,7 @@ namespace Terminal.Gui { 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; @@ -320,7 +348,7 @@ namespace Terminal.Gui { 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); + Source.Render (this, Driver, isSelected, item, col, row, f.Width - col, start); } } } @@ -572,6 +600,26 @@ namespace Terminal.Gui { SetNeedsDisplay (); } + /// + /// Scrolls the view right. + /// + /// Number of columns to scroll right. + public virtual void ScrollRight (int cols) + { + left = Math.Max (Math.Min (left + cols, Maxlength - 1), 0); + SetNeedsDisplay (); + } + + /// + /// Scrolls the view left. + /// + /// Number of columns to scroll left. + public virtual void ScrollLeft (int cols) + { + left = Math.Max (left - cols, 0); + SetNeedsDisplay (); + } + int lastSelectedItem = -1; private bool allowsMultipleSelection = true; @@ -639,7 +687,8 @@ namespace Terminal.Gui { 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.WheeledDown && me.Flags != MouseFlags.WheeledUp && + me.Flags != MouseFlags.WheeledRight && me.Flags != MouseFlags.WheeledLeft) return false; if (!HasFocus && CanFocus) { @@ -656,6 +705,12 @@ namespace Terminal.Gui { } 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) { @@ -687,7 +742,7 @@ namespace Terminal.Gui { public class ListWrapper : IListDataSource { IList src; BitArray marks; - int count; + int count, len; /// /// Initializes a new instance of given an @@ -698,7 +753,8 @@ namespace Terminal.Gui { if (source != null) { count = source.Count; marks = new BitArray (count); - this.src = source; + src = source; + len = GetMaxLengthItem (); } } @@ -707,11 +763,42 @@ namespace Terminal.Gui { /// public int Count => src != null ? src.Count : 0; - void RenderUstr (ConsoleDriver driver, ustring ustr, int col, int line, int width) + /// + /// Gets the maximum item length in the . + /// + public int Length => len; + + int GetMaxLengthItem () + { + if (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 = 0; i < byteLen;) { + 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) @@ -735,19 +822,21 @@ namespace Terminal.Gui { /// The col where to move. /// The line where to move. /// The item width. - public void Render (ListView container, ConsoleDriver driver, bool marked, int item, int col, int line, int width) + /// The index of the string to be displayed. + 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) { - RenderUstr (driver, (ustring)t, col, line, width); - } else if (t is string) { - RenderUstr (driver, (string)t, col, line, width); - } else - RenderUstr (driver, t.ToString (), col, line, width); + 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); + } } } @@ -793,14 +882,14 @@ namespace Terminal.Gui { /// public int Item { get; } /// - /// The the item. + /// The item. /// public object Value { get; } /// /// Initializes a new instance of /// - /// The index of the the item. + /// The index of the item. /// The item public ListViewItemEventArgs (int item, object value) { diff --git a/Terminal.Gui/Views/ScrollBarView.cs b/Terminal.Gui/Views/ScrollBarView.cs index 6163fbe9c..7cffd2a63 100644 --- a/Terminal.Gui/Views/ScrollBarView.cs +++ b/Terminal.Gui/Views/ScrollBarView.cs @@ -139,6 +139,8 @@ namespace Terminal.Gui { } else { position = Math.Max (position + max, 0); } + } else if (max < 0) { + position = Math.Max (position + max, 0); } OnChangedPosition (); SetNeedsDisplay (); @@ -173,6 +175,7 @@ namespace Terminal.Gui { Visible = true; } else { Visible = false; + Position = 0; } Width = vertical ? 1 : Dim.Width (Host); Height = vertical ? Dim.Height (Host) : 1; diff --git a/UICatalog/Scenarios/ListViewWithSelection.cs b/UICatalog/Scenarios/ListViewWithSelection.cs index dac0f619e..b729ee0c3 100644 --- a/UICatalog/Scenarios/ListViewWithSelection.cs +++ b/UICatalog/Scenarios/ListViewWithSelection.cs @@ -25,7 +25,7 @@ namespace UICatalog { Height = 1, }; Win.Add (_customRenderCB); - _customRenderCB.Toggled += _customRenderCB_Toggled; ; + _customRenderCB.Toggled += _customRenderCB_Toggled; _allowMarkingCB = new CheckBox ("Allow Marking") { X = Pos.Right (_customRenderCB) + 1, @@ -56,6 +56,9 @@ namespace UICatalog { Win.Add (_listView); var vertical = new ScrollBarView (_listView, true); + var horizontal = new ScrollBarView (_listView, false); + vertical.OtherScrollBarView = horizontal; + horizontal.OtherScrollBarView = vertical; vertical.ChangedPosition += () => { _listView.TopItem = vertical.Position; @@ -65,13 +68,26 @@ namespace UICatalog { _listView.SetNeedsDisplay (); }; + horizontal.ChangedPosition += () => { + _listView.LeftItem = horizontal.Position; + if (_listView.LeftItem != horizontal.Position) { + horizontal.Position = _listView.LeftItem; + } + _listView.SetNeedsDisplay (); + }; + _listView.DrawContent += (e) => { - vertical.Size = _listView.Source.Count; + vertical.Size = _listView.Source.Count - 1; vertical.Position = _listView.TopItem; - vertical.ColorScheme = _listView.ColorScheme; + horizontal.Size = _listView.Maxlength; + horizontal.Position = _listView.LeftItem; + vertical.ColorScheme = horizontal.ColorScheme = _listView.ColorScheme; if (vertical.ShowScrollIndicator) { vertical.Redraw (e); } + if (horizontal.ShowScrollIndicator) { + horizontal.Redraw (e); + } }; _listView.SetSource (_scenarios); @@ -114,15 +130,16 @@ namespace UICatalog { int _nameColumnWidth = 30; private List scenarios; BitArray marks; - int count; + int count, len; public List Scenarios { - get => scenarios; + get => scenarios; set { if (value != null) { count = value.Count; marks = new BitArray (count); scenarios = value; + len = GetMaxLengthItem (); } } } @@ -135,14 +152,16 @@ namespace UICatalog { public int Count => Scenarios != null ? Scenarios.Count : 0; + public int Length => len; + public ScenarioListDataSource (List itemList) => Scenarios = itemList; - public void Render (ListView container, ConsoleDriver driver, bool selected, int item, int col, int line, int width) + public void Render (ListView container, ConsoleDriver driver, bool selected, int item, int col, int line, int width, int start = 0) { container.Move (col, line); // Equivalent to an interpolated string like $"{Scenarios[item].Name, -widtestname}"; if such a thing were possible var s = String.Format (String.Format ("{{0,{0}}}", -_nameColumnWidth), Scenario.ScenarioMetadata.GetName (Scenarios [item])); - RenderUstr (driver, $"{s} {Scenario.ScenarioMetadata.GetDescription (Scenarios [item])}", col, line, width); + RenderUstr (driver, $"{s} {Scenario.ScenarioMetadata.GetDescription (Scenarios [item])}", col, line, width, start); } public void SetMark (int item, bool value) @@ -151,11 +170,30 @@ namespace UICatalog { marks [item] = value; } + int GetMaxLengthItem () + { + if (scenarios?.Count == 0) { + return 0; + } + + int maxLength = 0; + for (int i = 0; i < scenarios.Count; i++) { + var s = String.Format (String.Format ("{{0,{0}}}", -_nameColumnWidth), Scenario.ScenarioMetadata.GetName (Scenarios [i])); + var sc = $"{s} {Scenario.ScenarioMetadata.GetDescription (Scenarios [i])}"; + var l = sc.Length; + if (l > maxLength) { + maxLength = l; + } + } + + return maxLength; + } + // A slightly adapted method from: https://github.com/migueldeicaza/gui.cs/blob/fc1faba7452ccbdf49028ac49f0c9f0f42bbae91/Terminal.Gui/Views/ListView.cs#L433-L461 - private void RenderUstr (ConsoleDriver driver, ustring ustr, int col, int line, int width) + private void RenderUstr (ConsoleDriver driver, ustring ustr, int col, int line, int width, int start = 0) { int used = 0; - int index = 0; + int index = start; while (index < ustr.Length) { (var rune, var size) = Utf8.DecodeRune (ustr, index, index - ustr.Length); var count = Rune.ColumnWidth (rune); diff --git a/UICatalog/Scenarios/ListsAndCombos.cs b/UICatalog/Scenarios/ListsAndCombos.cs index 910d3496d..dda51619b 100644 --- a/UICatalog/Scenarios/ListsAndCombos.cs +++ b/UICatalog/Scenarios/ListsAndCombos.cs @@ -39,6 +39,41 @@ namespace UICatalog.Scenarios { listview.SelectedItemChanged += (ListViewItemEventArgs e) => lbListView.Text = items [listview.SelectedItem]; Win.Add (lbListView, listview); + var vertical = new ScrollBarView (listview, true); + var horizontal = new ScrollBarView (listview, false); + vertical.OtherScrollBarView = horizontal; + horizontal.OtherScrollBarView = vertical; + + vertical.ChangedPosition += () => { + listview.TopItem = vertical.Position; + if (listview.TopItem != vertical.Position) { + vertical.Position = listview.TopItem; + } + listview.SetNeedsDisplay (); + }; + + horizontal.ChangedPosition += () => { + listview.LeftItem = horizontal.Position; + if (listview.LeftItem != horizontal.Position) { + horizontal.Position = listview.LeftItem; + } + listview.SetNeedsDisplay (); + }; + + listview.DrawContent += (e) => { + vertical.Size = listview.Source.Count - 1; + vertical.Position = listview.TopItem; + horizontal.Size = listview.Maxlength; + horizontal.Position = listview.LeftItem; + vertical.ColorScheme = horizontal.ColorScheme = listview.ColorScheme; + if (vertical.ShowScrollIndicator) { + vertical.Redraw (e); + } + if (horizontal.ShowScrollIndicator) { + horizontal.Redraw (e); + } + }; + // ComboBox var lbComboBox = new Label ("ComboBox") { ColorScheme = Colors.TopLevel, diff --git a/UICatalog/UICatalog.cs b/UICatalog/UICatalog.cs index d71088b65..12b37bf18 100644 --- a/UICatalog/UICatalog.cs +++ b/UICatalog/UICatalog.cs @@ -498,31 +498,58 @@ namespace UICatalog { } internal class ScenarioListDataSource : IListDataSource { + private readonly int len; + public List Scenarios { get; set; } public bool IsMarked (int item) => false; public int Count => Scenarios.Count; - public ScenarioListDataSource (List itemList) => Scenarios = itemList; + public int Length => len; - public void Render (ListView container, ConsoleDriver driver, bool selected, int item, int col, int line, int width) + public ScenarioListDataSource (List itemList) + { + Scenarios = itemList; + len = GetMaxLengthItem (); + } + + public void Render (ListView container, ConsoleDriver driver, bool selected, int item, int col, int line, int width, int start = 0) { container.Move (col, line); // Equivalent to an interpolated string like $"{Scenarios[item].Name, -widtestname}"; if such a thing were possible var s = String.Format (String.Format ("{{0,{0}}}", -_nameColumnWidth), Scenario.ScenarioMetadata.GetName (Scenarios [item])); - RenderUstr (driver, $"{s} {Scenario.ScenarioMetadata.GetDescription (Scenarios [item])}", col, line, width); + RenderUstr (driver, $"{s} {Scenario.ScenarioMetadata.GetDescription (Scenarios [item])}", col, line, width, start); } public void SetMark (int item, bool value) { } + int GetMaxLengthItem () + { + if (Scenarios?.Count == 0) { + return 0; + } + + int maxLength = 0; + for (int i = 0; i < Scenarios.Count; i++) { + var s = String.Format (String.Format ("{{0,{0}}}", -_nameColumnWidth), Scenario.ScenarioMetadata.GetName (Scenarios [i])); + var sc = $"{s} {Scenario.ScenarioMetadata.GetDescription (Scenarios [i])}"; + var l = sc.Length; + if (l > maxLength) { + maxLength = l; + } + } + + return maxLength; + } + // A slightly adapted method from: https://github.com/migueldeicaza/gui.cs/blob/fc1faba7452ccbdf49028ac49f0c9f0f42bbae91/Terminal.Gui/Views/ListView.cs#L433-L461 - private void RenderUstr (ConsoleDriver driver, ustring ustr, int col, int line, int width) + private void RenderUstr (ConsoleDriver driver, ustring ustr, int col, int line, int width, int start = 0) { int used = 0; - int index = 0; + int index = start; while (index < ustr.Length) { (var rune, var size) = Utf8.DecodeRune (ustr, index, index - ustr.Length); var count = Rune.ColumnWidth (rune);