From ac58a77b9d849d22ff96aa090cd4e3df55be1931 Mon Sep 17 00:00:00 2001 From: Charlie Kindel Date: Sat, 22 Oct 2022 18:45:15 -0600 Subject: [PATCH 01/25] Enables sarching ListView with keyboard --- Terminal.Gui/Views/ListView.cs | 226 ++++++++++++++++++--------------- 1 file changed, 127 insertions(+), 99 deletions(-) diff --git a/Terminal.Gui/Views/ListView.cs b/Terminal.Gui/Views/ListView.cs index 1a37fea3af..61ed818c71 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -1,22 +1,3 @@ -// -// ListView.cs: ListView control -// -// Authors: -// Miguel de Icaza (miguel@gnome.org) -// -// -// TODO: -// - Should we support multiple columns, if so, how should that be done? -// - Show mark for items that have been marked. -// - Mouse support -// - Scrollbars? -// -// Column considerations: -// - Would need a way to specify widths -// - Should it automatically extract data out of structs/classes based on public fields/properties? -// - It seems that this would be useful just for the "simple" API, not the IListDAtaSource, as that one has full support for it. -// - Should a function be specified that retrieves the individual elements? -// using System; using System.Collections; using System.Collections.Generic; @@ -59,7 +40,7 @@ public interface IListDataSource { /// /// Should return whether the specified item is currently marked. /// - /// true, if marked, false otherwise. + /// , if marked, otherwise. /// Item index. bool IsMarked (int item); @@ -67,7 +48,7 @@ public interface IListDataSource { /// Flags the item as marked. /// /// Item index. - /// If set to true value. + /// If set to value. void SetMark (int item, bool value); /// @@ -77,6 +58,21 @@ public interface IListDataSource { IList ToList (); } + /// + /// Implement to provide custom rendering for a that + /// supports searching for items. + /// + public interface IListDataSourceSearchable : IListDataSource { + /// + /// 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. + /// + /// Text to search for. + /// The index of the first item that starts with . + /// Returns if was not found. + int StartsWith (string search); + } + /// /// ListView renders a scrollable list of data where each item can be activated to perform an action. /// @@ -89,8 +85,8 @@ public interface IListDataSource { /// /// By default uses to render the items of any /// object (e.g. arrays, , - /// and other collections). Alternatively, an object that implements the - /// interface can be provided giving full control of what is rendered. + /// and other collections). Alternatively, an object that implements + /// or can be provided giving full control of what is rendered. /// /// /// can display any object that implements the interface. @@ -107,6 +103,11 @@ public interface IListDataSource { /// [x] or [ ] and bind the SPACE key to toggle the selection. To implement a different /// marking style set to false and implement custom rendering. /// + /// + /// By default or if is set to an object that implements + /// , 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; @@ -169,11 +170,10 @@ public Task SetSourceAsync (IList source) /// /// Gets or sets whether this allows items to be marked. /// - /// true if allows marking elements of the list; otherwise, false. - /// + /// Set to to allow marking elements of the list. /// - /// If set to true, will render items marked items with "[x]", and unmarked items with "[ ]" - /// spaces. SPACE key will toggle marking. + /// 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; @@ -184,7 +184,8 @@ public bool AllowsMarking { } /// - /// If set to true allows more than one item to be selected. If false only allow one item selected. + /// 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; @@ -219,7 +220,7 @@ public int TopItem { } /// - /// Gets or sets the left column where the item start to be displayed at on the . + /// Gets or sets the leftmost column that is currently visible (when scrolling horizontally). /// /// The left position. public int LeftItem { @@ -236,7 +237,7 @@ public int LeftItem { } /// - /// Gets the widest item. + /// Gets the widest item in the list. /// public int Maxlength => (source?.Length) ?? 0; @@ -264,10 +265,12 @@ static IListDataSource MakeWrapper (IList source) } /// - /// Initializes a new instance of that will display the contents of the object implementing the interface, + /// 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. + /// 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)) { } @@ -296,7 +299,8 @@ public ListView () : base () /// 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. + /// 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 (); @@ -306,7 +310,9 @@ public ListView (Rect rect, IList source) : this (rect, MakeWrapper (source)) /// 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. + /// 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; @@ -331,13 +337,13 @@ void Initialize () AddCommand (Command.ToggleChecked, () => MarkUnmarkRow ()); // Default keybindings for all ListViews - AddKeyBinding (Key.CursorUp,Command.LineUp); + 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.PageUp, Command.PageUp); AddKeyBinding (Key.PageDown, Command.PageDown); AddKeyBinding (Key.V | Key.CtrlMask, Command.PageDown); @@ -386,7 +392,8 @@ public override void Redraw (Rect bounds) Driver.SetAttribute (current); } if (allowsMarking) { - Driver.AddRune (source.IsMarked (item) ? (AllowsMultipleSelection ? Driver.Checked : Driver.Selected) : (AllowsMultipleSelection ? Driver.UnChecked : Driver.UnSelected)); + 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); @@ -409,6 +416,8 @@ public override void Redraw (Rect bounds) /// public event Action RowRender; + private string search { get; set; } + /// public override bool ProcessKey (KeyEvent kb) { @@ -419,13 +428,37 @@ public override bool ProcessKey (KeyEvent kb) if (result != null) return (bool)result; + // Enable user to find & select an item by typing text + if (source is IListDataSourceSearchable && + !(kb.IsCapslock && kb.IsCtrl && kb.IsAlt && kb.IsScrolllock && kb.IsNumlock && kb.IsCapslock)) { + if (kb.KeyValue >= 32 && kb.KeyValue < 127) { + if (searchTimer == null) { + searchTimer = new System.Timers.Timer (500); + searchTimer.Elapsed += (o, e) => { + searchTimer.Stop (); + searchTimer = null; + search = ""; + }; + searchTimer.Start (); + } + search += (char)kb.KeyValue; + var found = ((IListDataSourceSearchable)source).StartsWith (search); + if (found != -1) { + SelectedItem = found; + SetNeedsDisplay (); + } + return true; + } + } + return false; } /// - /// Prevents marking if it's not allowed mark and if it's not allows multiple selection. + /// If and are both , + /// unmarks all marked items other than the currently selected. /// - /// + /// if unmarking was successful. public virtual bool AllowsAll () { if (!allowsMarking) @@ -442,9 +475,9 @@ public virtual bool AllowsAll () } /// - /// Marks an unmarked row. + /// Marks the if it is not already marked. /// - /// + /// if the was marked. public virtual bool MarkUnmarkRow () { if (AllowsAll ()) { @@ -457,7 +490,7 @@ public virtual bool MarkUnmarkRow () } /// - /// Moves the selected item index to the next page. + /// Changes the to the item at the top of the visible list. /// /// public virtual bool MovePageUp () @@ -476,7 +509,8 @@ public virtual bool MovePageUp () } /// - /// Moves the selected item index to the previous page. + /// Changes the to the item just below the bottom + /// of the visible list, scrolling if needed. /// /// public virtual bool MovePageDown () @@ -498,7 +532,8 @@ public virtual bool MovePageDown () } /// - /// Moves the selected item index to the next row. + /// Changes the to the next item in the list, + /// scrolling the list if needed. /// /// public virtual bool MoveDown () @@ -538,7 +573,8 @@ public virtual bool MoveDown () } /// - /// Moves the selected item index to the previous row. + /// Changes the to the previous item in the list, + /// scrolling the list if needed. /// /// public virtual bool MoveUp () @@ -574,7 +610,8 @@ public virtual bool MoveUp () } /// - /// Moves the selected item index to the last row. + /// Changes the to last item in the list, + /// scrolling the list if needed. /// /// public virtual bool MoveEnd () @@ -592,7 +629,8 @@ public virtual bool MoveEnd () } /// - /// Moves the selected item index to the first row. + /// Changes the to the first item in the list, + /// scrolling the list if needed. /// /// public virtual bool MoveHome () @@ -608,23 +646,23 @@ public virtual bool MoveHome () } /// - /// Scrolls the view down. + /// Scrolls the view down by items. /// - /// Number of lines to scroll down. - public virtual bool ScrollDown (int lines) + /// Number of items to scroll down. + public virtual bool ScrollDown (int items) { - top = Math.Max (Math.Min (top + lines, source.Count - 1), 0); + top = Math.Max (Math.Min (top + items, source.Count - 1), 0); SetNeedsDisplay (); return true; } /// - /// Scrolls the view up. + /// Scrolls the view up by items. /// - /// Number of lines to scroll up. - public virtual bool ScrollUp (int lines) + /// Number of items to scroll up. + public virtual bool ScrollUp (int items) { - top = Math.Max (top - lines, 0); + top = Math.Max (top - items, 0); SetNeedsDisplay (); return true; } @@ -653,9 +691,10 @@ public virtual bool ScrollLeft (int cols) int lastSelectedItem = -1; private bool allowsMultipleSelection = true; + private System.Timers.Timer searchTimer; /// - /// Invokes the SelectedChanged event if it is defined. + /// Invokes the event if it is defined. /// /// public virtual bool OnSelectedChanged () @@ -673,7 +712,7 @@ public virtual bool OnSelectedChanged () } /// - /// Invokes the OnOpenSelectedItem event if it is defined. + /// Invokes the event if it is defined. /// /// public virtual bool OnOpenSelectedItem () @@ -788,23 +827,15 @@ public override bool MouseEvent (MouseEvent me) return true; } - - } - /// - /// Implements an that renders arbitrary instances for . - /// - /// Implements support for rendering marked items. - public class ListWrapper : IListDataSource { + /// + public class ListWrapper : IListDataSourceSearchable { IList src; BitArray marks; int count, len; - /// - /// Initializes a new instance of given an - /// - /// + /// public ListWrapper (IList source) { if (source != null) { @@ -815,14 +846,10 @@ public ListWrapper (IList source) } } - /// - /// Gets the number of items in the . - /// + /// public int Count => src != null ? src.Count : 0; - /// - /// Gets the maximum item length in the . - /// + /// public int Length => len; int GetMaxLengthItem () @@ -869,17 +896,7 @@ void RenderUstr (ConsoleDriver driver, ustring ustr, int col, int line, int widt } } - /// - /// Renders a item to the appropriate type. - /// - /// The ListView. - /// The driver used by the caller. - /// Informs if it's marked or not. - /// The item. - /// The col where to move. - /// The line where to move. - /// The item 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); @@ -897,11 +914,7 @@ public void Render (ListView container, ConsoleDriver driver, bool marked, int i } } - /// - /// Returns true if the item is marked, false otherwise. - /// - /// The item. - /// trueIf is marked.falseotherwise. + /// public bool IsMarked (int item) { if (item >= 0 && item < count) @@ -909,25 +922,40 @@ public bool IsMarked (int item) return false; } - /// - /// Sets the item as marked or unmarked based on the value is true or false, respectively. - /// - /// The item - /// Marks the item.Unmarked the item.The value. + /// public void SetMark (int item, bool value) { if (item >= 0 && item < count) marks [item] = value; } - /// - /// Returns the source as IList. - /// - /// + /// 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.ToUpperInvariant().StartsWith (search.ToUpperInvariant())) { + return i; + } + } + } + return -1; + } } /// From 7c8180d863398da26b9ae4aca381349df1cb40c8 Mon Sep 17 00:00:00 2001 From: tznind Date: Mon, 24 Oct 2022 16:46:24 +0100 Subject: [PATCH 02/25] Add SearchCollectionNavigator --- .../Core/SearchCollectionNavigator.cs | 121 +++++++++++++++++ UnitTests/SearchCollectionNavigatorTests.cs | 126 ++++++++++++++++++ 2 files changed, 247 insertions(+) create mode 100644 Terminal.Gui/Core/SearchCollectionNavigator.cs create mode 100644 UnitTests/SearchCollectionNavigatorTests.cs diff --git a/Terminal.Gui/Core/SearchCollectionNavigator.cs b/Terminal.Gui/Core/SearchCollectionNavigator.cs new file mode 100644 index 0000000000..47d62b6613 --- /dev/null +++ b/Terminal.Gui/Core/SearchCollectionNavigator.cs @@ -0,0 +1,121 @@ +using System; +using System.Linq; + +namespace Terminal.Gui { + /// + /// Changes the index in a collection based on keys pressed + /// and the current state + /// + class SearchCollectionNavigator { + string state = ""; + DateTime lastKeystroke = DateTime.MinValue; + const int TypingDelay = 250; + public StringComparer Comparer { get; set; } = StringComparer.InvariantCultureIgnoreCase; + + public int CalculateNewIndex (string [] collection, int currentIndex, char keyStruck) + { + // if user presses a letter key + if (char.IsLetterOrDigit (keyStruck) || char.IsPunctuation (keyStruck)) { + + // maybe user pressed 'd' and now presses 'd' again. + // a candidate search is things that begin with "dd" + // but if we find none then we must fallback on cycling + // d instead and discard the candidate state + string candidateState = ""; + + // is it a second or third (etc) keystroke within a short time + if (state.Length > 0 && DateTime.Now - lastKeystroke < TimeSpan.FromMilliseconds (TypingDelay)) { + // "dd" is a candidate + candidateState = state + keyStruck; + } else { + // its a fresh keystroke after some time + // or its first ever key press + state = new string (keyStruck, 1); + } + + var idxCandidate = GetNextIndexMatching (collection, currentIndex, candidateState, + // prefer not to move if there are multiple characters e.g. "ca" + 'r' should stay on "car" and not jump to "cart" + candidateState.Length > 1); + + if (idxCandidate != -1) { + // found "dd" so candidate state is accepted + lastKeystroke = DateTime.Now; + state = candidateState; + return idxCandidate; + } + + + // nothing matches "dd" so discard it as a candidate + // and just cycle "d" instead + lastKeystroke = DateTime.Now; + idxCandidate = GetNextIndexMatching (collection, currentIndex, state); + + // if no changes to current state manifested + if (idxCandidate == currentIndex || idxCandidate == -1) { + // clear history and treat as a fresh letter + ClearState (); + + // match on the fresh letter alone + state = new string (keyStruck, 1); + idxCandidate = GetNextIndexMatching (collection, currentIndex, state); + return idxCandidate == -1 ? currentIndex : idxCandidate; + } + + // Found another "d" or just leave index as it was + return idxCandidate; + + } else { + // clear state because keypress was non letter + ClearState (); + + // no change in index for non letter keystrokes + return currentIndex; + } + } + + private int GetNextIndexMatching (string [] collection, int currentIndex, string search, bool preferNotToMoveToNewIndexes = false) + { + if (string.IsNullOrEmpty (search)) { + return -1; + } + + // find indexes of items that start with the search text + int [] matchingIndexes = collection.Select ((item, idx) => (item, idx)) + .Where (k => k.Item1?.StartsWith (search, StringComparison.InvariantCultureIgnoreCase) ?? false) + .Select (k => k.idx) + .ToArray (); + + // if there are items beginning with search + if (matchingIndexes.Length > 0) { + // is one of them currently selected? + var currentlySelected = Array.IndexOf (matchingIndexes, currentIndex); + + if (currentlySelected == -1) { + // we are not currently selecting any item beginning with the search + // so jump to first item in list that begins with the letter + return matchingIndexes [0]; + } else { + + // the current index is part of the matching collection + if (preferNotToMoveToNewIndexes) { + // if we would rather not jump around (e.g. user is typing lots of text to get this match) + return matchingIndexes [currentlySelected]; + } + + // cycle to next (circular) + return matchingIndexes [(currentlySelected + 1) % matchingIndexes.Length]; + } + } + + // nothing starts with the search + return -1; + } + + private void ClearState () + { + state = ""; + lastKeystroke = DateTime.MinValue; + + } + } +} diff --git a/UnitTests/SearchCollectionNavigatorTests.cs b/UnitTests/SearchCollectionNavigatorTests.cs new file mode 100644 index 0000000000..ac39b88642 --- /dev/null +++ b/UnitTests/SearchCollectionNavigatorTests.cs @@ -0,0 +1,126 @@ +using Terminal.Gui; +using Xunit; + +namespace UnitTests { + public class SearchCollectionNavigatorTests { + + [Fact] + public void TestSearchCollectionNavigator_Cycling () + { + var s = new string []{ + "appricot", + "arm", + "bat", + "batman", + "candle" + }; + + var n = new SearchCollectionNavigator (); + Assert.Equal (2, n.CalculateNewIndex (s, 0, 'b')); + Assert.Equal (3, n.CalculateNewIndex (s, 2, 'b')); + + // if 4 (candle) is selected it should loop back to bat + Assert.Equal (2, n.CalculateNewIndex (s, 4, 'b')); + + } + + + [Fact] + public void TestSearchCollectionNavigator_ToSearchText () + { + var s = new string []{ + "appricot", + "arm", + "bat", + "batman", + "bbfish", + "candle" + }; + + var n = new SearchCollectionNavigator (); + Assert.Equal (2, n.CalculateNewIndex (s, 0, 'b')); + Assert.Equal (4, n.CalculateNewIndex (s, 2, 'b')); + + // another 'b' means searching for "bbb" which does not exist + // so we go back to looking for "b" as a fresh key strike + Assert.Equal (4, n.CalculateNewIndex (s, 2, 'b')); + } + + [Fact] + public void TestSearchCollectionNavigator_FullText () + { + var s = new string []{ + "appricot", + "arm", + "ta", + "target", + "text", + "egg", + "candle" + }; + + var n = new SearchCollectionNavigator (); + Assert.Equal (2, n.CalculateNewIndex (s, 0, 't')); + + // should match "te" in "text" + Assert.Equal (4, n.CalculateNewIndex (s, 2, 'e')); + + // still matches text + Assert.Equal (4, n.CalculateNewIndex (s, 4, 'x')); + + // nothing starts texa so it jumps to a for appricot + Assert.Equal (0, n.CalculateNewIndex (s, 4, 'a')); + } + + [Fact] + public void TestSearchCollectionNavigator_Unicode () + { + var s = new string []{ + "appricot", + "arm", + "ta", + "丗丙业丞", + "丗丙丛", + "text", + "egg", + "candle" + }; + + var n = new SearchCollectionNavigator (); + Assert.Equal (3, n.CalculateNewIndex (s, 0, '丗')); + + // 丗丙业丞 is as good a match as 丗丙丛 + // so when doing multi character searches we should + // prefer to stay on the same index unless we invalidate + // our typed text + Assert.Equal (3, n.CalculateNewIndex (s, 3, '丙')); + + // No longer matches 丗丙业丞 and now only matches 丗丙丛 + // so we should move to the new match + Assert.Equal (4, n.CalculateNewIndex (s, 3, '丛')); + + // nothing starts "丗丙丛a" so it jumps to a for appricot + Assert.Equal (0, n.CalculateNewIndex (s, 4, 'a')); + } + + [Fact] + public void TestSearchCollectionNavigator_AtSymbol () + { + var s = new string []{ + "appricot", + "arm", + "ta", + "@bob", + "@bb", + "text", + "egg", + "candle" + }; + + var n = new SearchCollectionNavigator (); + Assert.Equal (3, n.CalculateNewIndex (s, 0, '@')); + Assert.Equal (3, n.CalculateNewIndex (s, 3, 'b')); + Assert.Equal (4, n.CalculateNewIndex (s, 3, 'b')); + } + } +} From 18ec9a2a70a0586bd2d136fe5de36b18c3e3fee1 Mon Sep 17 00:00:00 2001 From: Tig Kindel Date: Mon, 24 Oct 2022 18:56:06 -0600 Subject: [PATCH 03/25] integrated tznind's stuff --- Terminal.Gui/Core/Command.cs | 82 +++---- .../Core/SearchCollectionNavigator.cs | 20 +- Terminal.Gui/Views/ListView.cs | 44 ++-- UICatalog/Properties/launchSettings.json | 4 + .../SearchCollectionNavigatorTester.cs | 218 ++++++++++++++++++ UnitTests/SearchCollectionNavigatorTests.cs | 56 ++--- 6 files changed, 327 insertions(+), 97 deletions(-) create mode 100644 UICatalog/Scenarios/SearchCollectionNavigatorTester.cs diff --git a/Terminal.Gui/Core/Command.cs b/Terminal.Gui/Core/Command.cs index 42f0d0f1e8..9d106f6643 100644 --- a/Terminal.Gui/Core/Command.cs +++ b/Terminal.Gui/Core/Command.cs @@ -10,54 +10,54 @@ namespace Terminal.Gui { public enum Command { /// - /// Moves the caret down one line. + /// Moves down one item (cell, line, etc...). /// LineDown, /// - /// Extends the selection down one line. + /// Extends the selection down one (cell, line, etc...). /// LineDownExtend, /// - /// Moves the caret down to the last child node of the branch that holds the current selection + /// Moves down to the last child node of the branch that holds the current selection. /// LineDownToLastBranch, /// - /// Scrolls down one line (without changing the selection). + /// Scrolls down one (cell, line, etc...) (without changing the selection). /// ScrollDown, // -------------------------------------------------------------------- /// - /// Moves the caret up one line. + /// Moves up one (cell, line, etc...). /// LineUp, /// - /// Extends the selection up one line. + /// Extends the selection up one item (cell, line, etc...). /// LineUpExtend, /// - /// Moves the caret up to the first child node of the branch that holds the current selection + /// Moves up to the first child node of the branch that holds the current selection. /// LineUpToFirstBranch, /// - /// Scrolls up one line (without changing the selection). + /// Scrolls up one item (cell, line, etc...) (without changing the selection). /// ScrollUp, /// - /// Moves the selection left one by the minimum increment supported by the view e.g. single character, cell, item etc. + /// Moves the selection left one by the minimum increment supported by the e.g. single character, cell, item etc. /// Left, /// - /// Scrolls one character to the left + /// Scrolls one item (cell, character, etc...) to the left /// ScrollLeft, @@ -72,7 +72,7 @@ public enum Command { Right, /// - /// Scrolls one character to the right. + /// Scrolls one item (cell, character, etc...) to the right. /// ScrollRight, @@ -102,12 +102,12 @@ public enum Command { WordRightExtend, /// - /// Deletes and copies to the clipboard the characters from the current position to the end of the line. + /// Cuts to the clipboard the characters from the current position to the end of the line. /// CutToEndLine, /// - /// Deletes and copies to the clipboard the characters from the current position to the start of the line. + /// Cuts to the clipboard the characters from the current position to the start of the line. /// CutToStartLine, @@ -140,47 +140,47 @@ public enum Command { DisableOverwrite, /// - /// Move the page down. + /// Move one page down. /// PageDown, /// - /// Move the page down increase selection area to cover revealed objects/characters. + /// Move one page page extending the selection to cover revealed objects/characters. /// PageDownExtend, /// - /// Move the page up. + /// Move one page up. /// PageUp, /// - /// Move the page up increase selection area to cover revealed objects/characters. + /// Move one page up extending the selection to cover revealed objects/characters. /// PageUpExtend, /// - /// Moves to top begin. + /// Moves to the top/home. /// TopHome, /// - /// Extends the selection to the top begin. + /// Extends the selection to the top/home. /// TopHomeExtend, /// - /// Moves to bottom end. + /// Moves to the bottom/end. /// BottomEnd, /// - /// Extends the selection to the bottom end. + /// Extends the selection to the bottom/end. /// BottomEndExtend, /// - /// Open selected item. + /// Open the selected item. /// OpenSelectedItem, @@ -190,43 +190,43 @@ public enum Command { ToggleChecked, /// - /// Accepts the current state (e.g. selection, button press etc) + /// Accepts the current state (e.g. selection, button press etc). /// Accept, /// - /// Toggles the Expanded or collapsed state of a a list or item (with subitems) + /// Toggles the Expanded or collapsed state of a a list or item (with subitems). /// ToggleExpandCollapse, /// - /// Expands a list or item (with subitems) + /// Expands a list or item (with subitems). /// Expand, /// - /// Recursively Expands all child items and their child items (if any) + /// Recursively Expands all child items and their child items (if any). /// ExpandAll, /// - /// Collapses a list or item (with subitems) + /// Collapses a list or item (with subitems). /// Collapse, /// - /// Recursively collapses a list items of their children (if any) + /// Recursively collapses a list items of their children (if any). /// CollapseAll, /// - /// Cancels any current temporary states on the control e.g. expanding - /// a combo list + /// Cancels an action or any temporary states on the control e.g. expanding + /// a combo list. /// Cancel, /// - /// Unix emulation + /// Unix emulation. /// UnixEmulation, @@ -241,12 +241,12 @@ public enum Command { DeleteCharLeft, /// - /// Selects all objects in the control. + /// Selects all objects. /// SelectAll, /// - /// Deletes all objects in the control. + /// Deletes all objects. /// DeleteAll, @@ -336,7 +336,7 @@ public enum Command { Paste, /// - /// Quit a toplevel. + /// Quit a . /// QuitToplevel, @@ -356,37 +356,37 @@ public enum Command { PreviousView, /// - /// Moves focus to the next view or toplevel (case of Mdi). + /// Moves focus to the next view or toplevel (case of MDI). /// NextViewOrTop, /// - /// Moves focus to the next previous or toplevel (case of Mdi). + /// Moves focus to the next previous or toplevel (case of MDI). /// PreviousViewOrTop, /// - /// Refresh the application. + /// Refresh. /// Refresh, /// - /// Toggles the extended selection. + /// Toggles the selection. /// ToggleExtend, /// - /// Inserts a new line. + /// Inserts a new item. /// NewLine, /// - /// Inserts a tab. + /// Tabs to the next item. /// Tab, /// - /// Inserts a shift tab. + /// Tabs back to the previous item. /// BackTab } diff --git a/Terminal.Gui/Core/SearchCollectionNavigator.cs b/Terminal.Gui/Core/SearchCollectionNavigator.cs index 47d62b6613..9d113e256c 100644 --- a/Terminal.Gui/Core/SearchCollectionNavigator.cs +++ b/Terminal.Gui/Core/SearchCollectionNavigator.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; namespace Terminal.Gui { @@ -11,11 +12,17 @@ class SearchCollectionNavigator { DateTime lastKeystroke = DateTime.MinValue; const int TypingDelay = 250; public StringComparer Comparer { get; set; } = StringComparer.InvariantCultureIgnoreCase; + private IEnumerable Collection { get => _collection; set => _collection = value; } - public int CalculateNewIndex (string [] collection, int currentIndex, char keyStruck) + private IEnumerable _collection; + + public SearchCollectionNavigator (IEnumerable collection) { _collection = collection; } + + + public int CalculateNewIndex (IEnumerable collection, int currentIndex, char keyStruck) { - // if user presses a letter key - if (char.IsLetterOrDigit (keyStruck) || char.IsPunctuation (keyStruck)) { + // if user presses a key + if (true) {//char.IsLetterOrDigit (keyStruck) || char.IsPunctuation (keyStruck) || char.IsSymbol(keyStruck)) { // maybe user pressed 'd' and now presses 'd' again. // a candidate search is things that begin with "dd" @@ -73,7 +80,12 @@ public int CalculateNewIndex (string [] collection, int currentIndex, char keySt } } - private int GetNextIndexMatching (string [] collection, int currentIndex, string search, bool preferNotToMoveToNewIndexes = false) + public int CalculateNewIndex (int currentIndex, char keyStruck) + { + return CalculateNewIndex (Collection, currentIndex, keyStruck); + } + + private int GetNextIndexMatching (IEnumerable collection, int currentIndex, string search, bool preferNotToMoveToNewIndexes = false) { if (string.IsNullOrEmpty (search)) { return -1; diff --git a/Terminal.Gui/Views/ListView.cs b/Terminal.Gui/Views/ListView.cs index 61ed818c71..b58aa9490b 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using NStack; @@ -179,6 +180,12 @@ public bool AllowsMarking { get => allowsMarking; set { allowsMarking = value; + if (allowsMarking) { + AddKeyBinding (Key.Space, Command.ToggleChecked); + } else { + ClearKeybinding (Key.Space); + } + SetNeedsDisplay (); } } @@ -353,8 +360,6 @@ void Initialize () AddKeyBinding (Key.End, Command.BottomEnd); AddKeyBinding (Key.Enter, Command.OpenSelectedItem); - - AddKeyBinding (Key.Space, Command.ToggleChecked); } /// @@ -416,39 +421,30 @@ public override void Redraw (Rect bounds) /// public event Action RowRender; - private string search { get; set; } + private SearchCollectionNavigator navigator; /// public override bool ProcessKey (KeyEvent kb) { - if (source == null) + if (source == null) { return base.ProcessKey (kb); + } var result = InvokeKeybindings (kb); - if (result != null) + if (result != null) { return (bool)result; + } // Enable user to find & select an item by typing text - if (source is IListDataSourceSearchable && - !(kb.IsCapslock && kb.IsCtrl && kb.IsAlt && kb.IsScrolllock && kb.IsNumlock && kb.IsCapslock)) { - if (kb.KeyValue >= 32 && kb.KeyValue < 127) { - if (searchTimer == null) { - searchTimer = new System.Timers.Timer (500); - searchTimer.Elapsed += (o, e) => { - searchTimer.Stop (); - searchTimer = null; - search = ""; - }; - searchTimer.Start (); - } - search += (char)kb.KeyValue; - var found = ((IListDataSourceSearchable)source).StartsWith (search); - if (found != -1) { - SelectedItem = found; - SetNeedsDisplay (); - } - return true; + if (!kb.IsAlt && !kb.IsCapslock && !kb.IsCtrl && !kb.IsScrolllock && !kb.IsNumlock) { + if (navigator == null) { + // BUGBUG: If items change this needs to be recreated. + navigator = new SearchCollectionNavigator (source.ToList().Cast()); } + SelectedItem = navigator.CalculateNewIndex (SelectedItem, (char)kb.KeyValue); + EnsuresVisibilitySelectedItem (); + SetNeedsDisplay (); + return true; } return false; diff --git a/UICatalog/Properties/launchSettings.json b/UICatalog/Properties/launchSettings.json index f890e66cf7..1d9bae358a 100644 --- a/UICatalog/Properties/launchSettings.json +++ b/UICatalog/Properties/launchSettings.json @@ -26,6 +26,10 @@ "Issue1719Repro": { "commandName": "Project", "commandLineArgs": "\"ProgressBar Styles\"" + }, + "SearchCollectionNavTester": { + "commandName": "Project", + "commandLineArgs": "\"Search Collection Nav\"" } } } \ No newline at end of file diff --git a/UICatalog/Scenarios/SearchCollectionNavigatorTester.cs b/UICatalog/Scenarios/SearchCollectionNavigatorTester.cs new file mode 100644 index 0000000000..1e731dfd16 --- /dev/null +++ b/UICatalog/Scenarios/SearchCollectionNavigatorTester.cs @@ -0,0 +1,218 @@ +using System; +using System.IO; +using System.Linq; +using Terminal.Gui; +using Terminal.Gui.Trees; + +namespace UICatalog.Scenarios { + + [ScenarioMetadata (Name: "Search Collection Nav", Description: "Demonstrates & tests SearchCollectionNavigator.")] + [ScenarioCategory ("Controls"), ScenarioCategory ("ListView")] + public class SearchCollectionNavigatorTester : Scenario { + TabView tabView; + + private int numbeOfNewTabs = 1; + + // Don't create a Window, just return the top-level view + public override void Init (Toplevel top, ColorScheme colorScheme) + { + Application.Init (); + Top = top != null ? top : Application.Top; + Top.ColorScheme = Colors.Base; + } + + public override void Setup () + { + var allowMarking = new MenuItem ("Allow _Marking", "", null) { + CheckType = MenuItemCheckStyle.Checked, + Checked = false + }; + allowMarking.Action = () => allowMarking.Checked = _listView.AllowsMarking = !_listView.AllowsMarking; + + var allowMultiSelection = new MenuItem ("Allow Multi _Selection", "", null) { + CheckType = MenuItemCheckStyle.Checked, + Checked = false + }; + allowMultiSelection.Action = () => allowMultiSelection.Checked = _listView.AllowsMultipleSelection = !_listView.AllowsMultipleSelection; + allowMultiSelection.CanExecute = () => allowMarking.Checked; + + var menu = new MenuBar (new MenuBarItem [] { + new MenuBarItem ("_Configure", new MenuItem [] { + allowMarking, + allowMultiSelection, + null, + new MenuItem ("_Quit", "", () => Quit(), null, null, Key.Q | Key.CtrlMask), + }), + new MenuBarItem("_Quit", "CTRL-Q", () => Quit()) + }); + + Top.Add (menu); + + CreateListView (); + var vsep = new LineView (Terminal.Gui.Graphs.Orientation.Vertical) { + X = Pos.Right (_listView), + Y = 1, + Height = Dim.Fill () + }; + Top.Add (vsep); + + } + + ListView _listView = null; + + private void CreateListView () + { + var label = new Label () { + Text = "ListView", + TextAlignment = TextAlignment.Centered, + X = 0, + Y = 1, // for menu + Width = Dim.Percent (50), + Height = 1, + }; + Top.Add (label); + + _listView = new ListView () { + X = 0, + Y = Pos.Bottom(label), + Width = Dim.Percent (50) - 1, + Height = Dim.Fill (), + AllowsMarking = false, + AllowsMultipleSelection = false, + ColorScheme = Colors.TopLevel + }; + Top.Add (_listView); + + System.Collections.Generic.List items = new string [] { + "a", + "b", + "bb", + "c", + "ccc", + "ccc", + "cccc", + "ddd", + "dddd", + "dddd", + "ddddd", + "dddddd", + "ddddddd", + "this", + "this is a test", + "this was a test", + "this and", + "that and that", + "the", + "think", + "thunk", + "thunks", + "zip", + "zap", + "zoo", + "@jack", + "@sign", + "@at", + "@ateme", + "n@", + "n@brown", + ".net", + "$100.00", + "$101.00", + "$101.10", + "$101.11", + "appricot", + "arm", + "丗丙业丞", + "丗丙丛", + "text", + "egg", + "candle", + " <- space", + "q", + "quit", + "quitter" + }.ToList (); + items.Sort (StringComparer.OrdinalIgnoreCase); + _listView.SetSource (items); + } + + TreeView _treeView = null; + + private void CreateTreeView () + { + var label = new Label () { + Text = "TreeView", + TextAlignment = TextAlignment.Centered, + X = Pos.Right(_listView) + 2, + Y = 1, // for menu + Width = Dim.Percent (50), + Height = 1, + }; + Top.Add (label); + + _treeView = new TreeView () { + X = Pos.Right (_listView) + 2, + Y = Pos.Bottom (label), + Width = Dim.Percent (50) - 1, + Height = Dim.Fill (), + ColorScheme = Colors.TopLevel + }; + Top.Add (_treeView); + + System.Collections.Generic.List items = new string [] { "a", + "b", + "bb", + "c", + "ccc", + "ccc", + "cccc", + "ddd", + "dddd", + "dddd", + "ddddd", + "dddddd", + "ddddddd", + "this", + "this is a test", + "this was a test", + "this and", + "that and that", + "the", + "think", + "thunk", + "thunks", + "zip", + "zap", + "zoo", + "@jack", + "@sign", + "@at", + "@ateme", + "n@", + "n@brown", + ".net", + "$100.00", + "$101.00", + "$101.10", + "$101.11", + "appricot", + "arm", + "丗丙业丞", + "丗丙丛", + "text", + "egg", + "candle", + " <- space", + "q", + "quit", + "quitter" + }.ToList (); + items.Sort (StringComparer.OrdinalIgnoreCase); + _treeView.AddObjects (items); + } + private void Quit () + { + Application.RequestStop (); + } + } +} diff --git a/UnitTests/SearchCollectionNavigatorTests.cs b/UnitTests/SearchCollectionNavigatorTests.cs index ac39b88642..eea4c76d0f 100644 --- a/UnitTests/SearchCollectionNavigatorTests.cs +++ b/UnitTests/SearchCollectionNavigatorTests.cs @@ -1,13 +1,13 @@ using Terminal.Gui; using Xunit; -namespace UnitTests { +namespace Terminal.Gui.Core { public class SearchCollectionNavigatorTests { [Fact] public void TestSearchCollectionNavigator_Cycling () { - var s = new string []{ + var strings = new string []{ "appricot", "arm", "bat", @@ -15,12 +15,12 @@ public void TestSearchCollectionNavigator_Cycling () "candle" }; - var n = new SearchCollectionNavigator (); - Assert.Equal (2, n.CalculateNewIndex (s, 0, 'b')); - Assert.Equal (3, n.CalculateNewIndex (s, 2, 'b')); + var n = new SearchCollectionNavigator (strings); + Assert.Equal (2, n.CalculateNewIndex ( 0, 'b')); + Assert.Equal (3, n.CalculateNewIndex ( 2, 'b')); // if 4 (candle) is selected it should loop back to bat - Assert.Equal (2, n.CalculateNewIndex (s, 4, 'b')); + Assert.Equal (2, n.CalculateNewIndex ( 4, 'b')); } @@ -28,7 +28,7 @@ public void TestSearchCollectionNavigator_Cycling () [Fact] public void TestSearchCollectionNavigator_ToSearchText () { - var s = new string []{ + var strings = new string []{ "appricot", "arm", "bat", @@ -37,19 +37,19 @@ public void TestSearchCollectionNavigator_ToSearchText () "candle" }; - var n = new SearchCollectionNavigator (); - Assert.Equal (2, n.CalculateNewIndex (s, 0, 'b')); - Assert.Equal (4, n.CalculateNewIndex (s, 2, 'b')); + var n = new SearchCollectionNavigator (strings); + Assert.Equal (2, n.CalculateNewIndex (0, 'b')); + Assert.Equal (4, n.CalculateNewIndex (2, 'b')); // another 'b' means searching for "bbb" which does not exist // so we go back to looking for "b" as a fresh key strike - Assert.Equal (4, n.CalculateNewIndex (s, 2, 'b')); + Assert.Equal (4, n.CalculateNewIndex (2, 'b')); } [Fact] public void TestSearchCollectionNavigator_FullText () { - var s = new string []{ + var strings = new string []{ "appricot", "arm", "ta", @@ -59,23 +59,23 @@ public void TestSearchCollectionNavigator_FullText () "candle" }; - var n = new SearchCollectionNavigator (); - Assert.Equal (2, n.CalculateNewIndex (s, 0, 't')); + var n = new SearchCollectionNavigator (strings); + Assert.Equal (2, n.CalculateNewIndex (0, 't')); // should match "te" in "text" - Assert.Equal (4, n.CalculateNewIndex (s, 2, 'e')); + Assert.Equal (4, n.CalculateNewIndex (2, 'e')); // still matches text - Assert.Equal (4, n.CalculateNewIndex (s, 4, 'x')); + Assert.Equal (4, n.CalculateNewIndex (4, 'x')); // nothing starts texa so it jumps to a for appricot - Assert.Equal (0, n.CalculateNewIndex (s, 4, 'a')); + Assert.Equal (0, n.CalculateNewIndex (4, 'a')); } [Fact] public void TestSearchCollectionNavigator_Unicode () { - var s = new string []{ + var strings = new string []{ "appricot", "arm", "ta", @@ -86,27 +86,27 @@ public void TestSearchCollectionNavigator_Unicode () "candle" }; - var n = new SearchCollectionNavigator (); - Assert.Equal (3, n.CalculateNewIndex (s, 0, '丗')); + var n = new SearchCollectionNavigator (strings); + Assert.Equal (3, n.CalculateNewIndex (0, '丗')); // 丗丙业丞 is as good a match as 丗丙丛 // so when doing multi character searches we should // prefer to stay on the same index unless we invalidate // our typed text - Assert.Equal (3, n.CalculateNewIndex (s, 3, '丙')); + Assert.Equal (3, n.CalculateNewIndex (3, '丙')); // No longer matches 丗丙业丞 and now only matches 丗丙丛 // so we should move to the new match - Assert.Equal (4, n.CalculateNewIndex (s, 3, '丛')); + Assert.Equal (4, n.CalculateNewIndex (3, '丛')); // nothing starts "丗丙丛a" so it jumps to a for appricot - Assert.Equal (0, n.CalculateNewIndex (s, 4, 'a')); + Assert.Equal (0, n.CalculateNewIndex (4, 'a')); } [Fact] public void TestSearchCollectionNavigator_AtSymbol () { - var s = new string []{ + var strings = new string []{ "appricot", "arm", "ta", @@ -117,10 +117,10 @@ public void TestSearchCollectionNavigator_AtSymbol () "candle" }; - var n = new SearchCollectionNavigator (); - Assert.Equal (3, n.CalculateNewIndex (s, 0, '@')); - Assert.Equal (3, n.CalculateNewIndex (s, 3, 'b')); - Assert.Equal (4, n.CalculateNewIndex (s, 3, 'b')); + var n = new SearchCollectionNavigator (strings); + Assert.Equal (3, n.CalculateNewIndex (0, '@')); + Assert.Equal (3, n.CalculateNewIndex (3, 'b')); + Assert.Equal (4, n.CalculateNewIndex (3, 'b')); } } } From b09b3ad8f2bb82494b378b13c514e9988755ef61 Mon Sep 17 00:00:00 2001 From: Tig Kindel Date: Tue, 25 Oct 2022 10:13:49 -0600 Subject: [PATCH 04/25] Refactored UI Catalog Scenario class to support ToString() --- .../Core/SearchCollectionNavigator.cs | 12 ++-- Terminal.Gui/Views/ListView.cs | 2 +- UICatalog/Scenario.cs | 23 +++++-- UICatalog/Scenarios/ListViewWithSelection.cs | 21 +++--- UICatalog/UICatalog.cs | 69 +++++-------------- UnitTests/ScenarioTests.cs | 24 +++---- 6 files changed, 62 insertions(+), 89 deletions(-) diff --git a/Terminal.Gui/Core/SearchCollectionNavigator.cs b/Terminal.Gui/Core/SearchCollectionNavigator.cs index 9d113e256c..dda4b30ad6 100644 --- a/Terminal.Gui/Core/SearchCollectionNavigator.cs +++ b/Terminal.Gui/Core/SearchCollectionNavigator.cs @@ -12,14 +12,14 @@ class SearchCollectionNavigator { DateTime lastKeystroke = DateTime.MinValue; const int TypingDelay = 250; public StringComparer Comparer { get; set; } = StringComparer.InvariantCultureIgnoreCase; - private IEnumerable Collection { get => _collection; set => _collection = value; } + private IEnumerable Collection { get => _collection; set => _collection = value; } - private IEnumerable _collection; + private IEnumerable _collection; - public SearchCollectionNavigator (IEnumerable collection) { _collection = collection; } + public SearchCollectionNavigator (IEnumerable collection) { _collection = collection; } - public int CalculateNewIndex (IEnumerable collection, int currentIndex, char keyStruck) + public int CalculateNewIndex (IEnumerable collection, int currentIndex, char keyStruck) { // if user presses a key if (true) {//char.IsLetterOrDigit (keyStruck) || char.IsPunctuation (keyStruck) || char.IsSymbol(keyStruck)) { @@ -85,7 +85,7 @@ public int CalculateNewIndex (int currentIndex, char keyStruck) return CalculateNewIndex (Collection, currentIndex, keyStruck); } - private int GetNextIndexMatching (IEnumerable collection, int currentIndex, string search, bool preferNotToMoveToNewIndexes = false) + private int GetNextIndexMatching (IEnumerable collection, int currentIndex, string search, bool preferNotToMoveToNewIndexes = false) { if (string.IsNullOrEmpty (search)) { return -1; @@ -93,7 +93,7 @@ private int GetNextIndexMatching (IEnumerable collection, int currentInd // find indexes of items that start with the search text int [] matchingIndexes = collection.Select ((item, idx) => (item, idx)) - .Where (k => k.Item1?.StartsWith (search, StringComparison.InvariantCultureIgnoreCase) ?? false) + .Where (k => k.item?.ToString().StartsWith (search, StringComparison.InvariantCultureIgnoreCase) ?? false) .Select (k => k.idx) .ToArray (); diff --git a/Terminal.Gui/Views/ListView.cs b/Terminal.Gui/Views/ListView.cs index b58aa9490b..930b6a517f 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -439,7 +439,7 @@ public override bool ProcessKey (KeyEvent kb) if (!kb.IsAlt && !kb.IsCapslock && !kb.IsCtrl && !kb.IsScrolllock && !kb.IsNumlock) { if (navigator == null) { // BUGBUG: If items change this needs to be recreated. - navigator = new SearchCollectionNavigator (source.ToList().Cast()); + navigator = new SearchCollectionNavigator (source.ToList ().Cast ()); } SelectedItem = navigator.CalculateNewIndex (SelectedItem, (char)kb.KeyValue); EnsuresVisibilitySelectedItem (); diff --git a/UICatalog/Scenario.cs b/UICatalog/Scenario.cs index 797cf09a68..e2a28fbcda 100644 --- a/UICatalog/Scenario.cs +++ b/UICatalog/Scenario.cs @@ -73,7 +73,7 @@ public class Scenario : IDisposable { /// Overrides that do not call the base., must call before creating any views or calling other Terminal.Gui APIs. /// /// - public virtual void Init(Toplevel top, ColorScheme colorScheme) + public virtual void Init (Toplevel top, ColorScheme colorScheme) { Application.Init (); @@ -177,7 +177,14 @@ public static List GetCategories (Type t) => System.Attribute.GetCustomA /// list of category names public List GetCategories () => ScenarioCategory.GetCategories (this.GetType ()); - public override string ToString () => $"{GetName (),-30}{GetDescription ()}"; + private static int _maxScenarioNameLen = 30; + + /// + /// Gets the Scenario Name + Description with the Description padded + /// based on the longest known Scenario name. + /// + /// + public override string ToString () => $"{GetName ().PadRight(_maxScenarioNameLen)}{GetDescription ()}"; /// /// Override this to implement the setup logic (create controls, etc...). @@ -232,12 +239,14 @@ internal static List GetAllCategories () /// Returns an instance of each defined in the project. /// https://stackoverflow.com/questions/5411694/get-all-inherited-classes-of-an-abstract-class /// - public static List GetDerivedClasses () + public static List GetScenarios () { - List objects = new List (); - foreach (Type type in typeof (T).Assembly.GetTypes () - .Where (myType => myType.IsClass && !myType.IsAbstract && myType.IsSubclassOf (typeof (T)))) { - objects.Add (type); + List objects = new List (); + foreach (Type type in typeof (Scenario).Assembly.ExportedTypes + .Where (myType => myType.IsClass && !myType.IsAbstract && myType.IsSubclassOf (typeof (Scenario)))) { + var scenario = (Scenario)Activator.CreateInstance (type); + objects.Add (scenario); + _maxScenarioNameLen = Math.Max (_maxScenarioNameLen, scenario.GetName ().Length + 1); } return objects; } diff --git a/UICatalog/Scenarios/ListViewWithSelection.cs b/UICatalog/Scenarios/ListViewWithSelection.cs index 057dcb6935..bd1afc40b8 100644 --- a/UICatalog/Scenarios/ListViewWithSelection.cs +++ b/UICatalog/Scenarios/ListViewWithSelection.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Nodes; using Terminal.Gui; using Attribute = Terminal.Gui.Attribute; @@ -16,11 +17,13 @@ public class ListViewWithSelection : Scenario { public CheckBox _allowMultipleCB; public ListView _listView; - public List _scenarios = Scenario.GetDerivedClasses().OrderBy (t => Scenario.ScenarioMetadata.GetName (t)).ToList (); + public List _scenarios; public override void Setup () { - _customRenderCB = new CheckBox ("Render with columns") { + _scenarios = Scenario.GetScenarios ().OrderBy (s => s.GetName ()).ToList (); + + _customRenderCB = new CheckBox ("Use custom rendering") { X = 0, Y = 0, Height = 1, @@ -137,11 +140,11 @@ private void AllowMultipleCB_Toggled (bool prev) // This is basically the same implementation used by the UICatalog main window internal class ScenarioListDataSource : IListDataSource { int _nameColumnWidth = 30; - private List scenarios; + private List scenarios; BitArray marks; int count, len; - public List Scenarios { + public List Scenarios { get => scenarios; set { if (value != null) { @@ -163,14 +166,14 @@ public bool IsMarked (int item) public int Length => len; - public ScenarioListDataSource (List itemList) => Scenarios = itemList; + public ScenarioListDataSource (List itemList) => Scenarios = itemList; 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, start); + var s = String.Format (String.Format ("{{0,{0}}}", -_nameColumnWidth), Scenarios [item].GetName ()); + RenderUstr (driver, $"{s} ({Scenarios [item].GetDescription ()})", col, line, width, start); } public void SetMark (int item, bool value) @@ -187,8 +190,8 @@ int GetMaxLengthItem () 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 s = String.Format (String.Format ("{{0,{0}}}", -_nameColumnWidth), Scenarios [i].GetName ()); + var sc = $"{s} {Scenarios [i].GetDescription ()}"; var l = sc.Length; if (l > maxLength) { maxLength = l; diff --git a/UICatalog/UICatalog.cs b/UICatalog/UICatalog.cs index 9949c337f6..6229c6090e 100644 --- a/UICatalog/UICatalog.cs +++ b/UICatalog/UICatalog.cs @@ -53,7 +53,7 @@ public class UICatalogApp { private static List _categories; private static ListView _categoryListView; private static FrameView _rightPane; - private static List _scenarios; + private static List _scenarios; private static ListView _scenarioListView; private static StatusBar _statusBar; private static StatusItem _capslock; @@ -75,15 +75,15 @@ static void Main (string [] args) if (Debugger.IsAttached) CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.GetCultureInfo ("en-US"); - _scenarios = Scenario.GetDerivedClasses ().OrderBy (t => Scenario.ScenarioMetadata.GetName (t)).ToList (); + _scenarios = Scenario.GetScenarios (); if (args.Length > 0 && args.Contains ("-usc")) { _useSystemConsole = true; args = args.Where (val => val != "-usc").ToArray (); } if (args.Length > 0) { - var item = _scenarios.FindIndex (t => Scenario.ScenarioMetadata.GetName (t).Equals (args [0], StringComparison.OrdinalIgnoreCase)); - _runningScenario = (Scenario)Activator.CreateInstance (_scenarios [item]); + var item = _scenarios.FindIndex (s => s.GetName ().Equals (args [0], StringComparison.OrdinalIgnoreCase)); + _runningScenario = (Scenario)Activator.CreateInstance (_scenarios [item].GetType()); Application.UseSystemConsole = _useSystemConsole; Application.Init (); _runningScenario.Init (Application.Top, _baseColorScheme); @@ -218,7 +218,7 @@ private static Scenario GetScenarioToRun () _rightPane.Title = $"{_rightPane.Title} ({_rightPane.ShortcutTag})"; _rightPane.ShortcutAction = () => _rightPane.SetFocus (); - _nameColumnWidth = Scenario.ScenarioMetadata.GetName (_scenarios.OrderByDescending (t => Scenario.ScenarioMetadata.GetName (t).Length).FirstOrDefault ()).Length; + _nameColumnWidth = _scenarios.OrderByDescending (s => s.GetName ().Length).FirstOrDefault ().GetName().Length; _scenarioListView = new ListView () { X = 0, @@ -462,42 +462,6 @@ void SetDiagnosticsFlag (Enum diag, bool add) break; } } - - //MenuItem CheckedMenuMenuItem (ustring menuItem, Action action, Func checkFunction) - //{ - // var mi = new MenuItem (); - // mi.Title = menuItem; - // mi.Shortcut = Key.AltMask + index.ToString () [0]; - // index++; - // mi.CheckType |= MenuItemCheckStyle.Checked; - // mi.Checked = checkFunction (); - // mi.Action = () => { - // action?.Invoke (); - // mi.Title = menuItem; - // mi.Checked = checkFunction (); - // }; - // return mi; - //} - - //return new MenuItem [] { - // CheckedMenuMenuItem ("Use _System Console", - // () => { - // _useSystemConsole = !_useSystemConsole; - // }, - // () => _useSystemConsole), - // CheckedMenuMenuItem ("Diagnostics: _Frame Padding", - // () => { - // ConsoleDriver.Diagnostics ^= ConsoleDriver.DiagnosticFlags.FramePadding; - // _top.SetNeedsDisplay (); - // }, - // () => (ConsoleDriver.Diagnostics & ConsoleDriver.DiagnosticFlags.FramePadding) == ConsoleDriver.DiagnosticFlags.FramePadding), - // CheckedMenuMenuItem ("Diagnostics: Frame _Ruler", - // () => { - // ConsoleDriver.Diagnostics ^= ConsoleDriver.DiagnosticFlags.FrameRuler; - // _top.SetNeedsDisplay (); - // }, - // () => (ConsoleDriver.Diagnostics & ConsoleDriver.DiagnosticFlags.FrameRuler) == ConsoleDriver.DiagnosticFlags.FrameRuler), - //}; } static void SetColorScheme () @@ -533,8 +497,8 @@ private static void _scenarioListView_OpenSelectedItem (EventArgs e) { if (_runningScenario is null) { _scenarioListViewItem = _scenarioListView.SelectedItem; - var source = _scenarioListView.Source as ScenarioListDataSource; - _runningScenario = (Scenario)Activator.CreateInstance (source.Scenarios [_scenarioListView.SelectedItem]); + // Create new instance of scenario (even though Scenarios contains instnaces) + _runningScenario = (Scenario)Activator.CreateInstance (_scenarioListView.Source.ToList() [_scenarioListView.SelectedItem].GetType()); Application.RequestStop (); } } @@ -542,7 +506,7 @@ private static void _scenarioListView_OpenSelectedItem (EventArgs e) internal class ScenarioListDataSource : IListDataSource { private readonly int len; - public List Scenarios { get; set; } + public List Scenarios { get; set; } public bool IsMarked (int item) => false; @@ -550,7 +514,7 @@ internal class ScenarioListDataSource : IListDataSource { public int Length => len; - public ScenarioListDataSource (List itemList) + public ScenarioListDataSource (List itemList) { Scenarios = itemList; len = GetMaxLengthItem (); @@ -560,8 +524,8 @@ public void Render (ListView container, ConsoleDriver driver, bool selected, int { 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, start); + var s = String.Format (String.Format ("{{0,{0}}}", -_nameColumnWidth), Scenarios [item].GetName()); + RenderUstr (driver, $"{s} {Scenarios [item].GetDescription()}", col, line, width, start); } public void SetMark (int item, bool value) @@ -576,14 +540,13 @@ int GetMaxLengthItem () 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 s = String.Format (String.Format ("{{0,{0}}}", -_nameColumnWidth), Scenarios [i].GetName()); + var sc = $"{s} {Scenarios [i].GetDescription()}"; var l = sc.Length; if (l > maxLength) { maxLength = l; } } - return maxLength; } @@ -661,15 +624,15 @@ private static void CategoryListView_SelectedChanged (ListViewItemEventArgs e) } _categoryListViewItem = _categoryListView.SelectedItem; var item = _categories [_categoryListViewItem]; - List newlist; + List newlist; if (_categoryListViewItem == 0) { // First category is "All" newlist = _scenarios; } else { - newlist = _scenarios.Where (t => Scenario.ScenarioCategory.GetCategories (t).Contains (item)).ToList (); + newlist = _scenarios.Where (s => s.GetCategories ().Contains (item)).ToList (); } - _scenarioListView.Source = new ScenarioListDataSource (newlist); + _scenarioListView.SetSource(newlist.ToList()); _scenarioListView.SelectedItem = _scenarioListViewItem; } diff --git a/UnitTests/ScenarioTests.cs b/UnitTests/ScenarioTests.cs index 47e94b2162..ae40ea9883 100644 --- a/UnitTests/ScenarioTests.cs +++ b/UnitTests/ScenarioTests.cs @@ -49,19 +49,18 @@ int CreateInput (string input) [Fact] public void Run_All_Scenarios () { - List scenarioClasses = Scenario.GetDerivedClasses (); - Assert.NotEmpty (scenarioClasses); + List scenarios = Scenario.GetScenarios (); + Assert.NotEmpty (scenarios); - foreach (var scenarioClass in scenarioClasses) { + foreach (var scenario in scenarios) { - output.WriteLine ($"Running Scenario '{scenarioClass.Name}'"); + output.WriteLine ($"Running Scenario '{scenario}'"); Func closeCallback = (MainLoop loop) => { Application.RequestStop (); return false; }; - var scenario = (Scenario)Activator.CreateInstance (scenarioClass); Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true))); // Close after a short period of time @@ -83,11 +82,11 @@ public void Run_All_Scenarios () [Fact] public void Run_Generic () { - List scenarioClasses = Scenario.GetDerivedClasses (); - Assert.NotEmpty (scenarioClasses); + List scenarios = Scenario.GetScenarios (); + Assert.NotEmpty (scenarios); - var item = scenarioClasses.FindIndex (t => Scenario.ScenarioMetadata.GetName (t).Equals ("Generic", StringComparison.OrdinalIgnoreCase)); - var scenarioClass = scenarioClasses [item]; + var item = scenarios.FindIndex (s => s.GetName ().Equals ("Generic", StringComparison.OrdinalIgnoreCase)); + var generic = scenarios [item]; // Setup some fake keypresses // Passing empty string will cause just a ctrl-q to be fired int stackSize = CreateInput (""); @@ -116,13 +115,12 @@ public void Run_Generic () Assert.Equal (Key.CtrlMask | Key.Q, args.KeyEvent.Key); }; - var scenario = (Scenario)Activator.CreateInstance (scenarioClass); - scenario.Init (Application.Top, Colors.Base); - scenario.Setup (); + generic.Init (Application.Top, Colors.Base); + generic.Setup (); // There is no need to call Application.Begin because Init already creates the Application.Top // If Application.RunState is used then the Application.RunLoop must also be used instead Application.Run. //var rs = Application.Begin (Application.Top); - scenario.Run (); + generic.Run (); //Application.End (rs); From 71b00e9009a474ffbfa8b11955b09463527e5ef5 Mon Sep 17 00:00:00 2001 From: Tig Kindel Date: Tue, 25 Oct 2022 10:19:38 -0600 Subject: [PATCH 05/25] Nuked ScenarioListDataSource --- UICatalog/UICatalog.cs | 73 ------------------------------------------ 1 file changed, 73 deletions(-) diff --git a/UICatalog/UICatalog.cs b/UICatalog/UICatalog.cs index 6229c6090e..9cd1f865b2 100644 --- a/UICatalog/UICatalog.cs +++ b/UICatalog/UICatalog.cs @@ -503,79 +503,6 @@ private static void _scenarioListView_OpenSelectedItem (EventArgs e) } } - 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 int Length => len; - - 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), Scenarios [item].GetName()); - RenderUstr (driver, $"{s} {Scenarios [item].GetDescription()}", 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), Scenarios [i].GetName()); - var sc = $"{s} {Scenarios [i].GetDescription()}"; - var l = sc.Length; - if (l > maxLength) { - maxLength = l; - } - } - return maxLength; - } - - // A slightly adapted method from: https://github.com/gui-cs/Terminal.Gui/blob/fc1faba7452ccbdf49028ac49f0c9f0f42bbae91/Terminal.Gui/Views/ListView.cs#L433-L461 - private void RenderUstr (ConsoleDriver driver, ustring ustr, int col, int line, int width, int start = 0) - { - int used = 0; - int index = start; - while (index < ustr.Length) { - (var rune, var size) = Utf8.DecodeRune (ustr, index, index - ustr.Length); - var count = Rune.ColumnWidth (rune); - if (used + count >= width) break; - driver.AddRune (rune); - used += count; - index += size; - } - - while (used < width) { - driver.AddRune (' '); - used++; - } - } - - public IList ToList () - { - return Scenarios; - } - } - /// /// When Scenarios are running we need to override the behavior of the Menu /// and Statusbar to enable Scenarios that use those (or related key input) From 77ae85673155727a7519dea3c81ff9c70a849669 Mon Sep 17 00:00:00 2001 From: Tig Kindel Date: Tue, 25 Oct 2022 11:34:08 -0600 Subject: [PATCH 06/25] Added SetNeedsDisplay to AllowsMultipleSelection per bdisp --- Terminal.Gui/Views/ListView.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Terminal.Gui/Views/ListView.cs b/Terminal.Gui/Views/ListView.cs index 930b6a517f..19e8605fb8 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -191,7 +191,7 @@ public bool AllowsMarking { } /// - /// If set to more than one item can be selected. If selecting + /// 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 { @@ -206,6 +206,7 @@ public bool AllowsMultipleSelection { } } } + SetNeedsDisplay (); } } From 40514fbb9f691b7e033af0c23394c7fac446072e Mon Sep 17 00:00:00 2001 From: Tig Kindel Date: Sat, 29 Oct 2022 17:32:45 -0600 Subject: [PATCH 07/25] more progress --- .../Core/SearchCollectionNavigator.cs | 2 +- Terminal.Gui/Views/ListView.cs | 13 +- UICatalog/Scenarios/BordersComparisons.cs | 1 - UICatalog/UICatalog.cs | 233 +++++++++--------- 4 files changed, 119 insertions(+), 130 deletions(-) diff --git a/Terminal.Gui/Core/SearchCollectionNavigator.cs b/Terminal.Gui/Core/SearchCollectionNavigator.cs index dda4b30ad6..34424dc428 100644 --- a/Terminal.Gui/Core/SearchCollectionNavigator.cs +++ b/Terminal.Gui/Core/SearchCollectionNavigator.cs @@ -22,7 +22,7 @@ class SearchCollectionNavigator { public int CalculateNewIndex (IEnumerable collection, int currentIndex, char keyStruck) { // if user presses a key - if (true) {//char.IsLetterOrDigit (keyStruck) || char.IsPunctuation (keyStruck) || char.IsSymbol(keyStruck)) { + if (!char.IsControl(keyStruck)) {//char.IsLetterOrDigit (keyStruck) || char.IsPunctuation (keyStruck) || char.IsSymbol(keyStruck)) { // maybe user pressed 'd' and now presses 'd' again. // a candidate search is things that begin with "dd" diff --git a/Terminal.Gui/Views/ListView.cs b/Terminal.Gui/Views/ListView.cs index 19e8605fb8..1736209d9e 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -442,10 +442,13 @@ public override bool ProcessKey (KeyEvent kb) // BUGBUG: If items change this needs to be recreated. navigator = new SearchCollectionNavigator (source.ToList ().Cast ()); } - SelectedItem = navigator.CalculateNewIndex (SelectedItem, (char)kb.KeyValue); - EnsuresVisibilitySelectedItem (); - SetNeedsDisplay (); - return true; + var newItem = navigator.CalculateNewIndex (SelectedItem, (char)kb.KeyValue); + if (newItem != SelectedItem) { + SelectedItem = newItem; + EnsuresVisibilitySelectedItem (); + SetNeedsDisplay (); + return true; + } } return false; @@ -741,7 +744,7 @@ public override bool OnEnter (View view) if (lastSelectedItem == -1) { EnsuresVisibilitySelectedItem (); - OnSelectedChanged (); + //OnSelectedChanged (); } return base.OnEnter (view); diff --git a/UICatalog/Scenarios/BordersComparisons.cs b/UICatalog/Scenarios/BordersComparisons.cs index baaabcae01..9ea462f529 100644 --- a/UICatalog/Scenarios/BordersComparisons.cs +++ b/UICatalog/Scenarios/BordersComparisons.cs @@ -7,7 +7,6 @@ namespace UICatalog.Scenarios { public class BordersComparisons : Scenario { public override void Init (Toplevel top, ColorScheme colorScheme) { - top.Dispose (); Application.Init (); top = Application.Top; diff --git a/UICatalog/UICatalog.cs b/UICatalog/UICatalog.cs index 9cd1f865b2..cd625730a0 100644 --- a/UICatalog/UICatalog.cs +++ b/UICatalog/UICatalog.cs @@ -1,6 +1,5 @@ using NStack; using System; -using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; @@ -46,8 +45,6 @@ namespace UICatalog { /// UI Catalog is a comprehensive sample app and scenario library for /// public class UICatalogApp { - private static Toplevel _top; - private static MenuBar _menu; private static int _nameColumnWidth; private static FrameView _leftPane; private static List _categories; @@ -59,21 +56,28 @@ public class UICatalogApp { private static StatusItem _capslock; private static StatusItem _numlock; private static StatusItem _scrolllock; - private static int _categoryListViewItem; - private static int _scenarioListViewItem; - private static Scenario _runningScenario = null; + private static Scenario _selectedScenario = null; private static bool _useSystemConsole = false; private static ConsoleDriver.DiagnosticFlags _diagnosticFlags; private static bool _heightAsBuffer = false; private static bool _isFirstRunning = true; + // When a scenario is run, the main app is killed. These items + // are therefore cached so that when the scenario exits the + // main app UI can be restored to previous state + private static int _cachedScenarioIndex = 0; + private static int _cachedCategoryIndex = 0; + + private static StringBuilder _aboutMessage; + static void Main (string [] args) { Console.OutputEncoding = Encoding.Default; - if (Debugger.IsAttached) + if (Debugger.IsAttached) { CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.GetCultureInfo ("en-US"); + } _scenarios = Scenario.GetScenarios (); @@ -83,19 +87,31 @@ static void Main (string [] args) } if (args.Length > 0) { var item = _scenarios.FindIndex (s => s.GetName ().Equals (args [0], StringComparison.OrdinalIgnoreCase)); - _runningScenario = (Scenario)Activator.CreateInstance (_scenarios [item].GetType()); + _selectedScenario = (Scenario)Activator.CreateInstance (_scenarios [item].GetType ()); Application.UseSystemConsole = _useSystemConsole; Application.Init (); - _runningScenario.Init (Application.Top, _baseColorScheme); - _runningScenario.Setup (); - _runningScenario.Run (); - _runningScenario = null; + _selectedScenario.Init (Application.Top, _colorScheme); + _selectedScenario.Setup (); + _selectedScenario.Run (); + _selectedScenario = null; Application.Shutdown (); return; } + _aboutMessage = new StringBuilder (); + _aboutMessage.AppendLine (@"A comprehensive sample library for"); + _aboutMessage.AppendLine (@""); + _aboutMessage.AppendLine (@" _______ _ _ _____ _ "); + _aboutMessage.AppendLine (@" |__ __| (_) | | / ____| (_) "); + _aboutMessage.AppendLine (@" | | ___ _ __ _ __ ___ _ _ __ __ _| || | __ _ _ _ "); + _aboutMessage.AppendLine (@" | |/ _ \ '__| '_ ` _ \| | '_ \ / _` | || | |_ | | | | | "); + _aboutMessage.AppendLine (@" | | __/ | | | | | | | | | | | (_| | || |__| | |_| | | "); + _aboutMessage.AppendLine (@" |_|\___|_| |_| |_| |_|_|_| |_|\__,_|_(_)_____|\__,_|_| "); + _aboutMessage.AppendLine (@""); + _aboutMessage.AppendLine (@"https://github.com/gui-cs/Terminal.Gui"); + Scenario scenario; - while ((scenario = GetScenarioToRun ()) != null) { + while ((scenario = SelectScenario ()) != null) { #if DEBUG_IDISPOSABLE // Validate there are no outstanding Responder-based instances // after a scenario was selected to run. This proves the main UI Catalog @@ -106,18 +122,12 @@ static void Main (string [] args) Responder.Instances.Clear (); #endif - scenario.Init (Application.Top, _baseColorScheme); + scenario.Init (Application.Top, _colorScheme); scenario.Setup (); scenario.Run (); - //static void LoadedHandler () - //{ - // _rightPane.SetFocus (); - // _top.Loaded -= LoadedHandler; - //} - - //_top.Loaded += LoadedHandler; - + // This call to Application.Shutdown brackets the Application.Init call + // made by Scenario.Init() Application.Shutdown (); #if DEBUG_IDISPOSABLE @@ -130,7 +140,9 @@ static void Main (string [] args) #endif } - Application.Shutdown (); + // This call to Application.Shutdown brackets the Application.Init call + // for the main UI Catalog app (in SelectScenario()). + //Application.Shutdown (); #if DEBUG_IDISPOSABLE // This proves that when the user exited the UI Catalog app @@ -143,31 +155,24 @@ static void Main (string [] args) } /// - /// This shows the selection UI. Each time it is run, it calls Application.Init to reset everything. + /// Shows the UI Catalog selection UI. When the user selects a Scenario to run, the + /// UI Catalog main app UI is killed and the Scenario is run as though it were Application.Top. + /// When the Scenario exits, this function exits. /// /// - private static Scenario GetScenarioToRun () + private static Scenario SelectScenario () { Application.UseSystemConsole = _useSystemConsole; Application.Init (); + if (_colorScheme == null) { + // `Colors` is not initilized until the ConsoleDriver is loaded by + // Application.Init. Set it only the first time though so it is + // preserved between running multiple Scenarios + _colorScheme = Colors.Base; + } Application.HeightAsBuffer = _heightAsBuffer; - // Set this here because not initialized until driver is loaded - _baseColorScheme = Colors.Base; - - StringBuilder aboutMessage = new StringBuilder (); - aboutMessage.AppendLine (@"A comprehensive sample library for"); - aboutMessage.AppendLine (@""); - aboutMessage.AppendLine (@" _______ _ _ _____ _ "); - aboutMessage.AppendLine (@" |__ __| (_) | | / ____| (_) "); - aboutMessage.AppendLine (@" | | ___ _ __ _ __ ___ _ _ __ __ _| || | __ _ _ _ "); - aboutMessage.AppendLine (@" | |/ _ \ '__| '_ ` _ \| | '_ \ / _` | || | |_ | | | | | "); - aboutMessage.AppendLine (@" | | __/ | | | | | | | | | | | (_| | || |__| | |_| | | "); - aboutMessage.AppendLine (@" |_|\___|_| |_| |_| |_|_|_| |_|\__,_|_(_)_____|\__,_|_| "); - aboutMessage.AppendLine (@""); - aboutMessage.AppendLine (@"https://github.com/gui-cs/Terminal.Gui"); - - _menu = new MenuBar (new MenuBarItem [] { + var menu = new MenuBar (new MenuBarItem [] { new MenuBarItem ("_File", new MenuItem [] { new MenuItem ("_Quit", "Quit UI Catalog", () => Application.RequestStop(), null, null, Key.Q | Key.CtrlMask) }), @@ -177,7 +182,7 @@ private static Scenario GetScenarioToRun () new MenuItem ("_gui.cs API Overview", "", () => OpenUrl ("https://gui-cs.github.io/Terminal.Gui/articles/overview.html"), null, null, Key.F1), new MenuItem ("gui.cs _README", "", () => OpenUrl ("https://github.com/gui-cs/Terminal.Gui"), null, null, Key.F2), new MenuItem ("_About...", - "About UI Catalog", () => MessageBox.Query ("About UI Catalog", aboutMessage.ToString(), "_Ok"), null, null, Key.CtrlMask | Key.A), + "About UI Catalog", () => MessageBox.Query ("About UI Catalog", _aboutMessage.ToString(), "_Ok"), null, null, Key.CtrlMask | Key.A), }), }); @@ -186,7 +191,7 @@ private static Scenario GetScenarioToRun () Y = 1, // for menu Width = 25, Height = Dim.Fill (1), - CanFocus = false, + CanFocus = true, Shortcut = Key.CtrlMask | Key.C }; _leftPane.Title = $"{_leftPane.Title} ({_leftPane.ShortcutTag})"; @@ -218,7 +223,7 @@ private static Scenario GetScenarioToRun () _rightPane.Title = $"{_rightPane.Title} ({_rightPane.ShortcutTag})"; _rightPane.ShortcutAction = () => _rightPane.SetFocus (); - _nameColumnWidth = _scenarios.OrderByDescending (s => s.GetName ().Length).FirstOrDefault ().GetName().Length; + _nameColumnWidth = _scenarios.OrderByDescending (s => s.GetName ().Length).FirstOrDefault ().GetName ().Length; _scenarioListView = new ListView () { X = 0, @@ -232,9 +237,6 @@ private static Scenario GetScenarioToRun () _scenarioListView.OpenSelectedItem += _scenarioListView_OpenSelectedItem; _rightPane.Add (_scenarioListView); - _categoryListView.SelectedItem = _categoryListViewItem; - _categoryListView.OnSelectedChanged (); - _capslock = new StatusItem (Key.CharMask, "Caps", null); _numlock = new StatusItem (Key.CharMask, "Num", null); _scrolllock = new StatusItem (Key.CharMask, "Scroll", null); @@ -247,60 +249,76 @@ private static Scenario GetScenarioToRun () _numlock, _scrolllock, new StatusItem(Key.Q | Key.CtrlMask, "~CTRL-Q~ Quit", () => { - if (_runningScenario is null){ + if (_selectedScenario is null){ // This causes GetScenarioToRun to return null - _runningScenario = null; + _selectedScenario = null; Application.RequestStop(); } else { - _runningScenario.RequestStop(); + _selectedScenario.RequestStop(); } }), new StatusItem(Key.F10, "~F10~ Hide/Show Status Bar", () => { _statusBar.Visible = !_statusBar.Visible; _leftPane.Height = Dim.Fill(_statusBar.Visible ? 1 : 0); _rightPane.Height = Dim.Fill(_statusBar.Visible ? 1 : 0); - _top.LayoutSubviews(); - _top.SetChildNeedsDisplay(); + Application.Top.LayoutSubviews(); + Application.Top.SetChildNeedsDisplay(); }), new StatusItem (Key.CharMask, Application.Driver.GetType ().Name, null), }; - SetColorScheme (); - _top = Application.Top; - _top.KeyDown += KeyDownHandler; - _top.Add (_menu); - _top.Add (_leftPane); - _top.Add (_rightPane); - _top.Add (_statusBar); - - void TopHandler () { - if (_runningScenario != null) { - _runningScenario = null; + Application.Top.ColorScheme = _colorScheme; + Application.Top.KeyDown += KeyDownHandler; + Application.Top.Add (menu); + Application.Top.Add (_leftPane); + Application.Top.Add (_rightPane); + Application.Top.Add (_statusBar); + + void TopHandler () + { + if (_selectedScenario != null) { + _selectedScenario = null; _isFirstRunning = false; } if (!_isFirstRunning) { _rightPane.SetFocus (); } - _top.Loaded -= TopHandler; + Application.Top.Loaded -= TopHandler; + } + Application.Top.Loaded += TopHandler; + + // Restore previous selections + _categoryListView.SelectedItem = _cachedCategoryIndex; + _scenarioListView.SelectedItem = _cachedScenarioIndex; + + // Run UI Catalog UI. When it exits, if _runningScenario is != null then + // a Scenario was selected. Otherwise, the user wants to exit UI Catalog. + Application.Run (Application.Top); + + // BUGBUG: Shouldn't Application.Shutdown() be called here? Why is it currently + // outside of the SelectScenario() loop? + Application.Shutdown (); + + return _selectedScenario; + } + + + /// + /// Launches the selected scenario, setting the global _runningScenario + /// + /// + private static void _scenarioListView_OpenSelectedItem (EventArgs e) + { + if (_selectedScenario is null) { + // Save selected item state + _cachedCategoryIndex = _categoryListView.SelectedItem; + _cachedScenarioIndex = _scenarioListView.SelectedItem; + // Create new instance of scenario (even though Scenarios contains instances) + _selectedScenario = (Scenario)Activator.CreateInstance (_scenarioListView.Source.ToList () [_scenarioListView.SelectedItem].GetType ()); + + // Tell the main app to stop + Application.RequestStop (); } - _top.Loaded += TopHandler; - // The following code was moved to the TopHandler event - // because in the MainLoop.EventsPending (wait) - // from the Application.RunLoop with the WindowsDriver - // the OnReady event is triggered due the Focus event. - // On CursesDriver and NetDriver the focus event won't be triggered - // and if it's possible I don't know how to do it. - //void ReadyHandler () - //{ - // if (!_isFirstRunning) { - // _rightPane.SetFocus (); - // } - // _top.Ready -= ReadyHandler; - //} - //_top.Ready += ReadyHandler; - - Application.Run (_top); - return _runningScenario; } static List CreateDiagnosticMenuItems () @@ -329,7 +347,7 @@ private static MenuItem [] CreateDisabledEnabledMouse () return menuItems.ToArray (); } - private static MenuItem[] CreateKeybindings() + private static MenuItem [] CreateKeybindings () { List menuItems = new List (); @@ -410,7 +428,7 @@ static MenuItem [] CreateDiagnosticFlagsMenuItems () } } ConsoleDriver.Diagnostics = _diagnosticFlags; - _top.SetNeedsDisplay (); + Application.Top.SetNeedsDisplay (); }; menuItems.Add (item); } @@ -464,14 +482,7 @@ void SetDiagnosticsFlag (Enum diag, bool add) } } - static void SetColorScheme () - { - _leftPane.ColorScheme = _baseColorScheme; - _rightPane.ColorScheme = _baseColorScheme; - _top?.SetNeedsDisplay (); - } - - static ColorScheme _baseColorScheme; + static ColorScheme _colorScheme; static MenuItem [] CreateColorSchemeMenuItems () { List menuItems = new List (); @@ -480,12 +491,12 @@ static MenuItem [] CreateColorSchemeMenuItems () item.Title = $"_{sc.Key}"; item.Shortcut = Key.AltMask | (Key)sc.Key.Substring (0, 1) [0]; item.CheckType |= MenuItemCheckStyle.Radio; - item.Checked = sc.Value == _baseColorScheme; + item.Checked = sc.Value == _colorScheme; item.Action += () => { - _baseColorScheme = sc.Value; - SetColorScheme (); + Application.Top.ColorScheme = _colorScheme = sc.Value; + Application.Top?.SetNeedsDisplay (); foreach (var menuItem in menuItems) { - menuItem.Checked = menuItem.Title.Equals ($"_{sc.Key}") && sc.Value == _baseColorScheme; + menuItem.Checked = menuItem.Title.Equals ($"_{sc.Key}") && sc.Value == _colorScheme; } }; menuItems.Add (item); @@ -493,16 +504,6 @@ static MenuItem [] CreateColorSchemeMenuItems () return menuItems.ToArray (); } - private static void _scenarioListView_OpenSelectedItem (EventArgs e) - { - if (_runningScenario is null) { - _scenarioListViewItem = _scenarioListView.SelectedItem; - // Create new instance of scenario (even though Scenarios contains instnaces) - _runningScenario = (Scenario)Activator.CreateInstance (_scenarioListView.Source.ToList() [_scenarioListView.SelectedItem].GetType()); - Application.RequestStop (); - } - } - /// /// When Scenarios are running we need to override the behavior of the Menu /// and Statusbar to enable Scenarios that use those (or related key input) @@ -511,14 +512,6 @@ private static void _scenarioListView_OpenSelectedItem (EventArgs e) /// private static void KeyDownHandler (View.KeyEventEventArgs a) { - //if (a.KeyEvent.Key == Key.Tab || a.KeyEvent.Key == Key.BackTab) { - // // BUGBUG: Work around Issue #434 by implementing our own TAB navigation - // if (_top.MostFocused == _categoryListView) - // _top.SetFocus (_rightPane); - // else - // _top.SetFocus (_leftPane); - //} - if (a.KeyEvent.IsCapslock) { _capslock.Title = "Caps: On"; _statusBar.SetNeedsDisplay (); @@ -546,22 +539,16 @@ private static void KeyDownHandler (View.KeyEventEventArgs a) private static void CategoryListView_SelectedChanged (ListViewItemEventArgs e) { - if (_categoryListViewItem != _categoryListView.SelectedItem) { - _scenarioListViewItem = 0; - } - _categoryListViewItem = _categoryListView.SelectedItem; - var item = _categories [_categoryListViewItem]; + var item = _categories [e.Item]; List newlist; - if (_categoryListViewItem == 0) { + if (e.Item == 0) { // First category is "All" newlist = _scenarios; } else { newlist = _scenarios.Where (s => s.GetCategories ().Contains (item)).ToList (); } - _scenarioListView.SetSource(newlist.ToList()); - _scenarioListView.SelectedItem = _scenarioListViewItem; - + _scenarioListView.SetSource (newlist.ToList ()); } private static void OpenUrl (string url) From ebd01fc1068b9bc916dbf307657fc14963957063 Mon Sep 17 00:00:00 2001 From: Tig Kindel Date: Sat, 29 Oct 2022 18:51:15 -0600 Subject: [PATCH 08/25] TreeView example written; not wired up yet --- .../SearchCollectionNavigatorTester.cs | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/UICatalog/Scenarios/SearchCollectionNavigatorTester.cs b/UICatalog/Scenarios/SearchCollectionNavigatorTester.cs index 1e731dfd16..30ae6111c0 100644 --- a/UICatalog/Scenarios/SearchCollectionNavigatorTester.cs +++ b/UICatalog/Scenarios/SearchCollectionNavigatorTester.cs @@ -8,11 +8,9 @@ namespace UICatalog.Scenarios { [ScenarioMetadata (Name: "Search Collection Nav", Description: "Demonstrates & tests SearchCollectionNavigator.")] [ScenarioCategory ("Controls"), ScenarioCategory ("ListView")] + [ScenarioCategory ("Controls"), ScenarioCategory ("TreeView")] + [ScenarioCategory ("Controls"), ScenarioCategory ("Text")] public class SearchCollectionNavigatorTester : Scenario { - TabView tabView; - - private int numbeOfNewTabs = 1; - // Don't create a Window, just return the top-level view public override void Init (Toplevel top, ColorScheme colorScheme) { @@ -55,6 +53,7 @@ public override void Setup () Height = Dim.Fill () }; Top.Add (vsep); + CreateTreeView (); } @@ -136,7 +135,7 @@ private void CreateListView () _listView.SetSource (items); } - TreeView _treeView = null; + TreeView _treeView = null; private void CreateTreeView () { @@ -150,7 +149,7 @@ private void CreateTreeView () }; Top.Add (label); - _treeView = new TreeView () { + _treeView = new TreeView () { X = Pos.Right (_listView) + 2, Y = Pos.Bottom (label), Width = Dim.Percent (50) - 1, @@ -159,7 +158,8 @@ private void CreateTreeView () }; Top.Add (_treeView); - System.Collections.Generic.List items = new string [] { "a", + System.Collections.Generic.List items = new string [] { + "a", "b", "bb", "c", @@ -207,8 +207,14 @@ private void CreateTreeView () "quit", "quitter" }.ToList (); + items.Sort (StringComparer.OrdinalIgnoreCase); - _treeView.AddObjects (items); + var root = new TreeNode ("Alpha examples"); + root.Children = items.Where (i => char.IsLetterOrDigit (i [0])).Select (i => new TreeNode (i)).Cast().ToList (); + _treeView.AddObject (root); + root = new TreeNode ("Non-Alpha examples"); + root.Children = items.Where (i => !char.IsLetterOrDigit (i [0])).Select (i => new TreeNode (i)).Cast ().ToList (); + _treeView.AddObject (root); } private void Quit () { From 1e17cf0202a5dd59a3979e0ca9e53289af4b2e9c Mon Sep 17 00:00:00 2001 From: Tig Kindel Date: Sat, 29 Oct 2022 19:32:50 -0600 Subject: [PATCH 09/25] tweaks --- Terminal.Gui/Views/ListView.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Terminal.Gui/Views/ListView.cs b/Terminal.Gui/Views/ListView.cs index 1736209d9e..1513cb7ed3 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -126,6 +126,7 @@ public IListDataSource Source { get => source; set { source = value; + navigator = null; top = 0; selected = 0; lastSelectedItem = -1; @@ -439,7 +440,6 @@ public override bool ProcessKey (KeyEvent kb) // Enable user to find & select an item by typing text if (!kb.IsAlt && !kb.IsCapslock && !kb.IsCtrl && !kb.IsScrolllock && !kb.IsNumlock) { if (navigator == null) { - // BUGBUG: If items change this needs to be recreated. navigator = new SearchCollectionNavigator (source.ToList ().Cast ()); } var newItem = navigator.CalculateNewIndex (SelectedItem, (char)kb.KeyValue); @@ -744,7 +744,7 @@ public override bool OnEnter (View view) if (lastSelectedItem == -1) { EnsuresVisibilitySelectedItem (); - //OnSelectedChanged (); + OnSelectedChanged (); } return base.OnEnter (view); From 79f82d1c4c54b784e8fa2b7c6d33e49cb098d8ad Mon Sep 17 00:00:00 2001 From: tznind Date: Sun, 30 Oct 2022 09:38:31 +0000 Subject: [PATCH 10/25] Add SearchCollectionNavigator to TreeView --- .../Core/SearchCollectionNavigator.cs | 12 +++++ Terminal.Gui/Views/ListView.cs | 2 +- Terminal.Gui/Views/TreeView.cs | 51 ++++++++++++++----- 3 files changed, 51 insertions(+), 14 deletions(-) diff --git a/Terminal.Gui/Core/SearchCollectionNavigator.cs b/Terminal.Gui/Core/SearchCollectionNavigator.cs index 34424dc428..1ebb0dd199 100644 --- a/Terminal.Gui/Core/SearchCollectionNavigator.cs +++ b/Terminal.Gui/Core/SearchCollectionNavigator.cs @@ -129,5 +129,17 @@ private void ClearState () lastKeystroke = DateTime.MinValue; } + + /// + /// Returns true if is a searchable key + /// (e.g. letters, numbers etc) that is valid to pass to to this + /// class for search filtering + /// + /// + /// + public static bool IsCompatibleKey (KeyEvent kb) + { + return !kb.IsAlt && !kb.IsCapslock && !kb.IsCtrl && !kb.IsScrolllock && !kb.IsNumlock; + } } } diff --git a/Terminal.Gui/Views/ListView.cs b/Terminal.Gui/Views/ListView.cs index 1736209d9e..b1df9dd805 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -437,7 +437,7 @@ public override bool ProcessKey (KeyEvent kb) } // Enable user to find & select an item by typing text - if (!kb.IsAlt && !kb.IsCapslock && !kb.IsCtrl && !kb.IsScrolllock && !kb.IsNumlock) { + if (SearchCollectionNavigator.IsCompatibleKey(kb)) { if (navigator == null) { // BUGBUG: If items change this needs to be recreated. navigator = new SearchCollectionNavigator (source.ToList ().Cast ()); diff --git a/Terminal.Gui/Views/TreeView.cs b/Terminal.Gui/Views/TreeView.cs index 4f8692d743..8628964d59 100644 --- a/Terminal.Gui/Views/TreeView.cs +++ b/Terminal.Gui/Views/TreeView.cs @@ -140,12 +140,12 @@ public Key ObjectActivationKey { /// public MouseFlags? ObjectActivationButton { get; set; } = MouseFlags.Button1DoubleClicked; - + /// /// Delegate for multi colored tree views. Return the to use /// for each passed object or null to use the default. /// - public Func ColorGetter {get;set;} + public Func ColorGetter { get; set; } /// /// Secondary selected regions of tree when is true @@ -220,6 +220,7 @@ public int ScrollOffsetHorizontal { public AspectGetterDelegate AspectGetter { get; set; } = (o) => o.ToString () ?? ""; CursorVisibility desiredCursorVisibility = CursorVisibility.Invisible; + private SearchCollectionNavigator searchCollectionNavigator; /// /// Get / Set the wished cursor when the tree is focused. @@ -227,7 +228,7 @@ public int ScrollOffsetHorizontal { /// Defaults to /// public CursorVisibility DesiredCursorVisibility { - get { + get { return MultiSelect ? desiredCursorVisibility : CursorVisibility.Invisible; } set { @@ -576,19 +577,43 @@ public override bool ProcessKey (KeyEvent keyEvent) return false; } - // if it is a single character pressed without any control keys - if (keyEvent.KeyValue > 0 && keyEvent.KeyValue < 0xFFFF) { + try { + + // First of all deal with any registered keybindings + var result = InvokeKeybindings (keyEvent); + if (result != null) { + return (bool)result; + } + + // If not a keybinding, is the key a searchable key press? + if (SearchCollectionNavigator.IsCompatibleKey (keyEvent) && AllowLetterBasedNavigation) { + + IReadOnlyCollection> map; + + // If there has been a call to InvalidateMap since the last time we allocated a + // SearchCollectionNavigator then we need a new one to reflect the new exposed + // tree state + if (cachedLineMap == null || searchCollectionNavigator == null) { + map = BuildLineMap (); + searchCollectionNavigator = new SearchCollectionNavigator (map.Select (b => AspectGetter (b.Model)).ToArray ()); + } + else { + // we still need the map, handily its the cached one which means super fast access + map = BuildLineMap (); + } + + // Find the current selected object within the tree + var current = map.IndexOf (b => b.Model == SelectedObject); + var newIndex = searchCollectionNavigator.CalculateNewIndex (current, (char)keyEvent.KeyValue); + + if (newIndex != -1) { + SelectedObject = map.ElementAt (newIndex).Model; + SetNeedsDisplay (); + } - if (char.IsLetterOrDigit ((char)keyEvent.KeyValue) && AllowLetterBasedNavigation && !keyEvent.IsShift && !keyEvent.IsAlt && !keyEvent.IsCtrl) { - AdjustSelectionToNextItemBeginningWith ((char)keyEvent.KeyValue); return true; } - } - try { - var result = InvokeKeybindings (keyEvent); - if (result != null) - return (bool)result; } finally { PositionCursor (); @@ -626,7 +651,7 @@ public void ActivateSelectedObjectIfAny () /// /// /// - public int? GetObjectRow(T toFind) + public int? GetObjectRow (T toFind) { var idx = BuildLineMap ().IndexOf (o => o.Model.Equals (toFind)); From a6240807c9066278957660ad8f64759dbe043083 Mon Sep 17 00:00:00 2001 From: tznind Date: Sun, 30 Oct 2022 09:40:45 +0000 Subject: [PATCH 11/25] Add EnsureVisible call --- Terminal.Gui/Views/TreeView.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Terminal.Gui/Views/TreeView.cs b/Terminal.Gui/Views/TreeView.cs index 8628964d59..b267916084 100644 --- a/Terminal.Gui/Views/TreeView.cs +++ b/Terminal.Gui/Views/TreeView.cs @@ -608,6 +608,7 @@ public override bool ProcessKey (KeyEvent keyEvent) if (newIndex != -1) { SelectedObject = map.ElementAt (newIndex).Model; + EnsureVisible (selectedObject); SetNeedsDisplay (); } From c2a8d01f394ddc0cbee151bd290a8dc03205dc22 Mon Sep 17 00:00:00 2001 From: tznind Date: Sun, 30 Oct 2022 10:06:26 +0000 Subject: [PATCH 12/25] Added tests for 'bad' indexes being passed to SearchCollectionNavigator --- UnitTests/SearchCollectionNavigatorTests.cs | 36 +++++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/UnitTests/SearchCollectionNavigatorTests.cs b/UnitTests/SearchCollectionNavigatorTests.cs index eea4c76d0f..b59f8f7342 100644 --- a/UnitTests/SearchCollectionNavigatorTests.cs +++ b/UnitTests/SearchCollectionNavigatorTests.cs @@ -3,25 +3,41 @@ namespace Terminal.Gui.Core { public class SearchCollectionNavigatorTests { + static string [] simpleStrings = new string []{ + "appricot", // 0 + "arm", // 1 + "bat", // 2 + "batman", // 3 + "candle" // 4 + }; + [Fact] + public void TestSearchCollectionNavigator_ShouldAcceptNegativeOne () + { + var n = new SearchCollectionNavigator (simpleStrings); + + // Expect that index of -1 (i.e. no selection) should work correctly + // and select the first entry of the letter 'b' + Assert.Equal (2, n.CalculateNewIndex (-1, 'b')); + } + [Fact] + public void TestSearchCollectionNavigator_OutOfBoundsShouldBeIgnored() + { + var n = new SearchCollectionNavigator (simpleStrings); + + // Expect saying that index 500 is the current selection should not cause + // error and just be ignored (treated as no selection) + Assert.Equal (2, n.CalculateNewIndex (500, 'b')); + } [Fact] public void TestSearchCollectionNavigator_Cycling () { - var strings = new string []{ - "appricot", - "arm", - "bat", - "batman", - "candle" - }; - - var n = new SearchCollectionNavigator (strings); + var n = new SearchCollectionNavigator (simpleStrings); Assert.Equal (2, n.CalculateNewIndex ( 0, 'b')); Assert.Equal (3, n.CalculateNewIndex ( 2, 'b')); // if 4 (candle) is selected it should loop back to bat Assert.Equal (2, n.CalculateNewIndex ( 4, 'b')); - } From b713d6a46787139a366972a59611b7e4ab05ed9c Mon Sep 17 00:00:00 2001 From: Charlie Kindel Date: Sun, 30 Oct 2022 12:58:18 -0600 Subject: [PATCH 13/25] Integrating tznid's latest --- Terminal.Gui/Core/SearchCollectionNavigator.cs | 4 +++- Terminal.Gui/Views/TreeView.cs | 7 +++---- UICatalog/Scenarios/SearchCollectionNavigatorTester.cs | 9 +++++---- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/Terminal.Gui/Core/SearchCollectionNavigator.cs b/Terminal.Gui/Core/SearchCollectionNavigator.cs index 1ebb0dd199..8328154e3d 100644 --- a/Terminal.Gui/Core/SearchCollectionNavigator.cs +++ b/Terminal.Gui/Core/SearchCollectionNavigator.cs @@ -139,7 +139,9 @@ private void ClearState () /// public static bool IsCompatibleKey (KeyEvent kb) { - return !kb.IsAlt && !kb.IsCapslock && !kb.IsCtrl && !kb.IsScrolllock && !kb.IsNumlock; + // For some reason, at least on Windows/Windows Terminal, `$` is coming through with `IsAlt == true` + //return !kb.IsAlt && !kb.IsCapslock && !kb.IsCtrl && !kb.IsScrolllock && !kb.IsNumlock; + return !kb.IsCapslock && !kb.IsCtrl && !kb.IsScrolllock && !kb.IsNumlock; } } } diff --git a/Terminal.Gui/Views/TreeView.cs b/Terminal.Gui/Views/TreeView.cs index b267916084..3084383937 100644 --- a/Terminal.Gui/Views/TreeView.cs +++ b/Terminal.Gui/Views/TreeView.cs @@ -594,7 +594,7 @@ public override bool ProcessKey (KeyEvent keyEvent) // SearchCollectionNavigator then we need a new one to reflect the new exposed // tree state if (cachedLineMap == null || searchCollectionNavigator == null) { - map = BuildLineMap (); + map = BuildLineMap (); searchCollectionNavigator = new SearchCollectionNavigator (map.Select (b => AspectGetter (b.Model)).ToArray ()); } else { @@ -606,13 +606,12 @@ public override bool ProcessKey (KeyEvent keyEvent) var current = map.IndexOf (b => b.Model == SelectedObject); var newIndex = searchCollectionNavigator.CalculateNewIndex (current, (char)keyEvent.KeyValue); - if (newIndex != -1) { + if (newIndex != current) { SelectedObject = map.ElementAt (newIndex).Model; EnsureVisible (selectedObject); SetNeedsDisplay (); + return true; } - - return true; } } finally { diff --git a/UICatalog/Scenarios/SearchCollectionNavigatorTester.cs b/UICatalog/Scenarios/SearchCollectionNavigatorTester.cs index 30ae6111c0..67533675c9 100644 --- a/UICatalog/Scenarios/SearchCollectionNavigatorTester.cs +++ b/UICatalog/Scenarios/SearchCollectionNavigatorTester.cs @@ -150,9 +150,9 @@ private void CreateTreeView () Top.Add (label); _treeView = new TreeView () { - X = Pos.Right (_listView) + 2, + X = Pos.Right (_listView) + 1, Y = Pos.Bottom (label), - Width = Dim.Percent (50) - 1, + Width = Dim.Fill (), Height = Dim.Fill (), ColorScheme = Colors.TopLevel }; @@ -210,11 +210,12 @@ private void CreateTreeView () items.Sort (StringComparer.OrdinalIgnoreCase); var root = new TreeNode ("Alpha examples"); - root.Children = items.Where (i => char.IsLetterOrDigit (i [0])).Select (i => new TreeNode (i)).Cast().ToList (); - _treeView.AddObject (root); + //root.Children = items.Where (i => char.IsLetterOrDigit (i [0])).Select (i => new TreeNode (i)).Cast().ToList (); + //_treeView.AddObject (root); root = new TreeNode ("Non-Alpha examples"); root.Children = items.Where (i => !char.IsLetterOrDigit (i [0])).Select (i => new TreeNode (i)).Cast ().ToList (); _treeView.AddObject (root); + _treeView.ExpandAll (); } private void Quit () { From 1b2dc4023c228c27a21aec9fdff9abce27231036 Mon Sep 17 00:00:00 2001 From: Charlie Kindel Date: Mon, 31 Oct 2022 09:01:50 -0600 Subject: [PATCH 14/25] merge --- .../Core/SearchCollectionNavigator.cs | 4 +- Terminal.Gui/Views/ListView.cs | 1 - UICatalog/Scenarios/Keys.cs | 4 +- .../SearchCollectionNavigatorTester.cs | 162 +++++++----------- 4 files changed, 62 insertions(+), 109 deletions(-) diff --git a/Terminal.Gui/Core/SearchCollectionNavigator.cs b/Terminal.Gui/Core/SearchCollectionNavigator.cs index 8328154e3d..360aab5cf0 100644 --- a/Terminal.Gui/Core/SearchCollectionNavigator.cs +++ b/Terminal.Gui/Core/SearchCollectionNavigator.cs @@ -140,8 +140,8 @@ private void ClearState () public static bool IsCompatibleKey (KeyEvent kb) { // For some reason, at least on Windows/Windows Terminal, `$` is coming through with `IsAlt == true` - //return !kb.IsAlt && !kb.IsCapslock && !kb.IsCtrl && !kb.IsScrolllock && !kb.IsNumlock; - return !kb.IsCapslock && !kb.IsCtrl && !kb.IsScrolllock && !kb.IsNumlock; + return !kb.IsAlt && !kb.IsCapslock && !kb.IsCtrl && !kb.IsScrolllock && !kb.IsNumlock; + //return !kb.IsCapslock && !kb.IsCtrl && !kb.IsScrolllock && !kb.IsNumlock; } } } diff --git a/Terminal.Gui/Views/ListView.cs b/Terminal.Gui/Views/ListView.cs index 2cf853f29d..95a96dce6d 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -691,7 +691,6 @@ public virtual bool ScrollLeft (int cols) int lastSelectedItem = -1; private bool allowsMultipleSelection = true; - private System.Timers.Timer searchTimer; /// /// Invokes the event if it is defined. diff --git a/UICatalog/Scenarios/Keys.cs b/UICatalog/Scenarios/Keys.cs index 7880c952e0..78259dd3bc 100644 --- a/UICatalog/Scenarios/Keys.cs +++ b/UICatalog/Scenarios/Keys.cs @@ -51,8 +51,8 @@ public override bool ProcessColdKey (KeyEvent keyEvent) public override void Init (Toplevel top, ColorScheme colorScheme) { Application.Init (); - Top = top != null ? top : Application.Top; - + Top = top != null ? top : Application.Top != null ? top : Application.Top; + Win = new TestWindow ($"CTRL-Q to Close - Scenario: {GetName ()}") { X = 0, Y = 0, diff --git a/UICatalog/Scenarios/SearchCollectionNavigatorTester.cs b/UICatalog/Scenarios/SearchCollectionNavigatorTester.cs index 67533675c9..10feff7c31 100644 --- a/UICatalog/Scenarios/SearchCollectionNavigatorTester.cs +++ b/UICatalog/Scenarios/SearchCollectionNavigatorTester.cs @@ -19,6 +19,59 @@ public override void Init (Toplevel top, ColorScheme colorScheme) Top.ColorScheme = Colors.Base; } + System.Collections.Generic.List _items = new string [] { + "a", + "b", + "bb", + "c", + "ccc", + "ccc", + "cccc", + "ddd", + "dddd", + "dddd", + "ddddd", + "dddddd", + "ddddddd", + "this", + "this is a test", + "this was a test", + "this and", + "that and that", + "the", + "think", + "thunk", + "thunks", + "zip", + "zap", + "zoo", + "@jack", + "@sign", + "@at", + "@ateme", + "n@", + "n@brown", + ".net", + "$100.00", + "$101.00", + "$101.10", + "$101.11", + "$200.00", + "$210.99", + "$$", + "appricot", + "arm", + "丗丙业丞", + "丗丙丛", + "text", + "egg", + "candle", + " <- space", + "q", + "quit", + "quitter" + }.ToList (); + public override void Setup () { var allowMarking = new MenuItem ("Allow _Marking", "", null) { @@ -46,6 +99,8 @@ public override void Setup () Top.Add (menu); + _items.Sort (StringComparer.OrdinalIgnoreCase); + CreateListView (); var vsep = new LineView (Terminal.Gui.Graphs.Orientation.Vertical) { X = Pos.Right (_listView), @@ -81,58 +136,8 @@ private void CreateListView () ColorScheme = Colors.TopLevel }; Top.Add (_listView); - - System.Collections.Generic.List items = new string [] { - "a", - "b", - "bb", - "c", - "ccc", - "ccc", - "cccc", - "ddd", - "dddd", - "dddd", - "ddddd", - "dddddd", - "ddddddd", - "this", - "this is a test", - "this was a test", - "this and", - "that and that", - "the", - "think", - "thunk", - "thunks", - "zip", - "zap", - "zoo", - "@jack", - "@sign", - "@at", - "@ateme", - "n@", - "n@brown", - ".net", - "$100.00", - "$101.00", - "$101.10", - "$101.11", - "appricot", - "arm", - "丗丙业丞", - "丗丙丛", - "text", - "egg", - "candle", - " <- space", - "q", - "quit", - "quitter" - }.ToList (); - items.Sort (StringComparer.OrdinalIgnoreCase); - _listView.SetSource (items); + + _listView.SetSource (_items); } TreeView _treeView = null; @@ -157,63 +162,12 @@ private void CreateTreeView () ColorScheme = Colors.TopLevel }; Top.Add (_treeView); - - System.Collections.Generic.List items = new string [] { - "a", - "b", - "bb", - "c", - "ccc", - "ccc", - "cccc", - "ddd", - "dddd", - "dddd", - "ddddd", - "dddddd", - "ddddddd", - "this", - "this is a test", - "this was a test", - "this and", - "that and that", - "the", - "think", - "thunk", - "thunks", - "zip", - "zap", - "zoo", - "@jack", - "@sign", - "@at", - "@ateme", - "n@", - "n@brown", - ".net", - "$100.00", - "$101.00", - "$101.10", - "$101.11", - "appricot", - "arm", - "丗丙业丞", - "丗丙丛", - "text", - "egg", - "candle", - " <- space", - "q", - "quit", - "quitter" - }.ToList (); - items.Sort (StringComparer.OrdinalIgnoreCase); var root = new TreeNode ("Alpha examples"); //root.Children = items.Where (i => char.IsLetterOrDigit (i [0])).Select (i => new TreeNode (i)).Cast().ToList (); //_treeView.AddObject (root); root = new TreeNode ("Non-Alpha examples"); - root.Children = items.Where (i => !char.IsLetterOrDigit (i [0])).Select (i => new TreeNode (i)).Cast ().ToList (); + root.Children = _items.Where (i => !char.IsLetterOrDigit (i [0])).Select (i => new TreeNode (i)).Cast ().ToList (); _treeView.AddObject (root); _treeView.ExpandAll (); } From 60d116617ae4fe1895343ff073428202dc3638da Mon Sep 17 00:00:00 2001 From: Charlie Kindel Date: Mon, 31 Oct 2022 21:23:26 -0600 Subject: [PATCH 15/25] Near final fixes? Refactored/renamed stuff --- .../Core/SearchCollectionNavigator.cs | 169 ++++++--- Terminal.Gui/Core/Trees/Branch.cs | 6 +- Terminal.Gui/Views/ListView.cs | 40 +-- Terminal.Gui/Views/TreeView.cs | 287 +++++++-------- UICatalog/Properties/launchSettings.json | 4 + .../SearchCollectionNavigatorTester.cs | 26 +- UnitTests/ListViewTests.cs | 2 +- UnitTests/SearchCollectionNavigatorTests.cs | 326 ++++++++++++++---- 8 files changed, 555 insertions(+), 305 deletions(-) diff --git a/Terminal.Gui/Core/SearchCollectionNavigator.cs b/Terminal.Gui/Core/SearchCollectionNavigator.cs index 360aab5cf0..6c02b96648 100644 --- a/Terminal.Gui/Core/SearchCollectionNavigator.cs +++ b/Terminal.Gui/Core/SearchCollectionNavigator.cs @@ -4,25 +4,100 @@ namespace Terminal.Gui { /// - /// Changes the index in a collection based on keys pressed - /// and the current state + /// Navigates a collection of items using keystrokes. The keystrokes are used to build a search string. + /// The is used to find the next item in the collection that matches the search string + /// when is called. + /// + /// If the user types keystrokes that can't be found in the collection, + /// the search string is cleared and the next item is found that starts with the last keystroke. + /// + /// + /// If the user pauses keystrokes for a short time (250ms), the search string is cleared. + /// /// - class SearchCollectionNavigator { - string state = ""; - DateTime lastKeystroke = DateTime.MinValue; - const int TypingDelay = 250; + public class SearchCollectionNavigator { + /// + /// Constructs a new SearchCollectionNavigator. + /// + public SearchCollectionNavigator () { } + + /// + /// Constructs a new SearchCollectionNavigator for the given collection. + /// + /// + public SearchCollectionNavigator (IEnumerable collection) => Collection = collection; + + DateTime lastKeystroke = DateTime.Now; + internal int TypingDelay { get; set; } = 250; + + /// + /// The compararer function to use when searching the collection. + /// public StringComparer Comparer { get; set; } = StringComparer.InvariantCultureIgnoreCase; - private IEnumerable Collection { get => _collection; set => _collection = value; } - private IEnumerable _collection; + /// + /// The collection of objects to search. is used to search the collection. + /// + public IEnumerable Collection { get; set; } + + /// + /// Event arguments for the event. + /// + public class KeystrokeNavigatorEventArgs { + /// + /// he current . + /// + public string SearchString { get; } + + /// + /// Initializes a new instance of + /// + /// The current . + public KeystrokeNavigatorEventArgs (string searchString) + { + SearchString = searchString; + } + } - public SearchCollectionNavigator (IEnumerable collection) { _collection = collection; } + /// + /// This event is invoked when changes. Useful for debugging. + /// + public event Action SearchStringChanged; + private string _searchString = ""; + /// + /// Gets the current search string. This includes the set of keystrokes that have been pressed + /// since the last unsuccessful match or after a 250ms delay. Useful for debugging. + /// + public string SearchString { + get => _searchString; + private set { + _searchString = value; + OnSearchStringChanged (new KeystrokeNavigatorEventArgs (value)); + } + } - public int CalculateNewIndex (IEnumerable collection, int currentIndex, char keyStruck) + /// + /// Invoked when the changes. Useful for debugging. Invokes the event. + /// + /// + public virtual void OnSearchStringChanged (KeystrokeNavigatorEventArgs e) { - // if user presses a key - if (!char.IsControl(keyStruck)) {//char.IsLetterOrDigit (keyStruck) || char.IsPunctuation (keyStruck) || char.IsSymbol(keyStruck)) { + SearchStringChanged?.Invoke (e); + } + + /// + /// Gets the index of the next item in the collection that matches the current plus the provided character (typically + /// from a key press). + /// + /// The index in the collection to start the search from. + /// The character of the key the user pressed. + /// The index of the item that matches what the user has typed. + /// Returns if no item in the collection matched. + public int GetNextMatchingItem (int currentIndex, char keyStruck) + { + AssertCollectionIsNotNull (); + if (!char.IsControl (keyStruck)) { // maybe user pressed 'd' and now presses 'd' again. // a candidate search is things that begin with "dd" @@ -31,40 +106,39 @@ public int CalculateNewIndex (IEnumerable collection, int currentIndex, string candidateState = ""; // is it a second or third (etc) keystroke within a short time - if (state.Length > 0 && DateTime.Now - lastKeystroke < TimeSpan.FromMilliseconds (TypingDelay)) { + if (SearchString.Length > 0 && DateTime.Now - lastKeystroke < TimeSpan.FromMilliseconds (TypingDelay)) { // "dd" is a candidate - candidateState = state + keyStruck; + candidateState = SearchString + keyStruck; } else { // its a fresh keystroke after some time // or its first ever key press - state = new string (keyStruck, 1); + SearchString = new string (keyStruck, 1); } - var idxCandidate = GetNextIndexMatching (collection, currentIndex, candidateState, + var idxCandidate = GetNextMatchingItem (currentIndex, candidateState, // prefer not to move if there are multiple characters e.g. "ca" + 'r' should stay on "car" and not jump to "cart" candidateState.Length > 1); if (idxCandidate != -1) { // found "dd" so candidate state is accepted lastKeystroke = DateTime.Now; - state = candidateState; + SearchString = candidateState; return idxCandidate; } - - // nothing matches "dd" so discard it as a candidate - // and just cycle "d" instead + //// nothing matches "dd" so discard it as a candidate + //// and just cycle "d" instead lastKeystroke = DateTime.Now; - idxCandidate = GetNextIndexMatching (collection, currentIndex, state); + idxCandidate = GetNextMatchingItem (currentIndex, candidateState); // if no changes to current state manifested if (idxCandidate == currentIndex || idxCandidate == -1) { // clear history and treat as a fresh letter ClearState (); - + // match on the fresh letter alone - state = new string (keyStruck, 1); - idxCandidate = GetNextIndexMatching (collection, currentIndex, state); + SearchString = new string (keyStruck, 1); + idxCandidate = GetNextMatchingItem (currentIndex, SearchString); return idxCandidate == -1 ? currentIndex : idxCandidate; } @@ -72,28 +146,35 @@ public int CalculateNewIndex (IEnumerable collection, int currentIndex, return idxCandidate; } else { - // clear state because keypress was non letter + // clear state because keypress was a control char ClearState (); - // no change in index for non letter keystrokes - return currentIndex; + // control char indicates no selection + return -1; } } - public int CalculateNewIndex (int currentIndex, char keyStruck) - { - return CalculateNewIndex (Collection, currentIndex, keyStruck); - } - - private int GetNextIndexMatching (IEnumerable collection, int currentIndex, string search, bool preferNotToMoveToNewIndexes = false) + /// + /// Gets the index of the next item in the collection that matches the current + /// + /// The index in the collection to start the search from. + /// The search string to use. + /// Set to to stop the search on the first match + /// if there are multiple matches for . + /// e.g. "ca" + 'r' should stay on "car" and not jump to "cart". If (the default), + /// the next matching item will be returned, even if it is above in the collection. + /// + /// + internal int GetNextMatchingItem (int currentIndex, string search, bool minimizeMovement = false) { if (string.IsNullOrEmpty (search)) { return -1; } + AssertCollectionIsNotNull (); // find indexes of items that start with the search text - int [] matchingIndexes = collection.Select ((item, idx) => (item, idx)) - .Where (k => k.item?.ToString().StartsWith (search, StringComparison.InvariantCultureIgnoreCase) ?? false) + int [] matchingIndexes = Collection.Select ((item, idx) => (item, idx)) + .Where (k => k.item?.ToString ().StartsWith (search, StringComparison.InvariantCultureIgnoreCase) ?? false) .Select (k => k.idx) .ToArray (); @@ -109,7 +190,7 @@ private int GetNextIndexMatching (IEnumerable collection, int currentInd } else { // the current index is part of the matching collection - if (preferNotToMoveToNewIndexes) { + if (minimizeMovement) { // if we would rather not jump around (e.g. user is typing lots of text to get this match) return matchingIndexes [currentlySelected]; } @@ -123,25 +204,29 @@ private int GetNextIndexMatching (IEnumerable collection, int currentInd return -1; } - private void ClearState () + private void AssertCollectionIsNotNull () { - state = ""; - lastKeystroke = DateTime.MinValue; + if (Collection == null) { + throw new InvalidOperationException ("Collection is null"); + } + } + private void ClearState () + { + SearchString = ""; + lastKeystroke = DateTime.Now; } /// /// Returns true if is a searchable key /// (e.g. letters, numbers etc) that is valid to pass to to this - /// class for search filtering + /// class for search filtering. /// /// /// public static bool IsCompatibleKey (KeyEvent kb) { - // For some reason, at least on Windows/Windows Terminal, `$` is coming through with `IsAlt == true` return !kb.IsAlt && !kb.IsCapslock && !kb.IsCtrl && !kb.IsScrolllock && !kb.IsNumlock; - //return !kb.IsCapslock && !kb.IsCtrl && !kb.IsScrolllock && !kb.IsNumlock; } } } diff --git a/Terminal.Gui/Core/Trees/Branch.cs b/Terminal.Gui/Core/Trees/Branch.cs index 35a81965a2..a6d43cb0b1 100644 --- a/Terminal.Gui/Core/Trees/Branch.cs +++ b/Terminal.Gui/Core/Trees/Branch.cs @@ -89,8 +89,8 @@ public virtual int GetWidth (ConsoleDriver driver) public virtual void Draw (ConsoleDriver driver, ColorScheme colorScheme, int y, int availableWidth) { // true if the current line of the tree is the selected one and control has focus - bool isSelected = tree.IsSelected (Model) && tree.HasFocus; - Attribute lineColor = isSelected ? colorScheme.Focus : colorScheme.Normal; + bool isSelected = tree.IsSelected (Model);// && tree.HasFocus; + Attribute lineColor = isSelected ? (tree.HasFocus ? colorScheme.HotFocus : colorScheme.HotNormal) : colorScheme.Normal ; driver.SetAttribute (lineColor); @@ -418,7 +418,7 @@ internal bool IsHitOnExpandableSymbol (ConsoleDriver driver, int x) /// Expands the current branch and all children branches /// internal void ExpandAll () - { + { Expand (); if (ChildBranches != null) { diff --git a/Terminal.Gui/Views/ListView.cs b/Terminal.Gui/Views/ListView.cs index 95a96dce6d..388def50ac 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -59,21 +59,6 @@ public interface IListDataSource { IList ToList (); } - /// - /// Implement to provide custom rendering for a that - /// supports searching for items. - /// - public interface IListDataSourceSearchable : IListDataSource { - /// - /// 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. - /// - /// Text to search for. - /// The index of the first item that starts with . - /// Returns if was not found. - int StartsWith (string search); - } - /// /// ListView renders a scrollable list of data where each item can be activated to perform an action. /// @@ -87,7 +72,7 @@ public interface IListDataSourceSearchable : IListDataSource { /// By default uses to render the items of any /// object (e.g. arrays, , /// and other collections). Alternatively, an object that implements - /// or can be provided giving full control of what is rendered. + /// can be provided giving full control of what is rendered. /// /// /// can display any object that implements the interface. @@ -105,8 +90,7 @@ public interface IListDataSourceSearchable : IListDataSource { /// marking style set to false and implement custom rendering. /// /// - /// By default or if is set to an object that implements - /// , searching the ListView with the keyboard is supported. Users type the + /// 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. /// /// @@ -126,7 +110,7 @@ public IListDataSource Source { get => source; set { source = value; - navigator = null; + Navigator.Collection = source?.ToList ()?.Cast (); top = 0; selected = 0; lastSelectedItem = -1; @@ -423,7 +407,10 @@ public override void Redraw (Rect bounds) /// public event Action RowRender; - private SearchCollectionNavigator navigator; + /// + /// Gets the that is used to navigate the when searching. + /// + public SearchCollectionNavigator Navigator { get; private set; } = new SearchCollectionNavigator (); /// public override bool ProcessKey (KeyEvent kb) @@ -436,15 +423,12 @@ public override bool ProcessKey (KeyEvent 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 ()); - } - var newItem = navigator.CalculateNewIndex (SelectedItem, (char)kb.KeyValue); - if (newItem != SelectedItem) { - SelectedItem = newItem; + var newItem = Navigator?.GetNextMatchingItem (SelectedItem, (char)kb.KeyValue); + if (newItem is int && newItem != -1) { + SelectedItem = (int)newItem; EnsuresVisibilitySelectedItem (); SetNeedsDisplay (); return true; @@ -829,7 +813,7 @@ public override bool MouseEvent (MouseEvent me) } /// - public class ListWrapper : IListDataSourceSearchable { + public class ListWrapper : IListDataSource { IList src; BitArray marks; int count, len; diff --git a/Terminal.Gui/Views/TreeView.cs b/Terminal.Gui/Views/TreeView.cs index c6c2e00384..5ccf8b8c15 100644 --- a/Terminal.Gui/Views/TreeView.cs +++ b/Terminal.Gui/Views/TreeView.cs @@ -1,5 +1,5 @@ // This code is based on http://objectlistview.sourceforge.net (GPLv3 tree/list controls -// by phillip.piper@gmail.com). Phillip has explicitly granted permission for his design +// by phillip.piper@gmail.com). Phillip has explicitly granted permission for his design // and code to be used in this library under the MIT license. using NStack; @@ -12,18 +12,18 @@ namespace Terminal.Gui { /// - /// Interface for all non generic members of + /// Interface for all non generic members of . /// /// See TreeView Deep Dive for more information. /// public interface ITreeView { /// - /// Contains options for changing how the tree is rendered + /// Contains options for changing how the tree is rendered. /// TreeStyle Style { get; set; } /// - /// Removes all objects from the tree and clears selection + /// Removes all objects from the tree and clears selection. /// void ClearObjects (); @@ -43,7 +43,7 @@ public class TreeView : TreeView { /// /// Creates a new instance of the tree control with absolute positioning and initialises - /// with default based builder + /// with default based builder. /// public TreeView () { @@ -53,8 +53,8 @@ public TreeView () } /// - /// Hierarchical tree view with expandable branches. Branch objects are dynamically determined - /// when expanded using a user defined + /// Hierarchical tree view with expandable branches. Branch objects are dynamically determined + /// when expanded using a user defined . /// /// See TreeView Deep Dive for more information. /// @@ -64,7 +64,7 @@ public class TreeView : View, ITreeView where T : class { /// /// Determines how sub branches of the tree are dynamically built at runtime as the user - /// expands root nodes + /// expands root nodes. /// /// public ITreeBuilder TreeBuilder { get; set; } @@ -74,30 +74,27 @@ public class TreeView : View, ITreeView where T : class { /// T selectedObject; - /// - /// Contains options for changing how the tree is rendered + /// Contains options for changing how the tree is rendered. /// public TreeStyle Style { get; set; } = new TreeStyle (); - /// - /// True to allow multiple objects to be selected at once + /// True to allow multiple objects to be selected at once. /// /// public bool MultiSelect { get; set; } = true; - /// /// True makes a letter key press navigate to the next visible branch that begins with - /// that letter/digit + /// that letter/digit. /// /// public bool AllowLetterBasedNavigation { get; set; } = true; /// - /// The currently selected object in the tree. When is true this - /// is the object at which the cursor is at + /// The currently selected object in the tree. When is true this + /// is the object at which the cursor is at. /// public T SelectedObject { get => selectedObject; @@ -111,16 +108,15 @@ public T SelectedObject { } } - /// /// This event is raised when an object is activated e.g. by double clicking or - /// pressing + /// pressing . /// public event Action> ObjectActivated; /// /// Key which when pressed triggers . - /// Defaults to Enter + /// Defaults to Enter. /// public Key ObjectActivationKey { get => objectActivationKey; @@ -140,15 +136,14 @@ public Key ObjectActivationKey { /// public MouseFlags? ObjectActivationButton { get; set; } = MouseFlags.Button1DoubleClicked; - /// - /// Delegate for multi colored tree views. Return the to use + /// Delegate for multi colored tree views. Return the to use /// for each passed object or null to use the default. /// public Func ColorGetter { get; set; } /// - /// Secondary selected regions of tree when is true + /// Secondary selected regions of tree when is true. /// private Stack> multiSelectedRegions = new Stack> (); @@ -157,36 +152,35 @@ public Key ObjectActivationKey { /// private IReadOnlyCollection> cachedLineMap; - /// /// Error message to display when the control is not properly initialized at draw time - /// (nodes added but no tree builder set) + /// (nodes added but no tree builder set). /// public static ustring NoBuilderError = "ERROR: TreeBuilder Not Set"; private Key objectActivationKey = Key.Enter; /// - /// Called when the changes + /// Called when the changes. /// public event EventHandler> SelectionChanged; /// - /// The root objects in the tree, note that this collection is of root objects only + /// The root objects in the tree, note that this collection is of root objects only. /// public IEnumerable Objects { get => roots.Keys; } /// - /// Map of root objects to the branches under them. All objects have - /// a even if that branch has no children + /// Map of root objects to the branches under them. All objects have + /// a even if that branch has no children. /// internal Dictionary> roots { get; set; } = new Dictionary> (); /// /// The amount of tree view that has been scrolled off the top of the screen (by the user - /// scrolling down) + /// scrolling down). /// - /// Setting a value of less than 0 will result in a offset of 0. To see changes - /// in the UI call + /// Setting a value of less than 0 will result in a offset of 0. To see changes + /// in the UI call . public int ScrollOffsetVertical { get => scrollOffsetVertical; set { @@ -194,12 +188,11 @@ public int ScrollOffsetVertical { } } - /// - /// The amount of tree view that has been scrolled to the right (horizontally) + /// The amount of tree view that has been scrolled to the right (horizontally). /// - /// Setting a value of less than 0 will result in a offset of 0. To see changes - /// in the UI call + /// Setting a value of less than 0 will result in a offset of 0. To see changes + /// in the UI call . public int ScrollOffsetHorizontal { get => scrollOffsetHorizontal; set { @@ -208,24 +201,23 @@ public int ScrollOffsetHorizontal { } /// - /// The current number of rows in the tree (ignoring the controls bounds) + /// The current number of rows in the tree (ignoring the controls bounds). /// public int ContentHeight => BuildLineMap ().Count (); /// - /// Returns the string representation of model objects hosted in the tree. Default - /// implementation is to call + /// Returns the string representation of model objects hosted in the tree. Default + /// implementation is to call . /// /// public AspectGetterDelegate AspectGetter { get; set; } = (o) => o.ToString () ?? ""; CursorVisibility desiredCursorVisibility = CursorVisibility.Invisible; - private SearchCollectionNavigator searchCollectionNavigator; /// /// Get / Set the wished cursor when the tree is focused. /// Only applies when is true. - /// Defaults to + /// Defaults to . /// public CursorVisibility DesiredCursorVisibility { get { @@ -242,9 +234,9 @@ public CursorVisibility DesiredCursorVisibility { } /// - /// Creates a new tree view with absolute positioning. + /// Creates a new tree view with absolute positioning. /// Use to set set root objects for the tree. - /// Children will not be rendered until you set + /// Children will not be rendered until you set . /// public TreeView () : base () { @@ -301,7 +293,7 @@ public TreeView () : base () /// /// Initialises .Creates a new tree view with absolute - /// positioning. Use to set set root + /// positioning. Use to set set root /// objects for the tree. /// public TreeView (ITreeBuilder builder) : this () @@ -318,7 +310,7 @@ public override bool OnEnter (View view) } /// - /// Adds a new root level object unless it is already a root of the tree + /// Adds a new root level object unless it is already a root of the tree. /// /// public void AddObject (T o) @@ -330,9 +322,8 @@ public void AddObject (T o) } } - /// - /// Removes all objects from the tree and clears + /// Removes all objects from the tree and clears . /// public void ClearObjects () { @@ -347,7 +338,7 @@ public void ClearObjects () /// Removes the given root object from the tree /// /// If is the currently then the - /// selection is cleared + /// selection is cleared. /// public void Remove (T o) { @@ -363,9 +354,9 @@ public void Remove (T o) } /// - /// Adds many new root level objects. Objects that are already root objects are ignored + /// Adds many new root level objects. Objects that are already root objects are ignored. /// - /// Objects to add as new root level objects + /// Objects to add as new root level objects..\ public void AddObjects (IEnumerable collection) { bool objectsAdded = false; @@ -384,13 +375,13 @@ public void AddObjects (IEnumerable collection) } /// - /// Refreshes the state of the object in the tree. This will - /// recompute children, string representation etc + /// Refreshes the state of the object in the tree. This will + /// recompute children, string representation etc. /// /// This has no effect if the object is not exposed in the tree. /// /// True to also refresh all ancestors of the objects branch - /// (starting with the root). False to refresh only the passed node + /// (starting with the root). False to refresh only the passed node. public void RefreshObject (T o, bool startAtTop = false) { var branch = ObjectToBranch (o); @@ -405,7 +396,7 @@ public void RefreshObject (T o, bool startAtTop = false) /// /// Rebuilds the tree structure for all exposed objects starting with the root objects. /// Call this method when you know there are changes to the tree but don't know which - /// objects have changed (otherwise use ) + /// objects have changed (otherwise use ). /// public void RebuildTree () { @@ -418,10 +409,10 @@ public void RebuildTree () } /// - /// Returns the currently expanded children of the passed object. Returns an empty - /// collection if the branch is not exposed or not expanded + /// Returns the currently expanded children of the passed object. Returns an empty + /// collection if the branch is not exposed or not expanded. /// - /// An object in the tree + /// An object in the tree. /// public IEnumerable GetChildren (T o) { @@ -434,10 +425,10 @@ public IEnumerable GetChildren (T o) return branch.ChildBranches?.Values?.Select (b => b.Model)?.ToArray () ?? new T [0]; } /// - /// Returns the parent object of in the tree. Returns null if - /// the object is not exposed in the tree + /// Returns the parent object of in the tree. Returns null if + /// the object is not exposed in the tree. /// - /// An object in the tree + /// An object in the tree. /// public T GetParent (T o) { @@ -474,20 +465,19 @@ public override void Redraw (Rect bounds) Driver.SetAttribute (GetNormalColor ()); Driver.AddStr (new string (' ', bounds.Width)); } - } } /// /// Returns the index of the object if it is currently exposed (it's - /// parent(s) have been expanded). This can be used with - /// and to scroll to a specific object + /// parent(s) have been expanded). This can be used with + /// and to scroll to a specific object. /// /// Uses the Equals method and returns the first index at which the object is found - /// or -1 if it is not found - /// An object that appears in your tree and is currently exposed + /// or -1 if it is not found. + /// An object that appears in your tree and is currently exposed. /// The index the object was found at or -1 if it is not currently revealed or - /// not in the tree at all + /// not in the tree at all. public int GetScrollOffsetOf (T o) { var map = BuildLineMap (); @@ -502,11 +492,11 @@ public int GetScrollOffsetOf (T o) } /// - /// Returns the maximum width line in the tree including prefix and expansion symbols + /// Returns the maximum width line in the tree including prefix and expansion symbols. /// /// True to consider only rows currently visible (based on window - /// bounds and . False to calculate the width of - /// every exposed branch in the tree + /// bounds and . False to calculate the width of + /// every exposed branch in the tree. /// public int GetContentWidth (bool visible) { @@ -537,7 +527,7 @@ public int GetContentWidth (bool visible) /// /// Calculates all currently visible/expanded branches (including leafs) and outputs them - /// by index from the top of the screen + /// by index from the top of the screen. /// /// Index 0 of the returned array is the first item that should be visible in the /// top of the control, index 1 is the next etc. @@ -554,7 +544,11 @@ private IReadOnlyCollection> BuildLineMap () toReturn.AddRange (AddToLineMap (root)); } - return cachedLineMap = new ReadOnlyCollection> (toReturn); + cachedLineMap = new ReadOnlyCollection> (toReturn); + + // Update the collection used for search-typing + Navigator.Collection = cachedLineMap.Select (b => AspectGetter (b.Model)).ToArray (); + return cachedLineMap; } private IEnumerable> AddToLineMap (Branch currentBranch) @@ -562,7 +556,6 @@ private IEnumerable> AddToLineMap (Branch currentBranch) yield return currentBranch; if (currentBranch.IsExpanded) { - foreach (var subBranch in currentBranch.ChildBranches.Values) { foreach (var sub in AddToLineMap (subBranch)) { yield return sub; @@ -571,6 +564,12 @@ private IEnumerable> AddToLineMap (Branch currentBranch) } } + /// + /// Gets the that is used to navigate the + /// when searching with the keyboard. + /// + public SearchCollectionNavigator Navigator { get; private set; } = new SearchCollectionNavigator (); + /// public override bool ProcessKey (KeyEvent keyEvent) { @@ -579,7 +578,6 @@ public override bool ProcessKey (KeyEvent keyEvent) } try { - // First of all deal with any registered keybindings var result = InvokeKeybindings (keyEvent); if (result != null) { @@ -588,35 +586,24 @@ public override bool ProcessKey (KeyEvent keyEvent) // If not a keybinding, is the key a searchable key press? if (SearchCollectionNavigator.IsCompatibleKey (keyEvent) && AllowLetterBasedNavigation) { - IReadOnlyCollection> map; - // If there has been a call to InvalidateMap since the last time we allocated a - // SearchCollectionNavigator then we need a new one to reflect the new exposed - // tree state - if (cachedLineMap == null || searchCollectionNavigator == null) { - map = BuildLineMap (); - searchCollectionNavigator = new SearchCollectionNavigator (map.Select (b => AspectGetter (b.Model)).ToArray ()); - } - else { - // we still need the map, handily its the cached one which means super fast access - map = BuildLineMap (); - } - + // If there has been a call to InvalidateMap since the last time + // we need a new one to reflect the new exposed tree state + map = BuildLineMap (); + // Find the current selected object within the tree var current = map.IndexOf (b => b.Model == SelectedObject); - var newIndex = searchCollectionNavigator.CalculateNewIndex (current, (char)keyEvent.KeyValue); + var newIndex = Navigator?.GetNextMatchingItem (current, (char)keyEvent.KeyValue); - if (newIndex != current) { - SelectedObject = map.ElementAt (newIndex).Model; + if (newIndex is int && newIndex != -1) { + SelectedObject = map.ElementAt ((int)newIndex).Model; EnsureVisible (selectedObject); SetNeedsDisplay (); return true; } } - } finally { - PositionCursor (); } @@ -627,7 +614,7 @@ public override bool ProcessKey (KeyEvent keyEvent) /// /// Triggers the event with the . /// - /// This method also ensures that the selected object is visible + /// This method also ensures that the selected object is visible. /// public void ActivateSelectedObjectIfAny () { @@ -663,11 +650,11 @@ public void ActivateSelectedObjectIfAny () } /// - /// Moves the to the next item that begins with - /// This method will loop back to the start of the tree if reaching the end without finding a match + /// Moves the to the next item that begins with . + /// This method will loop back to the start of the tree if reaching the end without finding a match. /// - /// The first character of the next item you want selected - /// Case sensitivity of the search + /// The first character of the next item you want selected. + /// Case sensitivity of the search. public void AdjustSelectionToNextItemBeginningWith (char character, StringComparison caseSensitivity = StringComparison.CurrentCultureIgnoreCase) { // search for next branch that begins with that letter @@ -680,7 +667,7 @@ public void AdjustSelectionToNextItemBeginningWith (char character, StringCompar /// /// Moves the selection up by the height of the control (1 page). /// - /// True if the navigation should add the covered nodes to the selected current selection + /// True if the navigation should add the covered nodes to the selected current selection. /// public void MovePageUp (bool expandSelection = false) { @@ -690,7 +677,7 @@ public void MovePageUp (bool expandSelection = false) /// /// Moves the selection down by the height of the control (1 page). /// - /// True if the navigation should add the covered nodes to the selected current selection + /// True if the navigation should add the covered nodes to the selected current selection. /// public void MovePageDown (bool expandSelection = false) { @@ -698,7 +685,7 @@ public void MovePageDown (bool expandSelection = false) } /// - /// Scrolls the view area down a single line without changing the current selection + /// Scrolls the view area down a single line without changing the current selection. /// public void ScrollDown () { @@ -707,7 +694,7 @@ public void ScrollDown () } /// - /// Scrolls the view area up a single line without changing the current selection + /// Scrolls the view area up a single line without changing the current selection. /// public void ScrollUp () { @@ -716,7 +703,7 @@ public void ScrollUp () } /// - /// Raises the event + /// Raises the event. /// /// protected virtual void OnObjectActivated (ObjectActivatedEventArgs e) @@ -725,15 +712,15 @@ protected virtual void OnObjectActivated (ObjectActivatedEventArgs e) } /// - /// Returns the object in the tree list that is currently visible - /// at the provided row. Returns null if no object is at that location. + /// Returns the object in the tree list that is currently visible. + /// at the provided row. Returns null if no object is at that location. /// /// /// If you have screen coordinates then use /// to translate these into the client area of the . /// - /// The row of the of the - /// The object currently displayed on this row or null + /// The row of the of the . + /// The object currently displayed on this row or null. public T GetObjectOnRow (int row) { return HitTest (row)?.Model; @@ -758,7 +745,6 @@ public override bool MouseEvent (MouseEvent me) SetFocus (); } - if (me.Flags == MouseFlags.WheeledDown) { ScrollDown (); @@ -814,7 +800,6 @@ public override bool MouseEvent (MouseEvent me) multiSelectedRegions.Clear (); } } else { - // It is a first click somewhere in the current line that doesn't look like an expansion/collapse attempt SelectedObject = clickedBranch.Model; multiSelectedRegions.Clear (); @@ -844,16 +829,15 @@ public override bool MouseEvent (MouseEvent me) // mouse event is handled. return true; } - return false; } /// /// Returns the branch at the given client - /// coordinate e.g. following a click event + /// coordinate e.g. following a click event. /// - /// Client Y position in the controls bounds - /// The clicked branch or null if outside of tree region + /// Client Y position in the controls bounds. + /// The clicked branch or null if outside of tree region. private Branch HitTest (int y) { var map = BuildLineMap (); @@ -870,7 +854,7 @@ private Branch HitTest (int y) } /// - /// Positions the cursor at the start of the selected objects line (if visible) + /// Positions the cursor at the start of the selected objects line (if visible). /// public override void PositionCursor () { @@ -891,11 +875,10 @@ public override void PositionCursor () } } - /// - /// Determines systems behaviour when the left arrow key is pressed. Default behaviour is + /// Determines systems behaviour when the left arrow key is pressed. Default behaviour is /// to collapse the current tree node if possible otherwise changes selection to current - /// branches parent + /// branches parent. /// protected virtual void CursorLeft (bool ctrl) { @@ -919,7 +902,7 @@ protected virtual void CursorLeft (bool ctrl) /// /// Changes the to the first root object and resets - /// the to 0 + /// the to 0. /// public void GoToFirst () { @@ -931,7 +914,7 @@ public void GoToFirst () /// /// Changes the to the last object in the tree and scrolls so - /// that it is visible + /// that it is visible. /// public void GoToEnd () { @@ -944,8 +927,8 @@ public void GoToEnd () /// /// Changes the to and scrolls to ensure - /// it is visible. Has no effect if is not exposed in the tree (e.g. - /// its parents are collapsed) + /// it is visible. Has no effect if is not exposed in the tree (e.g. + /// its parents are collapsed). /// /// public void GoTo (T toSelect) @@ -960,14 +943,14 @@ public void GoTo (T toSelect) } /// - /// The number of screen lines to move the currently selected object by. Supports negative - /// . Each branch occupies 1 line on screen + /// The number of screen lines to move the currently selected object by. Supports negative values. + /// . Each branch occupies 1 line on screen. /// /// If nothing is currently selected or the selected object is no longer in the tree - /// then the first object in the tree is selected instead + /// then the first object in the tree is selected instead. /// Positive to move the selection down the screen, negative to move it up /// True to expand the selection (assuming - /// is enabled). False to replace + /// is enabled). False to replace. public void AdjustSelection (int offset, bool expandSelection = false) { // if it is not a shift click or we don't allow multi select @@ -983,7 +966,6 @@ public void AdjustSelection (int offset, bool expandSelection = false) var idx = map.IndexOf (b => b.Model.Equals (SelectedObject)); if (idx == -1) { - // The current selection has disapeared! SelectedObject = roots.Keys.FirstOrDefault (); } else { @@ -1007,14 +989,12 @@ public void AdjustSelection (int offset, bool expandSelection = false) EnsureVisible (SelectedObject); } - } - SetNeedsDisplay (); } /// - /// Moves the selection to the first child in the currently selected level + /// Moves the selection to the first child in the currently selected level. /// public void AdjustSelectionToBranchStart () { @@ -1054,7 +1034,7 @@ public void AdjustSelectionToBranchStart () } /// - /// Moves the selection to the last child in the currently selected level + /// Moves the selection to the last child in the currently selected level. /// public void AdjustSelectionToBranchEnd () { @@ -1088,13 +1068,12 @@ public void AdjustSelectionToBranchEnd () currentBranch = next; next = map.ElementAt (currentIdx); } - GoToEnd (); } /// - /// Sets the selection to the next branch that matches the + /// Sets the selection to the next branch that matches the . /// /// private void AdjustSelectionToNext (Func, bool> predicate) @@ -1132,7 +1111,7 @@ private void AdjustSelectionToNext (Func, bool> predicate) /// /// Adjusts the to ensure the given - /// is visible. Has no effect if already visible + /// is visible. Has no effect if already visible. /// public void EnsureVisible (T model) { @@ -1159,7 +1138,7 @@ public void EnsureVisible (T model) } /// - /// Expands the currently + /// Expands the currently . /// public void Expand () { @@ -1168,9 +1147,9 @@ public void Expand () /// /// Expands the supplied object if it is contained in the tree (either as a root object or - /// as an exposed branch object) + /// as an exposed branch object). /// - /// The object to expand + /// The object to expand. public void Expand (T toExpand) { if (toExpand == null) { @@ -1183,9 +1162,9 @@ public void Expand (T toExpand) } /// - /// Expands the supplied object and all child objects + /// Expands the supplied object and all child objects. /// - /// The object to expand + /// The object to expand. public void ExpandAll (T toExpand) { if (toExpand == null) { @@ -1198,7 +1177,7 @@ public void ExpandAll (T toExpand) } /// /// Fully expands all nodes in the tree, if the tree is very big and built dynamically this - /// may take a while (e.g. for file system) + /// may take a while (e.g. for file system). /// public void ExpandAll () { @@ -1211,7 +1190,7 @@ public void ExpandAll () } /// /// Returns true if the given object is exposed in the tree and can be - /// expanded otherwise false + /// expanded otherwise false. /// /// /// @@ -1222,7 +1201,7 @@ public bool CanExpand (T o) /// /// Returns true if the given object is exposed in the tree and - /// expanded otherwise false + /// expanded otherwise false. /// /// /// @@ -1240,26 +1219,26 @@ public void Collapse () } /// - /// Collapses the supplied object if it is currently expanded + /// Collapses the supplied object if it is currently expanded . /// - /// The object to collapse + /// The object to collapse. public void Collapse (T toCollapse) { CollapseImpl (toCollapse, false); } /// - /// Collapses the supplied object if it is currently expanded. Also collapses all children - /// branches (this will only become apparent when/if the user expands it again) + /// Collapses the supplied object if it is currently expanded. Also collapses all children + /// branches (this will only become apparent when/if the user expands it again). /// - /// The object to collapse + /// The object to collapse. public void CollapseAll (T toCollapse) { CollapseImpl (toCollapse, true); } /// - /// Collapses all root nodes in the tree + /// Collapses all root nodes in the tree. /// public void CollapseAll () { @@ -1272,19 +1251,17 @@ public void CollapseAll () } /// - /// Implementation of and . Performs - /// operation and updates selection if disapeared + /// Implementation of and . Performs + /// operation and updates selection if disapeared. /// /// /// protected void CollapseImpl (T toCollapse, bool all) { - if (toCollapse == null) { return; } - var branch = ObjectToBranch (toCollapse); // Nothing to collapse @@ -1317,12 +1294,12 @@ protected void InvalidateLineMap () /// /// Returns the corresponding in the tree for - /// . This will not work for objects hidden - /// by their parent being collapsed + /// . This will not work for objects hidden + /// by their parent being collapsed. /// /// /// The branch for or null if it is not currently - /// exposed in the tree + /// exposed in the tree. private Branch ObjectToBranch (T toFind) { return BuildLineMap ().FirstOrDefault (o => o.Model.Equals (toFind)); @@ -1330,7 +1307,7 @@ private Branch ObjectToBranch (T toFind) /// /// Returns true if the is either the - /// or part of a + /// or part of a . /// /// /// @@ -1365,7 +1342,7 @@ public IEnumerable GetAllSelectedObjects () /// /// Selects all objects in the tree when is enabled otherwise - /// does nothing + /// does nothing. /// public void SelectAll () { @@ -1387,9 +1364,8 @@ public void SelectAll () OnSelectionChanged (new SelectionChangedEventArgs (this, SelectedObject, SelectedObject)); } - /// - /// Raises the SelectionChanged event + /// Raises the SelectionChanged event. /// /// protected virtual void OnSelectionChanged (SelectionChangedEventArgs e) @@ -1431,5 +1407,4 @@ public bool Contains (T model) return included.Contains (model); } } - } \ No newline at end of file diff --git a/UICatalog/Properties/launchSettings.json b/UICatalog/Properties/launchSettings.json index eda283ad84..e1f2b1db27 100644 --- a/UICatalog/Properties/launchSettings.json +++ b/UICatalog/Properties/launchSettings.json @@ -44,6 +44,10 @@ "WSL": { "commandName": "WSL2", "distributionName": "" + }, + "SearchCollectionNavigatorTester": { + "commandName": "Project", + "commandLineArgs": "\"Search Collection Nav\"" } } } \ No newline at end of file diff --git a/UICatalog/Scenarios/SearchCollectionNavigatorTester.cs b/UICatalog/Scenarios/SearchCollectionNavigatorTester.cs index 10feff7c31..80b5148930 100644 --- a/UICatalog/Scenarios/SearchCollectionNavigatorTester.cs +++ b/UICatalog/Scenarios/SearchCollectionNavigatorTester.cs @@ -11,6 +11,7 @@ namespace UICatalog.Scenarios { [ScenarioCategory ("Controls"), ScenarioCategory ("TreeView")] [ScenarioCategory ("Controls"), ScenarioCategory ("Text")] public class SearchCollectionNavigatorTester : Scenario { + // Don't create a Window, just return the top-level view public override void Init (Toplevel top, ColorScheme colorScheme) { @@ -94,7 +95,7 @@ public override void Setup () null, new MenuItem ("_Quit", "", () => Quit(), null, null, Key.Q | Key.CtrlMask), }), - new MenuBarItem("_Quit", "CTRL-Q", () => Quit()) + new MenuBarItem("_Quit", "CTRL-Q", () => Quit()), }); Top.Add (menu); @@ -109,7 +110,6 @@ public override void Setup () }; Top.Add (vsep); CreateTreeView (); - } ListView _listView = null; @@ -128,7 +128,7 @@ private void CreateListView () _listView = new ListView () { X = 0, - Y = Pos.Bottom(label), + Y = Pos.Bottom (label), Width = Dim.Percent (50) - 1, Height = Dim.Fill (), AllowsMarking = false, @@ -136,8 +136,12 @@ private void CreateListView () ColorScheme = Colors.TopLevel }; Top.Add (_listView); - + _listView.SetSource (_items); + + _listView.Navigator.SearchStringChanged += (state) => { + label.Text = $"ListView: {state.SearchString}"; + }; } TreeView _treeView = null; @@ -147,7 +151,7 @@ private void CreateTreeView () var label = new Label () { Text = "TreeView", TextAlignment = TextAlignment.Centered, - X = Pos.Right(_listView) + 2, + X = Pos.Right (_listView) + 2, Y = 1, // for menu Width = Dim.Percent (50), Height = 1, @@ -162,15 +166,21 @@ private void CreateTreeView () ColorScheme = Colors.TopLevel }; Top.Add (_treeView); - + var root = new TreeNode ("Alpha examples"); - //root.Children = items.Where (i => char.IsLetterOrDigit (i [0])).Select (i => new TreeNode (i)).Cast().ToList (); - //_treeView.AddObject (root); + root.Children = _items.Where (i => char.IsLetterOrDigit (i [0])).Select (i => new TreeNode (i)).Cast ().ToList (); + _treeView.AddObject (root); root = new TreeNode ("Non-Alpha examples"); root.Children = _items.Where (i => !char.IsLetterOrDigit (i [0])).Select (i => new TreeNode (i)).Cast ().ToList (); _treeView.AddObject (root); _treeView.ExpandAll (); + _treeView.GoToFirst (); + + _treeView.Navigator.SearchStringChanged += (state) => { + label.Text = $"TreeView: {state.SearchString}"; + }; } + private void Quit () { Application.RequestStop (); diff --git a/UnitTests/ListViewTests.cs b/UnitTests/ListViewTests.cs index a9a29543df..3c2d12b4a7 100644 --- a/UnitTests/ListViewTests.cs +++ b/UnitTests/ListViewTests.cs @@ -151,7 +151,7 @@ public void SetMark (int item, bool value) public IList ToList () { - throw new NotImplementedException (); + return new List () { "One", "Two", "Three" }; } } diff --git a/UnitTests/SearchCollectionNavigatorTests.cs b/UnitTests/SearchCollectionNavigatorTests.cs index b59f8f7342..b7e0a4df8a 100644 --- a/UnitTests/SearchCollectionNavigatorTests.cs +++ b/UnitTests/SearchCollectionNavigatorTests.cs @@ -1,142 +1,334 @@ -using Terminal.Gui; +using System.Threading; using Xunit; namespace Terminal.Gui.Core { public class SearchCollectionNavigatorTests { static string [] simpleStrings = new string []{ - "appricot", // 0 - "arm", // 1 - "bat", // 2 - "batman", // 3 - "candle" // 4 - }; + "appricot", // 0 + "arm", // 1 + "bat", // 2 + "batman", // 3 + "candle" // 4 + }; + [Fact] - public void TestSearchCollectionNavigator_ShouldAcceptNegativeOne () + public void ShouldAcceptNegativeOne () { var n = new SearchCollectionNavigator (simpleStrings); - + // Expect that index of -1 (i.e. no selection) should work correctly // and select the first entry of the letter 'b' - Assert.Equal (2, n.CalculateNewIndex (-1, 'b')); + Assert.Equal (2, n.GetNextMatchingItem (-1, 'b')); } [Fact] - public void TestSearchCollectionNavigator_OutOfBoundsShouldBeIgnored() + public void OutOfBoundsShouldBeIgnored () { var n = new SearchCollectionNavigator (simpleStrings); // Expect saying that index 500 is the current selection should not cause // error and just be ignored (treated as no selection) - Assert.Equal (2, n.CalculateNewIndex (500, 'b')); + Assert.Equal (2, n.GetNextMatchingItem (500, 'b')); } [Fact] - public void TestSearchCollectionNavigator_Cycling () + public void Cycling () { var n = new SearchCollectionNavigator (simpleStrings); - Assert.Equal (2, n.CalculateNewIndex ( 0, 'b')); - Assert.Equal (3, n.CalculateNewIndex ( 2, 'b')); + Assert.Equal (2, n.GetNextMatchingItem (0, 'b')); + Assert.Equal (3, n.GetNextMatchingItem (2, 'b')); // if 4 (candle) is selected it should loop back to bat - Assert.Equal (2, n.CalculateNewIndex ( 4, 'b')); + Assert.Equal (2, n.GetNextMatchingItem (4, 'b')); } [Fact] - public void TestSearchCollectionNavigator_ToSearchText () + public void ToSearchText () { var strings = new string []{ - "appricot", - "arm", - "bat", - "batman", - "bbfish", - "candle" - }; + "appricot", + "arm", + "bat", + "batman", + "bbfish", + "candle" + }; + int current = 0; var n = new SearchCollectionNavigator (strings); - Assert.Equal (2, n.CalculateNewIndex (0, 'b')); - Assert.Equal (4, n.CalculateNewIndex (2, 'b')); + Assert.Equal (2, current = n.GetNextMatchingItem (current, 'b')); // match bat + Assert.Equal (4, current = n.GetNextMatchingItem (current, 'b')); // match bbfish // another 'b' means searching for "bbb" which does not exist // so we go back to looking for "b" as a fresh key strike - Assert.Equal (4, n.CalculateNewIndex (2, 'b')); + Assert.Equal (2, current = n.GetNextMatchingItem (current, 'b')); // match bat } [Fact] - public void TestSearchCollectionNavigator_FullText () + public void FullText () { var strings = new string []{ - "appricot", - "arm", - "ta", - "target", - "text", - "egg", - "candle" - }; + "appricot", + "arm", + "ta", + "target", + "text", + "egg", + "candle" + }; var n = new SearchCollectionNavigator (strings); - Assert.Equal (2, n.CalculateNewIndex (0, 't')); + Assert.Equal (2, n.GetNextMatchingItem (0, 't')); // should match "te" in "text" - Assert.Equal (4, n.CalculateNewIndex (2, 'e')); + Assert.Equal (4, n.GetNextMatchingItem (2, 'e')); // still matches text - Assert.Equal (4, n.CalculateNewIndex (4, 'x')); + Assert.Equal (4, n.GetNextMatchingItem (4, 'x')); // nothing starts texa so it jumps to a for appricot - Assert.Equal (0, n.CalculateNewIndex (4, 'a')); + Assert.Equal (0, n.GetNextMatchingItem (4, 'a')); } [Fact] - public void TestSearchCollectionNavigator_Unicode () + public void Unicode () { var strings = new string []{ - "appricot", - "arm", - "ta", - "丗丙业丞", - "丗丙丛", - "text", - "egg", - "candle" - }; + "appricot", + "arm", + "ta", + "丗丙业丞", + "丗丙丛", + "text", + "egg", + "candle" + }; var n = new SearchCollectionNavigator (strings); - Assert.Equal (3, n.CalculateNewIndex (0, '丗')); + Assert.Equal (3, n.GetNextMatchingItem (0, '丗')); // 丗丙业丞 is as good a match as 丗丙丛 // so when doing multi character searches we should // prefer to stay on the same index unless we invalidate // our typed text - Assert.Equal (3, n.CalculateNewIndex (3, '丙')); + Assert.Equal (3, n.GetNextMatchingItem (3, '丙')); // No longer matches 丗丙业丞 and now only matches 丗丙丛 // so we should move to the new match - Assert.Equal (4, n.CalculateNewIndex (3, '丛')); + Assert.Equal (4, n.GetNextMatchingItem (3, '丛')); // nothing starts "丗丙丛a" so it jumps to a for appricot - Assert.Equal (0, n.CalculateNewIndex (4, 'a')); + Assert.Equal (0, n.GetNextMatchingItem (4, 'a')); + } + + [Fact] + public void AtSymbol () + { + var strings = new string []{ + "appricot", + "arm", + "ta", + "@bob", + "@bb", + "text", + "egg", + "candle" + }; + + var n = new SearchCollectionNavigator (strings); + Assert.Equal (3, n.GetNextMatchingItem (0, '@')); + Assert.Equal (3, n.GetNextMatchingItem (3, 'b')); + Assert.Equal (4, n.GetNextMatchingItem (3, 'b')); + } + + [Fact] + public void Word () + { + var strings = new string []{ + "appricot", + "arm", + "bat", + "batman", + "bates hotel", + "candle" + }; + int current = 0; + var n = new SearchCollectionNavigator (strings); + Assert.Equal (strings.IndexOf ("bat"), current = n.GetNextMatchingItem (current, 'b')); // match bat + Assert.Equal (strings.IndexOf ("bat"), current = n.GetNextMatchingItem (current, 'a')); // match bat + Assert.Equal (strings.IndexOf ("bat"), current = n.GetNextMatchingItem (current, 't')); // match bat + Assert.Equal (strings.IndexOf ("bates hotel"), current = n.GetNextMatchingItem (current, 'e')); // match bates hotel + Assert.Equal (strings.IndexOf ("bates hotel"), current = n.GetNextMatchingItem (current, 's')); // match bates hotel + Assert.Equal (strings.IndexOf ("bates hotel"), current = n.GetNextMatchingItem (current, ' ')); // match bates hotel + + // another 'b' means searching for "bates b" which does not exist + // so we go back to looking for "b" as a fresh key strike + Assert.Equal (strings.IndexOf ("bat"), current = n.GetNextMatchingItem (current, 'b')); // match bat + } + + [Fact] + public void Symbols () + { + var strings = new string []{ + "$$", + "$100.00", + "$101.00", + "$101.10", + "$200.00", + "appricot" + }; + int current = 0; + var n = new SearchCollectionNavigator (strings); + Assert.Equal (strings.IndexOf ("appricot"), current = n.GetNextMatchingItem (current, 'a')); + Assert.Equal ("a", n.SearchString); + + Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, '$')); + Assert.Equal ("$", n.SearchString); + + Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, '1')); + Assert.Equal ("$1", n.SearchString); + + Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, '0')); + Assert.Equal ("$10", n.SearchString); + + Assert.Equal (strings.IndexOf ("$101.00"), current = n.GetNextMatchingItem (current, '1')); + Assert.Equal ("$101", n.SearchString); + + Assert.Equal (strings.IndexOf ("$101.00"), current = n.GetNextMatchingItem (current, '.')); + Assert.Equal ("$101.", n.SearchString); + + Assert.Equal (strings.IndexOf ("appricot"), current = n.GetNextMatchingItem (current, 'a')); + Assert.Equal ("a", n.SearchString); + + // another '$' means searching for "$" again + Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, '$')); + Assert.Equal ("$", n.SearchString); + + Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, '$')); + Assert.Equal ("$$", n.SearchString); + } [Fact] - public void TestSearchCollectionNavigator_AtSymbol () + public void Delay () { var strings = new string []{ - "appricot", - "arm", - "ta", - "@bob", - "@bb", - "text", - "egg", - "candle" - }; + "$$", + "$100.00", + "$101.00", + "$101.10", + "$200.00", + "appricot" + }; + int current = 0; + var n = new SearchCollectionNavigator (strings); + + // No delay + Assert.Equal (strings.IndexOf ("appricot"), current = n.GetNextMatchingItem (current, 'a')); + Assert.Equal ("a", n.SearchString); + Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, '$')); + Assert.Equal ("$", n.SearchString); + Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, '$')); + Assert.Equal ("$$", n.SearchString); + + // Delay + Assert.Equal (strings.IndexOf ("appricot"), current = n.GetNextMatchingItem (current, 'a')); + Assert.Equal ("a", n.SearchString); + + Thread.Sleep (n.TypingDelay + 10); + Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, '$')); + Assert.Equal ("$", n.SearchString); + + Thread.Sleep (n.TypingDelay + 10); + Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, '$')); + Assert.Equal ("$", n.SearchString); + + Thread.Sleep (n.TypingDelay + 10); + Assert.Equal (strings.IndexOf ("$101.00"), current = n.GetNextMatchingItem (current, '$')); + Assert.Equal ("$", n.SearchString); + + Thread.Sleep (n.TypingDelay + 10); + Assert.Equal (strings.IndexOf ("$101.10"), current = n.GetNextMatchingItem (current, '$')); + Assert.Equal ("$", n.SearchString); + + Thread.Sleep (n.TypingDelay + 10); + Assert.Equal (strings.IndexOf ("$101.10"), current = n.GetNextMatchingItem (current, '2')); // Shouldn't move + Assert.Equal ("2", n.SearchString); + } + + [Fact] + public void MinimizeMovement_False_ShouldMoveIfMultipleMatches () + { + var strings = new string [] { + "$$", + "$100.00", + "$101.00", + "$101.10", + "$200.00", + "appricot", + "c", + "car", + "cart", + }; + int current = 0; + var n = new SearchCollectionNavigator (strings); + Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$$", false)); + Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, "$", false)); + Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$$", false)); // back to top + Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, "$", false)); + Assert.Equal (strings.IndexOf ("$101.00"), current = n.GetNextMatchingItem (current, "$", false)); + Assert.Equal (strings.IndexOf ("$101.10"), current = n.GetNextMatchingItem (current, "$", false)); + Assert.Equal (strings.IndexOf ("$200.00"), current = n.GetNextMatchingItem (current, "$", false)); + + Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$", false)); // back to top + Assert.Equal (strings.IndexOf ("appricot"), current = n.GetNextMatchingItem (current, "a", false)); + Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$", false)); // back to top + + Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, "$100.00", false)); + Assert.Equal (strings.IndexOf ("$101.00"), current = n.GetNextMatchingItem (current, "$", false)); + Assert.Equal (strings.IndexOf ("$101.00"), current = n.GetNextMatchingItem (current, "$101.00", false)); + Assert.Equal (strings.IndexOf ("$200.00"), current = n.GetNextMatchingItem (current, "$2", false)); + + Assert.Equal (strings.IndexOf ("$200.00"), current = n.GetNextMatchingItem (current, "$200.00", false)); + Assert.Equal (strings.IndexOf ("$101.00"), current = n.GetNextMatchingItem (current, "$101.00", false)); + Assert.Equal (strings.IndexOf ("$200.00"), current = n.GetNextMatchingItem (current, "$2", false)); + + Assert.Equal (strings.IndexOf ("$101.00"), current = n.GetNextMatchingItem (current, "$101.00", false)); + Assert.Equal (strings.IndexOf ("$200.00"), current = n.GetNextMatchingItem (current, "$2", false)); + Assert.Equal (strings.IndexOf ("car"), current = n.GetNextMatchingItem (current, "car", false)); + Assert.Equal (strings.IndexOf ("cart"), current = n.GetNextMatchingItem (current, "car", false)); + + Assert.Equal (-1, current = n.GetNextMatchingItem (current, "x", false)); + } + + [Fact] + public void MinimizeMovement_True_ShouldStayOnCurrentIfMultipleMatches () + { + var strings = new string [] { + "$$", + "$100.00", + "$101.00", + "$101.10", + "$200.00", + "appricot", + "c", + "car", + "cart", + }; + int current = 0; var n = new SearchCollectionNavigator (strings); - Assert.Equal (3, n.CalculateNewIndex (0, '@')); - Assert.Equal (3, n.CalculateNewIndex (3, 'b')); - Assert.Equal (4, n.CalculateNewIndex (3, 'b')); + Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$$", true)); + Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$", true)); + Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$$", true)); // back to top + Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, "$1", true)); + Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, "$", true)); + Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, "$", true)); + + Assert.Equal (strings.IndexOf ("car"), current = n.GetNextMatchingItem (current, "car", true)); + Assert.Equal (strings.IndexOf ("car"), current = n.GetNextMatchingItem (current, "car", true)); + + Assert.Equal (-1, current = n.GetNextMatchingItem (current, "x", true)); } } } From 3ee24854474eff6e1874e76a3c042438c0476fef Mon Sep 17 00:00:00 2001 From: Charlie Kindel Date: Mon, 31 Oct 2022 21:26:25 -0600 Subject: [PATCH 16/25] fixed scenario categories --- UICatalog/Scenarios/SearchCollectionNavigatorTester.cs | 8 +++++--- UICatalog/Scenarios/VkeyPacketSimulator.cs | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/UICatalog/Scenarios/SearchCollectionNavigatorTester.cs b/UICatalog/Scenarios/SearchCollectionNavigatorTester.cs index 80b5148930..74dde8aef2 100644 --- a/UICatalog/Scenarios/SearchCollectionNavigatorTester.cs +++ b/UICatalog/Scenarios/SearchCollectionNavigatorTester.cs @@ -7,9 +7,11 @@ namespace UICatalog.Scenarios { [ScenarioMetadata (Name: "Search Collection Nav", Description: "Demonstrates & tests SearchCollectionNavigator.")] - [ScenarioCategory ("Controls"), ScenarioCategory ("ListView")] - [ScenarioCategory ("Controls"), ScenarioCategory ("TreeView")] - [ScenarioCategory ("Controls"), ScenarioCategory ("Text")] + [ScenarioCategory ("Controls"), + ScenarioCategory ("ListView"), + ScenarioCategory ("TreeView"), + ScenarioCategory ("Text and Formatting"), + ScenarioCategory ("Mouse and Keyboard")] public class SearchCollectionNavigatorTester : Scenario { // Don't create a Window, just return the top-level view diff --git a/UICatalog/Scenarios/VkeyPacketSimulator.cs b/UICatalog/Scenarios/VkeyPacketSimulator.cs index 12fc949b20..ff587e0422 100644 --- a/UICatalog/Scenarios/VkeyPacketSimulator.cs +++ b/UICatalog/Scenarios/VkeyPacketSimulator.cs @@ -6,7 +6,7 @@ namespace UICatalog.Scenarios { [ScenarioMetadata (Name: "VkeyPacketSimulator", Description: "Simulates the Virtual Key Packet")] - [ScenarioCategory ("Keys")] + [ScenarioCategory ("Mouse and Keyboard")] public class VkeyPacketSimulator : Scenario { List _keyboardStrokes = new List (); bool _outputStarted = false; From 859b8def47fdf39216f32b57112e63ecaaa48059 Mon Sep 17 00:00:00 2001 From: Charlie Kindel Date: Mon, 31 Oct 2022 21:50:45 -0600 Subject: [PATCH 17/25] fixed merge error --- UICatalog/Scenarios/Keys.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UICatalog/Scenarios/Keys.cs b/UICatalog/Scenarios/Keys.cs index 78259dd3bc..35cafbc212 100644 --- a/UICatalog/Scenarios/Keys.cs +++ b/UICatalog/Scenarios/Keys.cs @@ -51,7 +51,7 @@ public override bool ProcessColdKey (KeyEvent keyEvent) public override void Init (Toplevel top, ColorScheme colorScheme) { Application.Init (); - Top = top != null ? top : Application.Top != null ? top : Application.Top; + Top = top != null ? top : Application.Top; Win = new TestWindow ($"CTRL-Q to Close - Scenario: {GetName ()}") { X = 0, From e94cd4bc85960e52deccda9fb4d68a72dba157ed Mon Sep 17 00:00:00 2001 From: Charlie Kindel Date: Mon, 31 Oct 2022 22:20:25 -0600 Subject: [PATCH 18/25] renamed ClearState --- Terminal.Gui/Core/SearchCollectionNavigator.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Terminal.Gui/Core/SearchCollectionNavigator.cs b/Terminal.Gui/Core/SearchCollectionNavigator.cs index 6c02b96648..c87865b6e4 100644 --- a/Terminal.Gui/Core/SearchCollectionNavigator.cs +++ b/Terminal.Gui/Core/SearchCollectionNavigator.cs @@ -120,7 +120,7 @@ public int GetNextMatchingItem (int currentIndex, char keyStruck) candidateState.Length > 1); if (idxCandidate != -1) { - // found "dd" so candidate state is accepted + // found "dd" so candidate searchstring is accepted lastKeystroke = DateTime.Now; SearchString = candidateState; return idxCandidate; @@ -134,7 +134,7 @@ public int GetNextMatchingItem (int currentIndex, char keyStruck) // if no changes to current state manifested if (idxCandidate == currentIndex || idxCandidate == -1) { // clear history and treat as a fresh letter - ClearState (); + ClearSearchString (); // match on the fresh letter alone SearchString = new string (keyStruck, 1); @@ -147,7 +147,7 @@ public int GetNextMatchingItem (int currentIndex, char keyStruck) } else { // clear state because keypress was a control char - ClearState (); + ClearSearchString (); // control char indicates no selection return -1; @@ -155,7 +155,7 @@ public int GetNextMatchingItem (int currentIndex, char keyStruck) } /// - /// Gets the index of the next item in the collection that matches the current + /// Gets the index of the next item in the collection that matches . /// /// The index in the collection to start the search from. /// The search string to use. @@ -164,7 +164,7 @@ public int GetNextMatchingItem (int currentIndex, char keyStruck) /// e.g. "ca" + 'r' should stay on "car" and not jump to "cart". If (the default), /// the next matching item will be returned, even if it is above in the collection. /// - /// + /// The index of the next matching item or if no match was found. internal int GetNextMatchingItem (int currentIndex, string search, bool minimizeMovement = false) { if (string.IsNullOrEmpty (search)) { @@ -211,7 +211,7 @@ private void AssertCollectionIsNotNull () } } - private void ClearState () + private void ClearSearchString () { SearchString = ""; lastKeystroke = DateTime.Now; From 66398eb9ef3ec98e792d511829de4280a2be1d5b Mon Sep 17 00:00:00 2001 From: Charlie Kindel Date: Tue, 1 Nov 2022 09:12:37 -0600 Subject: [PATCH 19/25] Renamed classes; fixed rendering bug in ListView --- ...ionNavigator.cs => CollectionNavigator.cs} | 14 +++---- Terminal.Gui/Terminal.Gui.csproj | 2 +- Terminal.Gui/Views/ListView.cs | 39 ++++++++----------- Terminal.Gui/Views/TreeView.cs | 12 +++--- UICatalog/Properties/launchSettings.json | 2 +- UICatalog/Scenario.cs | 4 +- ...Tester.cs => CollectionNavigatorTester.cs} | 21 +++++----- UICatalog/Scenarios/ListViewWithSelection.cs | 2 +- ...orTests.cs => CollectionNavigatorTests.cs} | 26 ++++++------- 9 files changed, 59 insertions(+), 63 deletions(-) rename Terminal.Gui/Core/{SearchCollectionNavigator.cs => CollectionNavigator.cs} (94%) rename UICatalog/Scenarios/{SearchCollectionNavigatorTester.cs => CollectionNavigatorTester.cs} (87%) rename UnitTests/{SearchCollectionNavigatorTests.cs => CollectionNavigatorTests.cs} (94%) diff --git a/Terminal.Gui/Core/SearchCollectionNavigator.cs b/Terminal.Gui/Core/CollectionNavigator.cs similarity index 94% rename from Terminal.Gui/Core/SearchCollectionNavigator.cs rename to Terminal.Gui/Core/CollectionNavigator.cs index c87865b6e4..cc3b1124fc 100644 --- a/Terminal.Gui/Core/SearchCollectionNavigator.cs +++ b/Terminal.Gui/Core/CollectionNavigator.cs @@ -15,17 +15,17 @@ namespace Terminal.Gui { /// If the user pauses keystrokes for a short time (250ms), the search string is cleared. /// /// - public class SearchCollectionNavigator { + public class CollectionNavigator { /// - /// Constructs a new SearchCollectionNavigator. + /// Constructs a new CollectionNavigator. /// - public SearchCollectionNavigator () { } + public CollectionNavigator () { } /// - /// Constructs a new SearchCollectionNavigator for the given collection. + /// Constructs a new CollectionNavigator for the given collection. /// /// - public SearchCollectionNavigator (IEnumerable collection) => Collection = collection; + public CollectionNavigator (IEnumerable collection) => Collection = collection; DateTime lastKeystroke = DateTime.Now; internal int TypingDelay { get; set; } = 250; @@ -41,7 +41,7 @@ public SearchCollectionNavigator () { } public IEnumerable Collection { get; set; } /// - /// Event arguments for the event. + /// Event arguments for the event. /// public class KeystrokeNavigatorEventArgs { /// @@ -162,7 +162,7 @@ public int GetNextMatchingItem (int currentIndex, char keyStruck) /// Set to to stop the search on the first match /// if there are multiple matches for . /// e.g. "ca" + 'r' should stay on "car" and not jump to "cart". If (the default), - /// the next matching item will be returned, even if it is above in the collection. + /// the next matching item will be returned, even if it is above in the collection. /// /// The index of the next matching item or if no match was found. internal int GetNextMatchingItem (int currentIndex, string search, bool minimizeMovement = false) diff --git a/Terminal.Gui/Terminal.Gui.csproj b/Terminal.Gui/Terminal.Gui.csproj index 39d2e1ff23..79d2e41216 100644 --- a/Terminal.Gui/Terminal.Gui.csproj +++ b/Terminal.Gui/Terminal.Gui.csproj @@ -23,7 +23,7 @@ - + $(RestoreSources);..\..\NStack\NStack\bin\Debug;https://api.nuget.org/v3/index.json diff --git a/Terminal.Gui/Views/ListView.cs b/Terminal.Gui/Views/ListView.cs index 388def50ac..957216837e 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -110,7 +110,7 @@ public IListDataSource Source { get => source; set { source = value; - Navigator.Collection = source?.ToList ()?.Cast (); + KeystrokeNavigator.Collection = source?.ToList ()?.Cast (); top = 0; selected = 0; lastSelectedItem = -1; @@ -383,7 +383,7 @@ public override void Redraw (Rect bounds) Driver.SetAttribute (current); } if (allowsMarking) { - Driver.AddRune (source.IsMarked (item) ? (AllowsMultipleSelection ? Driver.Checked : Driver.Selected) : + Driver.AddRune (source.IsMarked (item) ? (AllowsMultipleSelection ? Driver.Checked : Driver.Selected) : (AllowsMultipleSelection ? Driver.UnChecked : Driver.UnSelected)); Driver.AddRune (' '); } @@ -408,9 +408,10 @@ public override void Redraw (Rect bounds) public event Action RowRender; /// - /// Gets the that is used to navigate the when searching. + /// Gets the that searches the collection as + /// the user types. /// - public SearchCollectionNavigator Navigator { get; private set; } = new SearchCollectionNavigator (); + public CollectionNavigator KeystrokeNavigator { get; private set; } = new CollectionNavigator (); /// public override bool ProcessKey (KeyEvent kb) @@ -423,10 +424,10 @@ public override bool ProcessKey (KeyEvent kb) if (result != null) { return (bool)result; } - + // Enable user to find & select an item by typing text - if (SearchCollectionNavigator.IsCompatibleKey(kb)) { - var newItem = Navigator?.GetNextMatchingItem (SelectedItem, (char)kb.KeyValue); + if (CollectionNavigator.IsCompatibleKey (kb)) { + var newItem = KeystrokeNavigator?.GetNextMatchingItem (SelectedItem, (char)kb.KeyValue); if (newItem is int && newItem != -1) { SelectedItem = (int)newItem; EnsuresVisibilitySelectedItem (); @@ -840,13 +841,13 @@ 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; + l = TextFormatter.GetTextWidth (u); } else if (t is string s) { l = s.Length; } else { @@ -863,18 +864,10 @@ int GetMaxLengthItem () 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++) { + var u = TextFormatter.ClipAndJustify (ustr, width, TextAlignment.Left); + driver.AddStr (u); + width -= TextFormatter.GetTextWidth (u); + while (width-- > 0) { driver.AddRune (' '); } } @@ -924,7 +917,7 @@ 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) { @@ -932,7 +925,7 @@ public int StartsWith (string search) return i; } } else if (t is string s) { - if (s.ToUpperInvariant().StartsWith (search.ToUpperInvariant())) { + if (s.ToUpperInvariant ().StartsWith (search.ToUpperInvariant ())) { return i; } } diff --git a/Terminal.Gui/Views/TreeView.cs b/Terminal.Gui/Views/TreeView.cs index 5ccf8b8c15..baab64642f 100644 --- a/Terminal.Gui/Views/TreeView.cs +++ b/Terminal.Gui/Views/TreeView.cs @@ -547,7 +547,7 @@ private IReadOnlyCollection> BuildLineMap () cachedLineMap = new ReadOnlyCollection> (toReturn); // Update the collection used for search-typing - Navigator.Collection = cachedLineMap.Select (b => AspectGetter (b.Model)).ToArray (); + KeystrokeNavigator.Collection = cachedLineMap.Select (b => AspectGetter (b.Model)).ToArray (); return cachedLineMap; } @@ -565,10 +565,10 @@ private IEnumerable> AddToLineMap (Branch currentBranch) } /// - /// Gets the that is used to navigate the - /// when searching with the keyboard. + /// Gets the that searches the collection as + /// the user types. /// - public SearchCollectionNavigator Navigator { get; private set; } = new SearchCollectionNavigator (); + public CollectionNavigator KeystrokeNavigator { get; private set; } = new CollectionNavigator (); /// public override bool ProcessKey (KeyEvent keyEvent) @@ -585,7 +585,7 @@ public override bool ProcessKey (KeyEvent keyEvent) } // If not a keybinding, is the key a searchable key press? - if (SearchCollectionNavigator.IsCompatibleKey (keyEvent) && AllowLetterBasedNavigation) { + if (CollectionNavigator.IsCompatibleKey (keyEvent) && AllowLetterBasedNavigation) { IReadOnlyCollection> map; // If there has been a call to InvalidateMap since the last time @@ -594,7 +594,7 @@ public override bool ProcessKey (KeyEvent keyEvent) // Find the current selected object within the tree var current = map.IndexOf (b => b.Model == SelectedObject); - var newIndex = Navigator?.GetNextMatchingItem (current, (char)keyEvent.KeyValue); + var newIndex = KeystrokeNavigator?.GetNextMatchingItem (current, (char)keyEvent.KeyValue); if (newIndex is int && newIndex != -1) { SelectedObject = map.ElementAt ((int)newIndex).Model; diff --git a/UICatalog/Properties/launchSettings.json b/UICatalog/Properties/launchSettings.json index e1f2b1db27..ec419ec207 100644 --- a/UICatalog/Properties/launchSettings.json +++ b/UICatalog/Properties/launchSettings.json @@ -45,7 +45,7 @@ "commandName": "WSL2", "distributionName": "" }, - "SearchCollectionNavigatorTester": { + "CollectionNavigatorTester": { "commandName": "Project", "commandLineArgs": "\"Search Collection Nav\"" } diff --git a/UICatalog/Scenario.cs b/UICatalog/Scenario.cs index c747829e37..30767e1906 100644 --- a/UICatalog/Scenario.cs +++ b/UICatalog/Scenario.cs @@ -233,7 +233,7 @@ internal static List GetAllCategories () } /// - /// Returns an instance of each defined in the project. + /// Returns a list of all instanaces defined in the project, sorted by . /// https://stackoverflow.com/questions/5411694/get-all-inherited-classes-of-an-abstract-class /// public static List GetScenarios () @@ -245,7 +245,7 @@ public static List GetScenarios () objects.Add (scenario); _maxScenarioNameLen = Math.Max (_maxScenarioNameLen, scenario.GetName ().Length + 1); } - return objects; + return objects.OrderBy (s => s.GetName ()).ToList (); } protected virtual void Dispose (bool disposing) diff --git a/UICatalog/Scenarios/SearchCollectionNavigatorTester.cs b/UICatalog/Scenarios/CollectionNavigatorTester.cs similarity index 87% rename from UICatalog/Scenarios/SearchCollectionNavigatorTester.cs rename to UICatalog/Scenarios/CollectionNavigatorTester.cs index 74dde8aef2..d97f6890c4 100644 --- a/UICatalog/Scenarios/SearchCollectionNavigatorTester.cs +++ b/UICatalog/Scenarios/CollectionNavigatorTester.cs @@ -6,13 +6,13 @@ namespace UICatalog.Scenarios { - [ScenarioMetadata (Name: "Search Collection Nav", Description: "Demonstrates & tests SearchCollectionNavigator.")] - [ScenarioCategory ("Controls"), - ScenarioCategory ("ListView"), - ScenarioCategory ("TreeView"), + [ScenarioMetadata (Name: "Collection Navigator", Description: "Demonstrates keyboard navigation in ListView & TreeView (CollectionNavigator).")] + [ScenarioCategory ("Controls"), + ScenarioCategory ("ListView"), + ScenarioCategory ("TreeView"), ScenarioCategory ("Text and Formatting"), ScenarioCategory ("Mouse and Keyboard")] - public class SearchCollectionNavigatorTester : Scenario { + public class CollectionNavigatorTester : Scenario { // Don't create a Window, just return the top-level view public override void Init (Toplevel top, ColorScheme colorScheme) @@ -70,6 +70,9 @@ public override void Init (Toplevel top, ColorScheme colorScheme) "egg", "candle", " <- space", + "\t<- tab", + "\n<- newline", + "\r<- formfeed", "q", "quit", "quitter" @@ -141,7 +144,7 @@ private void CreateListView () _listView.SetSource (_items); - _listView.Navigator.SearchStringChanged += (state) => { + _listView.KeystrokeNavigator.SearchStringChanged += (state) => { label.Text = $"ListView: {state.SearchString}"; }; } @@ -169,16 +172,16 @@ private void CreateTreeView () }; Top.Add (_treeView); - var root = new TreeNode ("Alpha examples"); + var root = new TreeNode ("IsLetterOrDigit examples"); root.Children = _items.Where (i => char.IsLetterOrDigit (i [0])).Select (i => new TreeNode (i)).Cast ().ToList (); _treeView.AddObject (root); - root = new TreeNode ("Non-Alpha examples"); + root = new TreeNode ("Non-IsLetterOrDigit examples"); root.Children = _items.Where (i => !char.IsLetterOrDigit (i [0])).Select (i => new TreeNode (i)).Cast ().ToList (); _treeView.AddObject (root); _treeView.ExpandAll (); _treeView.GoToFirst (); - _treeView.Navigator.SearchStringChanged += (state) => { + _treeView.KeystrokeNavigator.SearchStringChanged += (state) => { label.Text = $"TreeView: {state.SearchString}"; }; } diff --git a/UICatalog/Scenarios/ListViewWithSelection.cs b/UICatalog/Scenarios/ListViewWithSelection.cs index bd1afc40b8..c132cf8f57 100644 --- a/UICatalog/Scenarios/ListViewWithSelection.cs +++ b/UICatalog/Scenarios/ListViewWithSelection.cs @@ -21,7 +21,7 @@ public class ListViewWithSelection : Scenario { public override void Setup () { - _scenarios = Scenario.GetScenarios ().OrderBy (s => s.GetName ()).ToList (); + _scenarios = Scenario.GetScenarios (); _customRenderCB = new CheckBox ("Use custom rendering") { X = 0, diff --git a/UnitTests/SearchCollectionNavigatorTests.cs b/UnitTests/CollectionNavigatorTests.cs similarity index 94% rename from UnitTests/SearchCollectionNavigatorTests.cs rename to UnitTests/CollectionNavigatorTests.cs index b7e0a4df8a..06ebc0000d 100644 --- a/UnitTests/SearchCollectionNavigatorTests.cs +++ b/UnitTests/CollectionNavigatorTests.cs @@ -2,7 +2,7 @@ using Xunit; namespace Terminal.Gui.Core { - public class SearchCollectionNavigatorTests { + public class CollectionNavigatorTests { static string [] simpleStrings = new string []{ "appricot", // 0 "arm", // 1 @@ -14,7 +14,7 @@ public class SearchCollectionNavigatorTests { [Fact] public void ShouldAcceptNegativeOne () { - var n = new SearchCollectionNavigator (simpleStrings); + var n = new CollectionNavigator (simpleStrings); // Expect that index of -1 (i.e. no selection) should work correctly // and select the first entry of the letter 'b' @@ -23,7 +23,7 @@ public void ShouldAcceptNegativeOne () [Fact] public void OutOfBoundsShouldBeIgnored () { - var n = new SearchCollectionNavigator (simpleStrings); + var n = new CollectionNavigator (simpleStrings); // Expect saying that index 500 is the current selection should not cause // error and just be ignored (treated as no selection) @@ -33,7 +33,7 @@ public void OutOfBoundsShouldBeIgnored () [Fact] public void Cycling () { - var n = new SearchCollectionNavigator (simpleStrings); + var n = new CollectionNavigator (simpleStrings); Assert.Equal (2, n.GetNextMatchingItem (0, 'b')); Assert.Equal (3, n.GetNextMatchingItem (2, 'b')); @@ -55,7 +55,7 @@ public void ToSearchText () }; int current = 0; - var n = new SearchCollectionNavigator (strings); + var n = new CollectionNavigator (strings); Assert.Equal (2, current = n.GetNextMatchingItem (current, 'b')); // match bat Assert.Equal (4, current = n.GetNextMatchingItem (current, 'b')); // match bbfish @@ -77,7 +77,7 @@ public void FullText () "candle" }; - var n = new SearchCollectionNavigator (strings); + var n = new CollectionNavigator (strings); Assert.Equal (2, n.GetNextMatchingItem (0, 't')); // should match "te" in "text" @@ -104,7 +104,7 @@ public void Unicode () "candle" }; - var n = new SearchCollectionNavigator (strings); + var n = new CollectionNavigator (strings); Assert.Equal (3, n.GetNextMatchingItem (0, '丗')); // 丗丙业丞 is as good a match as 丗丙丛 @@ -135,7 +135,7 @@ public void AtSymbol () "candle" }; - var n = new SearchCollectionNavigator (strings); + var n = new CollectionNavigator (strings); Assert.Equal (3, n.GetNextMatchingItem (0, '@')); Assert.Equal (3, n.GetNextMatchingItem (3, 'b')); Assert.Equal (4, n.GetNextMatchingItem (3, 'b')); @@ -153,7 +153,7 @@ public void Word () "candle" }; int current = 0; - var n = new SearchCollectionNavigator (strings); + var n = new CollectionNavigator (strings); Assert.Equal (strings.IndexOf ("bat"), current = n.GetNextMatchingItem (current, 'b')); // match bat Assert.Equal (strings.IndexOf ("bat"), current = n.GetNextMatchingItem (current, 'a')); // match bat Assert.Equal (strings.IndexOf ("bat"), current = n.GetNextMatchingItem (current, 't')); // match bat @@ -178,7 +178,7 @@ public void Symbols () "appricot" }; int current = 0; - var n = new SearchCollectionNavigator (strings); + var n = new CollectionNavigator (strings); Assert.Equal (strings.IndexOf ("appricot"), current = n.GetNextMatchingItem (current, 'a')); Assert.Equal ("a", n.SearchString); @@ -221,7 +221,7 @@ public void Delay () "appricot" }; int current = 0; - var n = new SearchCollectionNavigator (strings); + var n = new CollectionNavigator (strings); // No delay Assert.Equal (strings.IndexOf ("appricot"), current = n.GetNextMatchingItem (current, 'a')); @@ -271,7 +271,7 @@ public void MinimizeMovement_False_ShouldMoveIfMultipleMatches () "cart", }; int current = 0; - var n = new SearchCollectionNavigator (strings); + var n = new CollectionNavigator (strings); Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$$", false)); Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, "$", false)); Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$$", false)); // back to top @@ -317,7 +317,7 @@ public void MinimizeMovement_True_ShouldStayOnCurrentIfMultipleMatches () "cart", }; int current = 0; - var n = new SearchCollectionNavigator (strings); + var n = new CollectionNavigator (strings); Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$$", true)); Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$", true)); Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$$", true)); // back to top From a2f04ed6f193cff7c71bb35dbef463beecb7c95b Mon Sep 17 00:00:00 2001 From: Charlie Kindel Date: Tue, 1 Nov 2022 09:21:04 -0600 Subject: [PATCH 20/25] Delay is now 500ms and TypingDelay is public --- Terminal.Gui/Core/CollectionNavigator.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Terminal.Gui/Core/CollectionNavigator.cs b/Terminal.Gui/Core/CollectionNavigator.cs index cc3b1124fc..a9bf3f9200 100644 --- a/Terminal.Gui/Core/CollectionNavigator.cs +++ b/Terminal.Gui/Core/CollectionNavigator.cs @@ -12,7 +12,7 @@ namespace Terminal.Gui { /// the search string is cleared and the next item is found that starts with the last keystroke. /// /// - /// If the user pauses keystrokes for a short time (250ms), the search string is cleared. + /// If the user pauses keystrokes for a short time (see ), the search string is cleared. /// /// public class CollectionNavigator { @@ -28,7 +28,11 @@ public CollectionNavigator () { } public CollectionNavigator (IEnumerable collection) => Collection = collection; DateTime lastKeystroke = DateTime.Now; - internal int TypingDelay { get; set; } = 250; + /// + /// Gets or sets the number of milliseconds to delay before clearing the search string. The delay is + /// reset on each call to . The default is 500ms. + /// + public int TypingDelay { get; set; } = 500; /// /// The compararer function to use when searching the collection. @@ -67,7 +71,7 @@ public KeystrokeNavigatorEventArgs (string searchString) private string _searchString = ""; /// /// Gets the current search string. This includes the set of keystrokes that have been pressed - /// since the last unsuccessful match or after a 250ms delay. Useful for debugging. + /// since the last unsuccessful match or after ) milliseconds. Useful for debugging. /// public string SearchString { get => _searchString; From 079a2e03ca0fa6f59fe06f3bee98ef5d68f37d79 Mon Sep 17 00:00:00 2001 From: Charlie Kindel Date: Tue, 1 Nov 2022 09:27:04 -0600 Subject: [PATCH 21/25] removed local nstack ref --- Terminal.Gui/Terminal.Gui.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Terminal.Gui/Terminal.Gui.csproj b/Terminal.Gui/Terminal.Gui.csproj index 79d2e41216..77c964b34d 100644 --- a/Terminal.Gui/Terminal.Gui.csproj +++ b/Terminal.Gui/Terminal.Gui.csproj @@ -23,7 +23,7 @@ - $(RestoreSources);..\..\NStack\NStack\bin\Debug;https://api.nuget.org/v3/index.json + From bc846bf83aba5d1c85023d74e6284edfdbb7c141 Mon Sep 17 00:00:00 2001 From: Charlie Kindel Date: Tue, 1 Nov 2022 10:03:14 -0600 Subject: [PATCH 22/25] added unit test that proves 'wrong' key behavior is broken --- UnitTests/CollectionNavigatorTests.cs | 48 ++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/UnitTests/CollectionNavigatorTests.cs b/UnitTests/CollectionNavigatorTests.cs index 06ebc0000d..89975d5a3f 100644 --- a/UnitTests/CollectionNavigatorTests.cs +++ b/UnitTests/CollectionNavigatorTests.cs @@ -1,4 +1,5 @@ -using System.Threading; +using System; +using System.Threading; using Xunit; namespace Terminal.Gui.Core { @@ -256,6 +257,51 @@ public void Delay () Assert.Equal ("2", n.SearchString); } + [Fact] + public void MutliKeySearchPlusWrongKeyStays () + { + var strings = new string []{ + "a", + "c", + "can", + "candle", + "candy", + "yellow" + }; + int current = 0; + var n = new CollectionNavigator (strings); + + // https://github.com/gui-cs/Terminal.Gui/pull/2132#issuecomment-1298425573 + // One thing that it currently does that is different from Explorer is that as soon as you hit a wrong key then it jumps to that index. + // So if you type cand then z it jumps you to something beginning with z. In the same situation Windows Explorer beeps (not the best!) + // but remains on candle. + // We might be able to update the behaviour so that a 'wrong' keypress (z) within 500ms of a 'right' keypress ("can" + 'd') is + // simply ignored (possibly ending the search process though). That would give a short delay for user to realise the thing + // they typed doesn't exist and then start a new search (which would be possible 500ms after the last 'good' keypress). + // This would only apply for 2+ character searches where theres been a successful 2+ character match right before. + + Assert.Equal (strings.IndexOf ("a"), current = n.GetNextMatchingItem (current, 'a')); + Assert.Equal (strings.IndexOf ("c"), current = n.GetNextMatchingItem (current, 'c')); + Assert.Equal ("c", n.SearchString); + Assert.Equal (strings.IndexOf ("can"), current = n.GetNextMatchingItem (current, 'a')); + Assert.Equal ("ca", n.SearchString); + Assert.Equal (strings.IndexOf ("can"), current = n.GetNextMatchingItem (current, 'n')); + Assert.Equal ("can", n.SearchString); + Assert.Equal (strings.IndexOf ("candle"), current = n.GetNextMatchingItem (current, 'd')); + Assert.Equal ("cand", n.SearchString); + + // Same as above, but with a 'wrong' key (z) + Assert.Equal (strings.IndexOf ("a"), current = n.GetNextMatchingItem (current, 'a')); + Assert.Equal (strings.IndexOf ("c"), current = n.GetNextMatchingItem (current, 'c')); + Assert.Equal ("c", n.SearchString); + Assert.Equal (strings.IndexOf ("can"), current = n.GetNextMatchingItem (current, 'a')); + Assert.Equal ("ca", n.SearchString); + Assert.Equal (strings.IndexOf ("can"), current = n.GetNextMatchingItem (current, 'n')); + Assert.Equal ("can", n.SearchString); + Assert.Equal (strings.IndexOf ("candle"), current = n.GetNextMatchingItem (current, 'z')); + Assert.Equal ("cand", n.SearchString); + } + [Fact] public void MinimizeMovement_False_ShouldMoveIfMultipleMatches () { From c7f439295707ac2f51d4b8890ca9821a3b0a68fa Mon Sep 17 00:00:00 2001 From: Charlie Kindel Date: Tue, 1 Nov 2022 10:10:05 -0600 Subject: [PATCH 23/25] fixed unit test that proves 'wrong' key behavior is broken --- UnitTests/CollectionNavigatorTests.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/UnitTests/CollectionNavigatorTests.cs b/UnitTests/CollectionNavigatorTests.cs index 89975d5a3f..030856d923 100644 --- a/UnitTests/CollectionNavigatorTests.cs +++ b/UnitTests/CollectionNavigatorTests.cs @@ -266,7 +266,8 @@ public void MutliKeySearchPlusWrongKeyStays () "can", "candle", "candy", - "yellow" + "yellow", + "zebra" }; int current = 0; var n = new CollectionNavigator (strings); @@ -298,8 +299,8 @@ public void MutliKeySearchPlusWrongKeyStays () Assert.Equal ("ca", n.SearchString); Assert.Equal (strings.IndexOf ("can"), current = n.GetNextMatchingItem (current, 'n')); Assert.Equal ("can", n.SearchString); - Assert.Equal (strings.IndexOf ("candle"), current = n.GetNextMatchingItem (current, 'z')); - Assert.Equal ("cand", n.SearchString); + Assert.Equal (strings.IndexOf ("can"), current = n.GetNextMatchingItem (current, 'z')); // Shouldn't move + Assert.Equal ("can", n.SearchString); // Shouldn't change } [Fact] From 2200dc0964b768192dc954a5f66db79d40a5e805 Mon Sep 17 00:00:00 2001 From: Charlie Kindel Date: Tue, 1 Nov 2022 10:18:24 -0600 Subject: [PATCH 24/25] fixed (for real?) unit test that proves 'wrong' key behavior is broken --- Terminal.Gui/Core/CollectionNavigator.cs | 8 ++++++++ UnitTests/CollectionNavigatorTests.cs | 3 +++ 2 files changed, 11 insertions(+) diff --git a/Terminal.Gui/Core/CollectionNavigator.cs b/Terminal.Gui/Core/CollectionNavigator.cs index a9bf3f9200..94546f134f 100644 --- a/Terminal.Gui/Core/CollectionNavigator.cs +++ b/Terminal.Gui/Core/CollectionNavigator.cs @@ -135,6 +135,14 @@ public int GetNextMatchingItem (int currentIndex, char keyStruck) lastKeystroke = DateTime.Now; idxCandidate = GetNextMatchingItem (currentIndex, candidateState); + //// if a match wasn't found, the user typed a 'wrong' key in their search ("can" + 'z' + //// instead of "can" + 'd'). + //if (SearchString.Length > 1 && idxCandidate == -1) { + // // ignore it since we're still within the typing delay + // // don't add it to SearchString either + // return currentIndex; + //} + // if no changes to current state manifested if (idxCandidate == currentIndex || idxCandidate == -1) { // clear history and treat as a fresh letter diff --git a/UnitTests/CollectionNavigatorTests.cs b/UnitTests/CollectionNavigatorTests.cs index 030856d923..96a4c11263 100644 --- a/UnitTests/CollectionNavigatorTests.cs +++ b/UnitTests/CollectionNavigatorTests.cs @@ -282,6 +282,7 @@ public void MutliKeySearchPlusWrongKeyStays () // This would only apply for 2+ character searches where theres been a successful 2+ character match right before. Assert.Equal (strings.IndexOf ("a"), current = n.GetNextMatchingItem (current, 'a')); + Assert.Equal ("a", n.SearchString); Assert.Equal (strings.IndexOf ("c"), current = n.GetNextMatchingItem (current, 'c')); Assert.Equal ("c", n.SearchString); Assert.Equal (strings.IndexOf ("can"), current = n.GetNextMatchingItem (current, 'a')); @@ -292,7 +293,9 @@ public void MutliKeySearchPlusWrongKeyStays () Assert.Equal ("cand", n.SearchString); // Same as above, but with a 'wrong' key (z) + Thread.Sleep (n.TypingDelay + 10); Assert.Equal (strings.IndexOf ("a"), current = n.GetNextMatchingItem (current, 'a')); + Assert.Equal ("a", n.SearchString); Assert.Equal (strings.IndexOf ("c"), current = n.GetNextMatchingItem (current, 'c')); Assert.Equal ("c", n.SearchString); Assert.Equal (strings.IndexOf ("can"), current = n.GetNextMatchingItem (current, 'a')); From 1247a129e16fe35ade853cdfddb1cf34fc69a824 Mon Sep 17 00:00:00 2001 From: Charlie Kindel Date: Tue, 1 Nov 2022 10:34:20 -0600 Subject: [PATCH 25/25] updated unit tests for new 'wrong' key behavior --- Terminal.Gui/Core/CollectionNavigator.cs | 14 ++--- UnitTests/CollectionNavigatorTests.cs | 66 ++++++++++-------------- 2 files changed, 33 insertions(+), 47 deletions(-) diff --git a/Terminal.Gui/Core/CollectionNavigator.cs b/Terminal.Gui/Core/CollectionNavigator.cs index 94546f134f..6637daf209 100644 --- a/Terminal.Gui/Core/CollectionNavigator.cs +++ b/Terminal.Gui/Core/CollectionNavigator.cs @@ -135,13 +135,13 @@ public int GetNextMatchingItem (int currentIndex, char keyStruck) lastKeystroke = DateTime.Now; idxCandidate = GetNextMatchingItem (currentIndex, candidateState); - //// if a match wasn't found, the user typed a 'wrong' key in their search ("can" + 'z' - //// instead of "can" + 'd'). - //if (SearchString.Length > 1 && idxCandidate == -1) { - // // ignore it since we're still within the typing delay - // // don't add it to SearchString either - // return currentIndex; - //} + // if a match wasn't found, the user typed a 'wrong' key in their search ("can" + 'z' + // instead of "can" + 'd'). + if (SearchString.Length > 1 && idxCandidate == -1) { + // ignore it since we're still within the typing delay + // don't add it to SearchString either + return currentIndex; + } // if no changes to current state manifested if (idxCandidate == currentIndex || idxCandidate == -1) { diff --git a/UnitTests/CollectionNavigatorTests.cs b/UnitTests/CollectionNavigatorTests.cs index 96a4c11263..d0de09f3a4 100644 --- a/UnitTests/CollectionNavigatorTests.cs +++ b/UnitTests/CollectionNavigatorTests.cs @@ -42,29 +42,6 @@ public void Cycling () Assert.Equal (2, n.GetNextMatchingItem (4, 'b')); } - - [Fact] - public void ToSearchText () - { - var strings = new string []{ - "appricot", - "arm", - "bat", - "batman", - "bbfish", - "candle" - }; - - int current = 0; - var n = new CollectionNavigator (strings); - Assert.Equal (2, current = n.GetNextMatchingItem (current, 'b')); // match bat - Assert.Equal (4, current = n.GetNextMatchingItem (current, 'b')); // match bbfish - - // another 'b' means searching for "bbb" which does not exist - // so we go back to looking for "b" as a fresh key strike - Assert.Equal (2, current = n.GetNextMatchingItem (current, 'b')); // match bat - } - [Fact] public void FullText () { @@ -79,16 +56,21 @@ public void FullText () }; var n = new CollectionNavigator (strings); - Assert.Equal (2, n.GetNextMatchingItem (0, 't')); + int current = 0; + Assert.Equal (strings.IndexOf ("ta"), current = n.GetNextMatchingItem (current, 't')); // should match "te" in "text" - Assert.Equal (4, n.GetNextMatchingItem (2, 'e')); + Assert.Equal (strings.IndexOf ("text"), current = n.GetNextMatchingItem (current, 'e')); // still matches text - Assert.Equal (4, n.GetNextMatchingItem (4, 'x')); + Assert.Equal (strings.IndexOf ("text"), current = n.GetNextMatchingItem (current, 'x')); - // nothing starts texa so it jumps to a for appricot - Assert.Equal (0, n.GetNextMatchingItem (4, 'a')); + // nothing starts texa so it should NOT jump to appricot + Assert.Equal (strings.IndexOf ("text"), current = n.GetNextMatchingItem (current, 'a')); + + Thread.Sleep (n.TypingDelay + 100); + // nothing starts "texa". Since were past timedelay we DO jump to appricot + Assert.Equal (strings.IndexOf ("appricot"), current = n.GetNextMatchingItem (current, 'a')); } [Fact] @@ -106,20 +88,25 @@ public void Unicode () }; var n = new CollectionNavigator (strings); - Assert.Equal (3, n.GetNextMatchingItem (0, '丗')); + int current = 0; + Assert.Equal (strings.IndexOf ("丗丙业丞"), current = n.GetNextMatchingItem (current, '丗')); // 丗丙业丞 is as good a match as 丗丙丛 // so when doing multi character searches we should // prefer to stay on the same index unless we invalidate // our typed text - Assert.Equal (3, n.GetNextMatchingItem (3, '丙')); + Assert.Equal (strings.IndexOf ("丗丙业丞"), current = n.GetNextMatchingItem (current, '丙')); // No longer matches 丗丙业丞 and now only matches 丗丙丛 // so we should move to the new match - Assert.Equal (4, n.GetNextMatchingItem (3, '丛')); + Assert.Equal (strings.IndexOf ("丗丙丛"), current = n.GetNextMatchingItem (current, '丛')); + + // nothing starts "丗丙丛a". Since were still in the timedelay we do not jump to appricot + Assert.Equal (strings.IndexOf ("丗丙丛"), current = n.GetNextMatchingItem (current, 'a')); - // nothing starts "丗丙丛a" so it jumps to a for appricot - Assert.Equal (0, n.GetNextMatchingItem (4, 'a')); + Thread.Sleep (n.TypingDelay + 100); + // nothing starts "丗丙丛a". Since were past timedelay we DO jump to appricot + Assert.Equal (strings.IndexOf ("appricot"), current = n.GetNextMatchingItem (current, 'a')); } [Fact] @@ -161,10 +148,6 @@ public void Word () Assert.Equal (strings.IndexOf ("bates hotel"), current = n.GetNextMatchingItem (current, 'e')); // match bates hotel Assert.Equal (strings.IndexOf ("bates hotel"), current = n.GetNextMatchingItem (current, 's')); // match bates hotel Assert.Equal (strings.IndexOf ("bates hotel"), current = n.GetNextMatchingItem (current, ' ')); // match bates hotel - - // another 'b' means searching for "bates b" which does not exist - // so we go back to looking for "b" as a fresh key strike - Assert.Equal (strings.IndexOf ("bat"), current = n.GetNextMatchingItem (current, 'b')); // match bat } [Fact] @@ -198,11 +181,13 @@ public void Symbols () Assert.Equal (strings.IndexOf ("$101.00"), current = n.GetNextMatchingItem (current, '.')); Assert.Equal ("$101.", n.SearchString); - Assert.Equal (strings.IndexOf ("appricot"), current = n.GetNextMatchingItem (current, 'a')); - Assert.Equal ("a", n.SearchString); + // stay on the same item becuase still in timedelay + Assert.Equal (strings.IndexOf ("$101.00"), current = n.GetNextMatchingItem (current, 'a')); + Assert.Equal ("$101.", n.SearchString); + Thread.Sleep (n.TypingDelay + 100); // another '$' means searching for "$" again - Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, '$')); + Assert.Equal (strings.IndexOf ("$101.10"), current = n.GetNextMatchingItem (current, '$')); Assert.Equal ("$", n.SearchString); Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, '$')); @@ -233,6 +218,7 @@ public void Delay () Assert.Equal ("$$", n.SearchString); // Delay + Thread.Sleep (n.TypingDelay + 10); Assert.Equal (strings.IndexOf ("appricot"), current = n.GetNextMatchingItem (current, 'a')); Assert.Equal ("a", n.SearchString);