From ac58a77b9d849d22ff96aa090cd4e3df55be1931 Mon Sep 17 00:00:00 2001 From: Charlie Kindel Date: Sat, 22 Oct 2022 18:45:15 -0600 Subject: [PATCH 01/33] 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 1a37fea3a..61ed818c7 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 @@ namespace Terminal.Gui { /// /// 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 @@ namespace Terminal.Gui { /// 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { /// /// 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 @@ namespace Terminal.Gui { /// [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 @@ namespace Terminal.Gui { /// /// 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 @@ namespace Terminal.Gui { } /// - /// 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 @@ namespace Terminal.Gui { } /// - /// 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 @@ namespace Terminal.Gui { } /// - /// Gets the widest item. + /// Gets the widest item in the list. /// public int Maxlength => (source?.Length) ?? 0; @@ -264,10 +265,12 @@ namespace Terminal.Gui { } /// - /// 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 @@ namespace Terminal.Gui { /// 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 @@ namespace Terminal.Gui { /// 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { /// public event Action RowRender; + private string search { get; set; } + /// public override bool ProcessKey (KeyEvent kb) { @@ -419,13 +428,37 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { } /// - /// 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 @@ namespace Terminal.Gui { } /// - /// 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 @@ namespace Terminal.Gui { } /// - /// 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 @@ namespace Terminal.Gui { } /// - /// 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 @@ namespace Terminal.Gui { } /// - /// 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 @@ namespace Terminal.Gui { } /// - /// 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 @@ namespace Terminal.Gui { } /// - /// 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 @@ namespace Terminal.Gui { } /// - /// 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { } /// - /// Invokes the OnOpenSelectedItem event if it is defined. + /// Invokes the event if it is defined. /// /// public virtual bool OnOpenSelectedItem () @@ -788,23 +827,15 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { } } - /// - /// 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 @@ namespace Terminal.Gui { } } - /// - /// 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 @@ namespace Terminal.Gui { } } - /// - /// 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 @@ namespace Terminal.Gui { 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/33] 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 000000000..47d62b661 --- /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 000000000..ac39b8864 --- /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/33] 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 42f0d0f1e..9d106f664 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 @@ namespace Terminal.Gui { Right, /// - /// Scrolls one character to the right. + /// Scrolls one item (cell, character, etc...) to the right. /// ScrollRight, @@ -102,12 +102,12 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { Paste, /// - /// Quit a toplevel. + /// Quit a . /// QuitToplevel, @@ -356,37 +356,37 @@ namespace Terminal.Gui { 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 47d62b661..9d113e256 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { } } - 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 61ed818c7..b58aa9490 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 @@ namespace Terminal.Gui { get => allowsMarking; set { allowsMarking = value; + if (allowsMarking) { + AddKeyBinding (Key.Space, Command.ToggleChecked); + } else { + ClearKeybinding (Key.Space); + } + SetNeedsDisplay (); } } @@ -353,8 +360,6 @@ namespace Terminal.Gui { AddKeyBinding (Key.End, Command.BottomEnd); AddKeyBinding (Key.Enter, Command.OpenSelectedItem); - - AddKeyBinding (Key.Space, Command.ToggleChecked); } /// @@ -416,39 +421,30 @@ namespace Terminal.Gui { /// 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 f890e66cf..1d9bae358 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 000000000..1e731dfd1 --- /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 ac39b8864..eea4c76d0 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 @@ namespace UnitTests { "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 @@ namespace UnitTests { [Fact] public void TestSearchCollectionNavigator_ToSearchText () { - var s = new string []{ + var strings = new string []{ "appricot", "arm", "bat", @@ -37,19 +37,19 @@ namespace UnitTests { "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 @@ namespace UnitTests { "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 @@ namespace UnitTests { "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 @@ namespace UnitTests { "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/33] 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 9d113e256..dda4b30ad 100644 --- a/Terminal.Gui/Core/SearchCollectionNavigator.cs +++ b/Terminal.Gui/Core/SearchCollectionNavigator.cs @@ -12,14 +12,14 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { // 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 b58aa9490..930b6a517 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -439,7 +439,7 @@ namespace Terminal.Gui { 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 797cf09a6..e2a28fbcd 100644 --- a/UICatalog/Scenario.cs +++ b/UICatalog/Scenario.cs @@ -73,7 +73,7 @@ namespace UICatalog { /// 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 @@ namespace UICatalog { /// 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 @@ namespace UICatalog { /// 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 057dcb693..bd1afc40b 100644 --- a/UICatalog/Scenarios/ListViewWithSelection.cs +++ b/UICatalog/Scenarios/ListViewWithSelection.cs @@ -3,6 +3,7 @@ using System; 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 @@ namespace UICatalog.Scenarios { 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 @@ namespace UICatalog.Scenarios { // 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 @@ namespace UICatalog.Scenarios { 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 @@ namespace UICatalog.Scenarios { 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 9949c337f..6229c6090 100644 --- a/UICatalog/UICatalog.cs +++ b/UICatalog/UICatalog.cs @@ -53,7 +53,7 @@ namespace UICatalog { 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 @@ namespace UICatalog { 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 @@ namespace UICatalog { _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 @@ namespace UICatalog { 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 @@ namespace UICatalog { { 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 @@ namespace UICatalog { 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 @@ namespace UICatalog { public int Length => len; - public ScenarioListDataSource (List itemList) + public ScenarioListDataSource (List itemList) { Scenarios = itemList; len = GetMaxLengthItem (); @@ -560,8 +524,8 @@ namespace UICatalog { { 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 @@ namespace UICatalog { 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 @@ namespace UICatalog { } _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 47e94b216..ae40ea988 100644 --- a/UnitTests/ScenarioTests.cs +++ b/UnitTests/ScenarioTests.cs @@ -49,19 +49,18 @@ namespace Terminal.Gui { [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 @@ namespace Terminal.Gui { [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 @@ namespace Terminal.Gui { 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/33] Nuked ScenarioListDataSource --- UICatalog/UICatalog.cs | 73 ------------------------------------------ 1 file changed, 73 deletions(-) diff --git a/UICatalog/UICatalog.cs b/UICatalog/UICatalog.cs index 6229c6090..9cd1f865b 100644 --- a/UICatalog/UICatalog.cs +++ b/UICatalog/UICatalog.cs @@ -503,79 +503,6 @@ namespace UICatalog { } } - internal class ScenarioListDataSource : IListDataSource { - private readonly int len; - - public List Scenarios { get; set; } - - public bool IsMarked (int item) => false; - - public int Count => Scenarios.Count; - - public 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/33] 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 930b6a517..19e8605fb 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -191,7 +191,7 @@ namespace Terminal.Gui { } /// - /// 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 @@ namespace Terminal.Gui { } } } + 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/33] more progress --- .../Core/SearchCollectionNavigator.cs | 2 +- Terminal.Gui/Views/ListView.cs | 13 +- UICatalog/Scenarios/BordersComparisons.cs | 1 - UICatalog/UICatalog.cs | 229 +++++++++--------- 4 files changed, 117 insertions(+), 128 deletions(-) diff --git a/Terminal.Gui/Core/SearchCollectionNavigator.cs b/Terminal.Gui/Core/SearchCollectionNavigator.cs index dda4b30ad..34424dc42 100644 --- a/Terminal.Gui/Core/SearchCollectionNavigator.cs +++ b/Terminal.Gui/Core/SearchCollectionNavigator.cs @@ -22,7 +22,7 @@ namespace Terminal.Gui { 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 19e8605fb..1736209d9 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -442,10 +442,13 @@ namespace Terminal.Gui { // 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 @@ namespace Terminal.Gui { if (lastSelectedItem == -1) { EnsuresVisibilitySelectedItem (); - OnSelectedChanged (); + //OnSelectedChanged (); } return base.OnEnter (view); diff --git a/UICatalog/Scenarios/BordersComparisons.cs b/UICatalog/Scenarios/BordersComparisons.cs index baaabcae0..9ea462f52 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 9cd1f865b..cd625730a 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 @@ namespace UICatalog { 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 @@ namespace UICatalog { } 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 @@ namespace UICatalog { 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 @@ namespace UICatalog { #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 @@ namespace UICatalog { } /// - /// 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 @@ namespace UICatalog { 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 @@ namespace UICatalog { 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 @@ namespace UICatalog { _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 @@ namespace UICatalog { _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 @@ namespace UICatalog { _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); + 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 (_runningScenario != null) { - _runningScenario = null; + void TopHandler () + { + if (_selectedScenario != null) { + _selectedScenario = null; _isFirstRunning = false; } if (!_isFirstRunning) { _rightPane.SetFocus (); } - _top.Loaded -= TopHandler; + Application.Top.Loaded -= TopHandler; } - _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.Top.Loaded += TopHandler; - Application.Run (_top); - return _runningScenario; + // 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 (); + } } static List CreateDiagnosticMenuItems () @@ -329,7 +347,7 @@ namespace UICatalog { return menuItems.ToArray (); } - private static MenuItem[] CreateKeybindings() + private static MenuItem [] CreateKeybindings () { List menuItems = new List (); @@ -410,7 +428,7 @@ namespace UICatalog { } } ConsoleDriver.Diagnostics = _diagnosticFlags; - _top.SetNeedsDisplay (); + Application.Top.SetNeedsDisplay (); }; menuItems.Add (item); } @@ -464,14 +482,7 @@ namespace UICatalog { } } - 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 @@ namespace UICatalog { 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 @@ namespace UICatalog { 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 @@ namespace UICatalog { /// 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 @@ namespace UICatalog { 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/33] 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 1e731dfd1..30ae6111c 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 @@ namespace UICatalog.Scenarios { Height = Dim.Fill () }; Top.Add (vsep); + CreateTreeView (); } @@ -136,7 +135,7 @@ namespace UICatalog.Scenarios { _listView.SetSource (items); } - TreeView _treeView = null; + TreeView _treeView = null; private void CreateTreeView () { @@ -150,7 +149,7 @@ namespace UICatalog.Scenarios { }; 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 @@ namespace UICatalog.Scenarios { }; 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 @@ namespace UICatalog.Scenarios { "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/33] 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 1736209d9..1513cb7ed 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -126,6 +126,7 @@ namespace Terminal.Gui { get => source; set { source = value; + navigator = null; top = 0; selected = 0; lastSelectedItem = -1; @@ -439,7 +440,6 @@ namespace Terminal.Gui { // 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 @@ namespace Terminal.Gui { 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/33] 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 34424dc42..1ebb0dd19 100644 --- a/Terminal.Gui/Core/SearchCollectionNavigator.cs +++ b/Terminal.Gui/Core/SearchCollectionNavigator.cs @@ -129,5 +129,17 @@ namespace Terminal.Gui { 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 1736209d9..b1df9dd80 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -437,7 +437,7 @@ namespace Terminal.Gui { } // 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 4f8692d74..8628964d5 100644 --- a/Terminal.Gui/Views/TreeView.cs +++ b/Terminal.Gui/Views/TreeView.cs @@ -140,12 +140,12 @@ namespace Terminal.Gui { /// 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { /// Defaults to /// public CursorVisibility DesiredCursorVisibility { - get { + get { return MultiSelect ? desiredCursorVisibility : CursorVisibility.Invisible; } set { @@ -576,19 +577,43 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { /// /// /// - 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/33] 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 8628964d5..b26791608 100644 --- a/Terminal.Gui/Views/TreeView.cs +++ b/Terminal.Gui/Views/TreeView.cs @@ -608,6 +608,7 @@ namespace Terminal.Gui { 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/33] 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 eea4c76d0..b59f8f734 100644 --- a/UnitTests/SearchCollectionNavigatorTests.cs +++ b/UnitTests/SearchCollectionNavigatorTests.cs @@ -3,25 +3,41 @@ using Xunit; 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/33] 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 1ebb0dd19..8328154e3 100644 --- a/Terminal.Gui/Core/SearchCollectionNavigator.cs +++ b/Terminal.Gui/Core/SearchCollectionNavigator.cs @@ -139,7 +139,9 @@ namespace Terminal.Gui { /// 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 b26791608..308438393 100644 --- a/Terminal.Gui/Views/TreeView.cs +++ b/Terminal.Gui/Views/TreeView.cs @@ -594,7 +594,7 @@ namespace Terminal.Gui { // 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 @@ namespace Terminal.Gui { 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 30ae6111c..67533675c 100644 --- a/UICatalog/Scenarios/SearchCollectionNavigatorTester.cs +++ b/UICatalog/Scenarios/SearchCollectionNavigatorTester.cs @@ -150,9 +150,9 @@ namespace UICatalog.Scenarios { 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 @@ namespace UICatalog.Scenarios { 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/33] 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 8328154e3..360aab5cf 100644 --- a/Terminal.Gui/Core/SearchCollectionNavigator.cs +++ b/Terminal.Gui/Core/SearchCollectionNavigator.cs @@ -140,8 +140,8 @@ namespace Terminal.Gui { 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 2cf853f29..95a96dce6 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -691,7 +691,6 @@ namespace Terminal.Gui { 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 7880c952e..78259dd3b 100644 --- a/UICatalog/Scenarios/Keys.cs +++ b/UICatalog/Scenarios/Keys.cs @@ -51,8 +51,8 @@ namespace UICatalog.Scenarios { 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 67533675c..10feff7c3 100644 --- a/UICatalog/Scenarios/SearchCollectionNavigatorTester.cs +++ b/UICatalog/Scenarios/SearchCollectionNavigatorTester.cs @@ -19,6 +19,59 @@ namespace UICatalog.Scenarios { 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 @@ namespace UICatalog.Scenarios { 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 @@ namespace UICatalog.Scenarios { 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 @@ namespace UICatalog.Scenarios { 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/33] 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 360aab5cf..6c02b9664 100644 --- a/Terminal.Gui/Core/SearchCollectionNavigator.cs +++ b/Terminal.Gui/Core/SearchCollectionNavigator.cs @@ -4,25 +4,100 @@ using System.Linq; 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; } - public SearchCollectionNavigator (IEnumerable collection) { _collection = collection; } + /// + /// 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 int CalculateNewIndex (IEnumerable collection, int currentIndex, char keyStruck) + /// + /// 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)); + } + } + + /// + /// 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { } 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 @@ namespace Terminal.Gui { return -1; } + private void AssertCollectionIsNotNull () + { + if (Collection == null) { + throw new InvalidOperationException ("Collection is null"); + } + } + private void ClearState () { - state = ""; - lastKeystroke = DateTime.MinValue; - + 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 35a81965a..a6d43cb0b 100644 --- a/Terminal.Gui/Core/Trees/Branch.cs +++ b/Terminal.Gui/Core/Trees/Branch.cs @@ -89,8 +89,8 @@ namespace Terminal.Gui.Trees { 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 @@ namespace Terminal.Gui.Trees { /// 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 95a96dce6..388def50a 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -59,21 +59,6 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { /// 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 @@ namespace Terminal.Gui { /// 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 @@ namespace Terminal.Gui { get => source; set { source = value; - navigator = null; + Navigator.Collection = source?.ToList ()?.Cast (); top = 0; selected = 0; lastSelectedItem = -1; @@ -423,7 +407,10 @@ namespace Terminal.Gui { /// 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { } /// - 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 c6c2e0038..5ccf8b8c1 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 @@ using Terminal.Gui.Trees; 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 @@ namespace Terminal.Gui { /// /// 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 @@ namespace Terminal.Gui { } /// - /// 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 @@ namespace Terminal.Gui { /// /// 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 @@ namespace Terminal.Gui { /// 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 @@ namespace Terminal.Gui { } } - /// /// 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 @@ namespace Terminal.Gui { /// 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 @@ namespace Terminal.Gui { /// 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 @@ namespace Terminal.Gui { } } - /// - /// 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 @@ namespace Terminal.Gui { } /// - /// 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 @@ namespace Terminal.Gui { } /// - /// 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 @@ namespace Terminal.Gui { /// /// 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 @@ namespace Terminal.Gui { } /// - /// 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 @@ namespace Terminal.Gui { } } - /// - /// Removes all objects from the tree and clears + /// Removes all objects from the tree and clears . /// public void ClearObjects () { @@ -347,7 +338,7 @@ namespace Terminal.Gui { /// 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 @@ namespace Terminal.Gui { } /// - /// 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 @@ namespace Terminal.Gui { } /// - /// 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 @@ namespace Terminal.Gui { /// /// 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 @@ namespace Terminal.Gui { } /// - /// 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { } /// - /// 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 @@ namespace Terminal.Gui { /// /// 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { } } + /// + /// 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 @@ namespace Terminal.Gui { } try { - // First of all deal with any registered keybindings var result = InvokeKeybindings (keyEvent); if (result != null) { @@ -588,35 +586,24 @@ namespace Terminal.Gui { // 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 @@ namespace Terminal.Gui { /// /// 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 @@ namespace Terminal.Gui { } /// - /// 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 @@ namespace Terminal.Gui { /// /// 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 @@ namespace Terminal.Gui { /// /// 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 @@ namespace Terminal.Gui { } /// - /// 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 @@ namespace Terminal.Gui { } /// - /// 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 @@ namespace Terminal.Gui { } /// - /// Raises the event + /// Raises the event. /// /// protected virtual void OnObjectActivated (ObjectActivatedEventArgs e) @@ -725,15 +712,15 @@ namespace Terminal.Gui { } /// - /// 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 @@ namespace Terminal.Gui { SetFocus (); } - if (me.Flags == MouseFlags.WheeledDown) { ScrollDown (); @@ -814,7 +800,6 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { // 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 @@ namespace Terminal.Gui { } /// - /// 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 @@ namespace Terminal.Gui { } } - /// - /// 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 @@ namespace Terminal.Gui { /// /// Changes the to the first root object and resets - /// the to 0 + /// the to 0. /// public void GoToFirst () { @@ -931,7 +914,7 @@ namespace Terminal.Gui { /// /// 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 @@ namespace Terminal.Gui { /// /// 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 @@ namespace Terminal.Gui { } /// - /// 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { } /// - /// 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { /// /// 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 @@ namespace Terminal.Gui { } /// - /// Expands the currently + /// Expands the currently . /// public void Expand () { @@ -1168,9 +1147,9 @@ namespace Terminal.Gui { /// /// 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 @@ namespace Terminal.Gui { } /// - /// 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 @@ namespace Terminal.Gui { } /// /// 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 @@ namespace Terminal.Gui { } /// /// Returns true if the given object is exposed in the tree and can be - /// expanded otherwise false + /// expanded otherwise false. /// /// /// @@ -1222,7 +1201,7 @@ namespace Terminal.Gui { /// /// Returns true if the given object is exposed in the tree and - /// expanded otherwise false + /// expanded otherwise false. /// /// /// @@ -1240,26 +1219,26 @@ namespace Terminal.Gui { } /// - /// 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 @@ namespace Terminal.Gui { } /// - /// 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 @@ namespace Terminal.Gui { /// /// 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 @@ namespace Terminal.Gui { /// /// Returns true if the is either the - /// or part of a + /// or part of a . /// /// /// @@ -1365,7 +1342,7 @@ namespace Terminal.Gui { /// /// Selects all objects in the tree when is enabled otherwise - /// does nothing + /// does nothing. /// public void SelectAll () { @@ -1387,9 +1364,8 @@ namespace Terminal.Gui { OnSelectionChanged (new SelectionChangedEventArgs (this, SelectedObject, SelectedObject)); } - /// - /// Raises the SelectionChanged event + /// Raises the SelectionChanged event. /// /// protected virtual void OnSelectionChanged (SelectionChangedEventArgs e) @@ -1431,5 +1407,4 @@ namespace Terminal.Gui { return included.Contains (model); } } - } \ No newline at end of file diff --git a/UICatalog/Properties/launchSettings.json b/UICatalog/Properties/launchSettings.json index eda283ad8..e1f2b1db2 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 10feff7c3..80b514893 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 @@ namespace UICatalog.Scenarios { 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 @@ namespace UICatalog.Scenarios { }; Top.Add (vsep); CreateTreeView (); - } ListView _listView = null; @@ -128,7 +128,7 @@ namespace UICatalog.Scenarios { _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 @@ namespace UICatalog.Scenarios { 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 @@ namespace UICatalog.Scenarios { 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 @@ namespace UICatalog.Scenarios { 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 a9a29543d..3c2d12b4a 100644 --- a/UnitTests/ListViewTests.cs +++ b/UnitTests/ListViewTests.cs @@ -151,7 +151,7 @@ namespace Terminal.Gui.Views { public IList ToList () { - throw new NotImplementedException (); + return new List () { "One", "Two", "Three" }; } } diff --git a/UnitTests/SearchCollectionNavigatorTests.cs b/UnitTests/SearchCollectionNavigatorTests.cs index b59f8f734..b7e0a4df8 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 TestSearchCollectionNavigator_AtSymbol () + public void AtSymbol () { var strings = new string []{ - "appricot", - "arm", - "ta", - "@bob", - "@bb", - "text", - "egg", - "candle" - }; + "appricot", + "arm", + "ta", + "@bob", + "@bb", + "text", + "egg", + "candle" + }; 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 (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 Delay () + { + var strings = new string []{ + "$$", + "$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 (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/33] 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 80b514893..74dde8aef 100644 --- a/UICatalog/Scenarios/SearchCollectionNavigatorTester.cs +++ b/UICatalog/Scenarios/SearchCollectionNavigatorTester.cs @@ -7,9 +7,11 @@ using Terminal.Gui.Trees; 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 12fc949b2..ff587e042 100644 --- a/UICatalog/Scenarios/VkeyPacketSimulator.cs +++ b/UICatalog/Scenarios/VkeyPacketSimulator.cs @@ -6,7 +6,7 @@ using Terminal.Gui; 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/33] 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 78259dd3b..35cafbc21 100644 --- a/UICatalog/Scenarios/Keys.cs +++ b/UICatalog/Scenarios/Keys.cs @@ -51,7 +51,7 @@ namespace UICatalog.Scenarios { 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/33] 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 6c02b9664..c87865b6e 100644 --- a/Terminal.Gui/Core/SearchCollectionNavigator.cs +++ b/Terminal.Gui/Core/SearchCollectionNavigator.cs @@ -120,7 +120,7 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { // 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 @@ namespace Terminal.Gui { } else { // clear state because keypress was a control char - ClearState (); + ClearSearchString (); // control char indicates no selection return -1; @@ -155,7 +155,7 @@ namespace Terminal.Gui { } /// - /// 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 @@ namespace Terminal.Gui { /// 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 @@ namespace Terminal.Gui { } } - 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/33] 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 c87865b6e..cc3b1124f 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 @@ namespace Terminal.Gui { public IEnumerable Collection { get; set; } /// - /// Event arguments for the event. + /// Event arguments for the event. /// public class KeystrokeNavigatorEventArgs { /// @@ -162,7 +162,7 @@ namespace Terminal.Gui { /// 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 39d2e1ff2..79d2e4121 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 388def50a..957216837 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -110,7 +110,7 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 5ccf8b8c1..baab64642 100644 --- a/Terminal.Gui/Views/TreeView.cs +++ b/Terminal.Gui/Views/TreeView.cs @@ -547,7 +547,7 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { } /// - /// 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 @@ namespace Terminal.Gui { } // 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 @@ namespace Terminal.Gui { // 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 e1f2b1db2..ec419ec20 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 c747829e3..30767e190 100644 --- a/UICatalog/Scenario.cs +++ b/UICatalog/Scenario.cs @@ -233,7 +233,7 @@ namespace UICatalog { } /// - /// 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 @@ namespace UICatalog { 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 74dde8aef..d97f6890c 100644 --- a/UICatalog/Scenarios/SearchCollectionNavigatorTester.cs +++ b/UICatalog/Scenarios/CollectionNavigatorTester.cs @@ -6,13 +6,13 @@ using Terminal.Gui.Trees; 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 @@ namespace UICatalog.Scenarios { "egg", "candle", " <- space", + "\t<- tab", + "\n<- newline", + "\r<- formfeed", "q", "quit", "quitter" @@ -141,7 +144,7 @@ namespace UICatalog.Scenarios { _listView.SetSource (_items); - _listView.Navigator.SearchStringChanged += (state) => { + _listView.KeystrokeNavigator.SearchStringChanged += (state) => { label.Text = $"ListView: {state.SearchString}"; }; } @@ -169,16 +172,16 @@ namespace UICatalog.Scenarios { }; 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 bd1afc40b..c132cf8f5 100644 --- a/UICatalog/Scenarios/ListViewWithSelection.cs +++ b/UICatalog/Scenarios/ListViewWithSelection.cs @@ -21,7 +21,7 @@ namespace UICatalog.Scenarios { 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 b7e0a4df8..06ebc0000 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 @@ namespace Terminal.Gui.Core { [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 @@ namespace Terminal.Gui.Core { [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 @@ namespace Terminal.Gui.Core { [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 @@ namespace Terminal.Gui.Core { }; 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 @@ namespace Terminal.Gui.Core { "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 @@ namespace Terminal.Gui.Core { "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 @@ namespace Terminal.Gui.Core { "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 @@ namespace Terminal.Gui.Core { "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 @@ namespace Terminal.Gui.Core { "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 @@ namespace Terminal.Gui.Core { "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 @@ namespace Terminal.Gui.Core { "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 @@ namespace Terminal.Gui.Core { "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/33] 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 cc3b1124f..a9bf3f920 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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/33] 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 79d2e4121..77c964b34 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 3e7bbb09c26ac80f6dd667a880d87e826de2e81d Mon Sep 17 00:00:00 2001 From: Charlie Kindel Date: Tue, 1 Nov 2022 09:33:34 -0600 Subject: [PATCH 22/33] removed call to OnSelectedChange from OnEnter --- Terminal.Gui/Views/ListView.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Terminal.Gui/Views/ListView.cs b/Terminal.Gui/Views/ListView.cs index 1a37fea3a..4739339c3 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -705,7 +705,6 @@ namespace Terminal.Gui { if (lastSelectedItem == -1) { EnsuresVisibilitySelectedItem (); - OnSelectedChanged (); } return base.OnEnter (view); From bc846bf83aba5d1c85023d74e6284edfdbb7c141 Mon Sep 17 00:00:00 2001 From: Charlie Kindel Date: Tue, 1 Nov 2022 10:03:14 -0600 Subject: [PATCH 23/33] 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 06ebc0000..89975d5a3 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 @@ namespace Terminal.Gui.Core { 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 24/33] 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 89975d5a3..030856d92 100644 --- a/UnitTests/CollectionNavigatorTests.cs +++ b/UnitTests/CollectionNavigatorTests.cs @@ -266,7 +266,8 @@ namespace Terminal.Gui.Core { "can", "candle", "candy", - "yellow" + "yellow", + "zebra" }; int current = 0; var n = new CollectionNavigator (strings); @@ -298,8 +299,8 @@ namespace Terminal.Gui.Core { 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 25/33] 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 a9bf3f920..94546f134 100644 --- a/Terminal.Gui/Core/CollectionNavigator.cs +++ b/Terminal.Gui/Core/CollectionNavigator.cs @@ -135,6 +135,14 @@ namespace Terminal.Gui { 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 030856d92..96a4c1126 100644 --- a/UnitTests/CollectionNavigatorTests.cs +++ b/UnitTests/CollectionNavigatorTests.cs @@ -282,6 +282,7 @@ namespace Terminal.Gui.Core { // 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 @@ namespace Terminal.Gui.Core { 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 26/33] 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 94546f134..6637daf20 100644 --- a/Terminal.Gui/Core/CollectionNavigator.cs +++ b/Terminal.Gui/Core/CollectionNavigator.cs @@ -135,13 +135,13 @@ namespace Terminal.Gui { 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 96a4c1126..d0de09f3a 100644 --- a/UnitTests/CollectionNavigatorTests.cs +++ b/UnitTests/CollectionNavigatorTests.cs @@ -42,29 +42,6 @@ namespace Terminal.Gui.Core { 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 @@ namespace Terminal.Gui.Core { }; 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 @@ namespace Terminal.Gui.Core { }; 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" so it jumps to a for appricot - Assert.Equal (0, n.GetNextMatchingItem (4, 'a')); + // nothing starts "丗丙丛a". Since were still in the timedelay we do not jump to appricot + Assert.Equal (strings.IndexOf ("丗丙丛"), current = n.GetNextMatchingItem (current, '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 @@ namespace Terminal.Gui.Core { 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 @@ namespace Terminal.Gui.Core { 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 @@ namespace Terminal.Gui.Core { 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); From 060cb77e95f058d765202108727b77113ce27970 Mon Sep 17 00:00:00 2001 From: BDisp Date: Wed, 2 Nov 2022 12:37:57 +0000 Subject: [PATCH 27/33] Fix IsOverridden method per @tig in https://github.com/gui-cs/Terminal.Gui/issues/2156#issuecomment-1299338732 --- Terminal.Gui/Core/View.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/Terminal.Gui/Core/View.cs b/Terminal.Gui/Core/View.cs index 0fcd73b07..7cafe2ad8 100644 --- a/Terminal.Gui/Core/View.cs +++ b/Terminal.Gui/Core/View.cs @@ -3080,12 +3080,17 @@ namespace Terminal.Gui { /// The view. /// The method name. /// if it's overridden, otherwise. - public bool IsOverridden (View view, string method) + public static bool IsOverridden (View view, string method) { - Type t = view.GetType (); - MethodInfo m = t.GetMethod (method); - - return (m.DeclaringType == t || m.ReflectedType == t) && m.GetBaseDefinition ().DeclaringType == typeof (Responder); + MethodInfo m = view.GetType ().GetMethod (method, + BindingFlags.Instance + | BindingFlags.Public + | BindingFlags.NonPublic + | BindingFlags.DeclaredOnly); + if (m == null) { + return false; + } + return m.GetBaseDefinition ().DeclaringType != m.DeclaringType; } } } From ab45da5b60876530d5dbdf80ff194bf98816a71e Mon Sep 17 00:00:00 2001 From: BDisp Date: Wed, 2 Nov 2022 12:40:56 +0000 Subject: [PATCH 28/33] Using ConsoleWidth is more accurate. --- Terminal.Gui/Views/ComboBox.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Terminal.Gui/Views/ComboBox.cs b/Terminal.Gui/Views/ComboBox.cs index c26a340ed..173dc188d 100644 --- a/Terminal.Gui/Views/ComboBox.cs +++ b/Terminal.Gui/Views/ComboBox.cs @@ -484,7 +484,7 @@ namespace Terminal.Gui { search.SetFocus (); } - search.CursorPosition = search.Text.RuneCount; + search.CursorPosition = search.Text.ConsoleWidth; return base.OnEnter (view); } From f60476ddc2174679380abffab024029b0362fa3f Mon Sep 17 00:00:00 2001 From: BDisp Date: Wed, 2 Nov 2022 12:42:26 +0000 Subject: [PATCH 29/33] Testing all the frame. --- UnitTests/ComboBoxTests.cs | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/UnitTests/ComboBoxTests.cs b/UnitTests/ComboBoxTests.cs index bf5b385c0..41adc4b40 100644 --- a/UnitTests/ComboBoxTests.cs +++ b/UnitTests/ComboBoxTests.cs @@ -826,9 +826,9 @@ Three ", output); TestHelpers.AssertDriverColorsAre (@" 000000 -00000 -22222 -22222", attributes); +222222 +222222 +222222", attributes); Assert.True (cb.Subviews [1].ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ()))); Assert.Equal ("", selected); @@ -838,9 +838,9 @@ Three ", output); cb.Redraw (cb.Bounds); TestHelpers.AssertDriverColorsAre (@" 000000 -22222 -00000 -22222", attributes); +222222 +000002 +222222", attributes); Assert.True (cb.Subviews [1].ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ()))); Assert.Equal ("", selected); @@ -850,9 +850,9 @@ Three ", output); cb.Redraw (cb.Bounds); TestHelpers.AssertDriverColorsAre (@" 000000 -22222 -22222 -00000", attributes); +222222 +222222 +000002", attributes); Assert.True (cb.Subviews [1].ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ()))); Assert.Equal ("Three", selected); @@ -868,9 +868,9 @@ Three ", output); cb.Redraw (cb.Bounds); TestHelpers.AssertDriverColorsAre (@" 000000 -22222 -22222 -00000", attributes); +222222 +222222 +000002", attributes); Assert.True (cb.Subviews [1].ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ()))); Assert.Equal ("Three", selected); @@ -880,9 +880,9 @@ Three ", output); cb.Redraw (cb.Bounds); TestHelpers.AssertDriverColorsAre (@" 000000 -22222 -00000 -11111", attributes); +222222 +000002 +111112", attributes); Assert.True (cb.Subviews [1].ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ()))); Assert.Equal ("Three", selected); @@ -892,9 +892,9 @@ Three ", output); cb.Redraw (cb.Bounds); TestHelpers.AssertDriverColorsAre (@" 000000 -00000 -22222 -11111", attributes); +000002 +222222 +111112", attributes); Assert.True (cb.ProcessKey (new KeyEvent (Key.F4, new KeyModifiers ()))); Assert.Equal ("Three", selected); From 7e8f90d8753e57e1d88b5ab1bc216b501a1f1192 Mon Sep 17 00:00:00 2001 From: BDisp Date: Wed, 2 Nov 2022 12:43:30 +0000 Subject: [PATCH 30/33] IsOverridden unit tests per @tig. --- UnitTests/ViewTests.cs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/UnitTests/ViewTests.cs b/UnitTests/ViewTests.cs index e2a20e80b..03969c4a7 100644 --- a/UnitTests/ViewTests.cs +++ b/UnitTests/ViewTests.cs @@ -4062,5 +4062,29 @@ This is a tes Assert.False (view.IsKeyPress); Assert.True (view.IsKeyUp); } + + [Fact, AutoInitShutdown] + public void IsOverridden_False_IfNotOverriden () + { + var view = new DerivedView () { Text = "DerivedView does not override MouseEvent", Width = 10, Height = 10 }; + + Assert.False (View.IsOverridden (view, "MouseEvent")); + + var view2 = new Button () { Text = "Button does not overrides OnKeyDown", Width = 10, Height = 10 }; + + Assert.False (View.IsOverridden (view2, "OnKeyDown")); + } + + [Fact, AutoInitShutdown] + public void IsOverridden_True_IfOverriden () + { + var view = new Button () { Text = "Button overrides MouseEvent", Width = 10, Height = 10 }; + + Assert.True (View.IsOverridden (view, "MouseEvent")); + + var view2 = new DerivedView () { Text = "DerivedView overrides OnKeyDown", Width = 10, Height = 10 }; + + Assert.True (View.IsOverridden (view2, "OnKeyDown")); + } } } From fb7f8a72655cc1340edfcfd9323a4c294507add4 Mon Sep 17 00:00:00 2001 From: Charlie Kindel Date: Wed, 2 Nov 2022 15:41:29 -0600 Subject: [PATCH 31/33] manual update of branch --- Terminal.Gui/Core/View.cs | 13 +++++++++---- Terminal.Gui/Views/ComboBox.cs | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/Terminal.Gui/Core/View.cs b/Terminal.Gui/Core/View.cs index 0fcd73b07..f59de33ee 100644 --- a/Terminal.Gui/Core/View.cs +++ b/Terminal.Gui/Core/View.cs @@ -3082,10 +3082,15 @@ namespace Terminal.Gui { /// if it's overridden, otherwise. public bool IsOverridden (View view, string method) { - Type t = view.GetType (); - MethodInfo m = t.GetMethod (method); - - return (m.DeclaringType == t || m.ReflectedType == t) && m.GetBaseDefinition ().DeclaringType == typeof (Responder); + MethodInfo m = view.GetType ().GetMethod (method, + BindingFlags.Instance + | BindingFlags.Public + | BindingFlags.NonPublic + | BindingFlags.DeclaredOnly); + if (m == null) { + return false; + } + return m.GetBaseDefinition ().DeclaringType != m.DeclaringType; } } } diff --git a/Terminal.Gui/Views/ComboBox.cs b/Terminal.Gui/Views/ComboBox.cs index c26a340ed..173dc188d 100644 --- a/Terminal.Gui/Views/ComboBox.cs +++ b/Terminal.Gui/Views/ComboBox.cs @@ -484,7 +484,7 @@ namespace Terminal.Gui { search.SetFocus (); } - search.CursorPosition = search.Text.RuneCount; + search.CursorPosition = search.Text.ConsoleWidth; return base.OnEnter (view); } From b29754cc5af9fe6cae1299f76879e9ae224d3ed4 Mon Sep 17 00:00:00 2001 From: Charlie Kindel Date: Wed, 2 Nov 2022 16:18:14 -0600 Subject: [PATCH 32/33] moves isovverriden to Responder, adds unit tests --- Terminal.Gui/Core/Responder.cs | 20 ++++++++++++++ Terminal.Gui/Core/View.cs | 19 -------------- UnitTests/ResponderTests.cs | 48 ++++++++++++++++++++++++++++++++++ UnitTests/ViewTests.cs | 24 ----------------- 4 files changed, 68 insertions(+), 43 deletions(-) diff --git a/Terminal.Gui/Core/Responder.cs b/Terminal.Gui/Core/Responder.cs index 37de82145..7b92f8a04 100644 --- a/Terminal.Gui/Core/Responder.cs +++ b/Terminal.Gui/Core/Responder.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Reflection; namespace Terminal.Gui { /// @@ -236,6 +237,25 @@ namespace Terminal.Gui { /// public virtual void OnVisibleChanged () { } + /// + /// Utilty function to determine is overridden in the . + /// + /// The view. + /// The method name. + /// if it's overridden, otherwise. + internal static bool IsOverridden (Responder subclass, string method) + { + MethodInfo m = subclass.GetType ().GetMethod (method, + BindingFlags.Instance + | BindingFlags.Public + | BindingFlags.NonPublic + | BindingFlags.DeclaredOnly); + if (m == null) { + return false; + } + return m.GetBaseDefinition ().DeclaringType != m.DeclaringType; + } + /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// diff --git a/Terminal.Gui/Core/View.cs b/Terminal.Gui/Core/View.cs index 7cafe2ad8..488478991 100644 --- a/Terminal.Gui/Core/View.cs +++ b/Terminal.Gui/Core/View.cs @@ -3073,24 +3073,5 @@ namespace Terminal.Gui { return top; } - - /// - /// Check if the is overridden in the . - /// - /// The view. - /// The method name. - /// if it's overridden, otherwise. - public static bool IsOverridden (View view, string method) - { - MethodInfo m = view.GetType ().GetMethod (method, - BindingFlags.Instance - | BindingFlags.Public - | BindingFlags.NonPublic - | BindingFlags.DeclaredOnly); - if (m == null) { - return false; - } - return m.GetBaseDefinition ().DeclaringType != m.DeclaringType; - } } } diff --git a/UnitTests/ResponderTests.cs b/UnitTests/ResponderTests.cs index 7f2d927f8..3b21a9af7 100644 --- a/UnitTests/ResponderTests.cs +++ b/UnitTests/ResponderTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using Terminal.Gui; using Xunit; +using static Terminal.Gui.Core.ViewTests; // Alias Console to MockConsole so we don't accidentally use Console using Console = Terminal.Gui.FakeConsole; @@ -44,5 +45,52 @@ namespace Terminal.Gui.Core { { } + + public class DerivedView : View { + public DerivedView () + { + } + + public override bool OnKeyDown (KeyEvent keyEvent) + { + return true; + } + } + + [Fact] + public void IsOverridden_False_IfNotOverridden () + { + // MouseEvent IS defined on Responder but NOT overridden + Assert.False (Responder.IsOverridden (new Responder () { }, "MouseEvent")); + + // MouseEvent is defined on Responder and NOT overrident on View + Assert.False (Responder.IsOverridden (new View () { Text = "View does not override MouseEvent" }, "MouseEvent")); + Assert.False (Responder.IsOverridden (new DerivedView () { Text = "DerivedView does not override MouseEvent" }, "MouseEvent")); + + // MouseEvent is NOT defined on DerivedView + Assert.False (Responder.IsOverridden (new DerivedView () { Text = "DerivedView does not override MouseEvent" }, "MouseEvent")); + + // OnKeyDown is defined on View and NOT overrident on Button + Assert.False (Responder.IsOverridden (new Button () { Text = "Button does not override OnKeyDown" }, "OnKeyDown")); + } + + [Fact] + public void IsOverridden_True_IfOverridden () + { + // MouseEvent is defined on Responder IS overriden on ScrollBarView (but not View) + Assert.True (Responder.IsOverridden (new ScrollBarView () { Text = "ScrollBarView overrides MouseEvent" }, "MouseEvent")); + + // OnKeyDown is defined on View + Assert.True (Responder.IsOverridden (new View () { Text = "View overrides OnKeyDown" }, "OnKeyDown")); + + // OnKeyDown is defined on DerivedView + Assert.True (Responder.IsOverridden (new DerivedView () { Text = "DerivedView overrides OnKeyDown" }, "OnKeyDown")); + + // ScrollBarView overrides both MouseEvent (from Responder) and Redraw (from View) + Assert.True (Responder.IsOverridden (new ScrollBarView () { Text = "ScrollBarView overrides MouseEvent" }, "MouseEvent")); + Assert.True (Responder.IsOverridden (new ScrollBarView () { Text = "ScrollBarView overrides Redraw" }, "Redraw")); + + Assert.True (Responder.IsOverridden (new Button () { Text = "Button overrides MouseEvent" }, "MouseEvent")); + } } } diff --git a/UnitTests/ViewTests.cs b/UnitTests/ViewTests.cs index 03969c4a7..e2a20e80b 100644 --- a/UnitTests/ViewTests.cs +++ b/UnitTests/ViewTests.cs @@ -4062,29 +4062,5 @@ This is a tes Assert.False (view.IsKeyPress); Assert.True (view.IsKeyUp); } - - [Fact, AutoInitShutdown] - public void IsOverridden_False_IfNotOverriden () - { - var view = new DerivedView () { Text = "DerivedView does not override MouseEvent", Width = 10, Height = 10 }; - - Assert.False (View.IsOverridden (view, "MouseEvent")); - - var view2 = new Button () { Text = "Button does not overrides OnKeyDown", Width = 10, Height = 10 }; - - Assert.False (View.IsOverridden (view2, "OnKeyDown")); - } - - [Fact, AutoInitShutdown] - public void IsOverridden_True_IfOverriden () - { - var view = new Button () { Text = "Button overrides MouseEvent", Width = 10, Height = 10 }; - - Assert.True (View.IsOverridden (view, "MouseEvent")); - - var view2 = new DerivedView () { Text = "DerivedView overrides OnKeyDown", Width = 10, Height = 10 }; - - Assert.True (View.IsOverridden (view2, "OnKeyDown")); - } } } From bc617252c477ea73d3a026facc19172adc6899b8 Mon Sep 17 00:00:00 2001 From: Charlie Kindel Date: Wed, 2 Nov 2022 16:29:11 -0600 Subject: [PATCH 33/33] removed files not having to with this PR --- Terminal.Gui/Views/ComboBox.cs | 2 +- Terminal.Gui/Views/ListView.cs | 1 + UnitTests/ComboBoxTests.cs | 36 +++++++++++++++++----------------- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/Terminal.Gui/Views/ComboBox.cs b/Terminal.Gui/Views/ComboBox.cs index 173dc188d..c26a340ed 100644 --- a/Terminal.Gui/Views/ComboBox.cs +++ b/Terminal.Gui/Views/ComboBox.cs @@ -484,7 +484,7 @@ namespace Terminal.Gui { search.SetFocus (); } - search.CursorPosition = search.Text.ConsoleWidth; + search.CursorPosition = search.Text.RuneCount; return base.OnEnter (view); } diff --git a/Terminal.Gui/Views/ListView.cs b/Terminal.Gui/Views/ListView.cs index 6997ce5ce..957216837 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -728,6 +728,7 @@ namespace Terminal.Gui { if (lastSelectedItem == -1) { EnsuresVisibilitySelectedItem (); + OnSelectedChanged (); } return base.OnEnter (view); diff --git a/UnitTests/ComboBoxTests.cs b/UnitTests/ComboBoxTests.cs index 41adc4b40..bf5b385c0 100644 --- a/UnitTests/ComboBoxTests.cs +++ b/UnitTests/ComboBoxTests.cs @@ -826,9 +826,9 @@ Three ", output); TestHelpers.AssertDriverColorsAre (@" 000000 -222222 -222222 -222222", attributes); +00000 +22222 +22222", attributes); Assert.True (cb.Subviews [1].ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ()))); Assert.Equal ("", selected); @@ -838,9 +838,9 @@ Three ", output); cb.Redraw (cb.Bounds); TestHelpers.AssertDriverColorsAre (@" 000000 -222222 -000002 -222222", attributes); +22222 +00000 +22222", attributes); Assert.True (cb.Subviews [1].ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ()))); Assert.Equal ("", selected); @@ -850,9 +850,9 @@ Three ", output); cb.Redraw (cb.Bounds); TestHelpers.AssertDriverColorsAre (@" 000000 -222222 -222222 -000002", attributes); +22222 +22222 +00000", attributes); Assert.True (cb.Subviews [1].ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ()))); Assert.Equal ("Three", selected); @@ -868,9 +868,9 @@ Three ", output); cb.Redraw (cb.Bounds); TestHelpers.AssertDriverColorsAre (@" 000000 -222222 -222222 -000002", attributes); +22222 +22222 +00000", attributes); Assert.True (cb.Subviews [1].ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ()))); Assert.Equal ("Three", selected); @@ -880,9 +880,9 @@ Three ", output); cb.Redraw (cb.Bounds); TestHelpers.AssertDriverColorsAre (@" 000000 -222222 -000002 -111112", attributes); +22222 +00000 +11111", attributes); Assert.True (cb.Subviews [1].ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ()))); Assert.Equal ("Three", selected); @@ -892,9 +892,9 @@ Three ", output); cb.Redraw (cb.Bounds); TestHelpers.AssertDriverColorsAre (@" 000000 -000002 -222222 -111112", attributes); +00000 +22222 +11111", attributes); Assert.True (cb.ProcessKey (new KeyEvent (Key.F4, new KeyModifiers ()))); Assert.Equal ("Three", selected);