From 18ec9a2a70a0586bd2d136fe5de36b18c3e3fee1 Mon Sep 17 00:00:00 2001 From: Tig Kindel Date: Mon, 24 Oct 2022 18:56:06 -0600 Subject: [PATCH] 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')); } } }