diff --git a/Examples/UICatalog/Scenarios/CollectionNavigatorTester.cs b/Examples/UICatalog/Scenarios/CollectionNavigatorTester.cs index fd2d3562f5..2e075cba21 100644 --- a/Examples/UICatalog/Scenarios/CollectionNavigatorTester.cs +++ b/Examples/UICatalog/Scenarios/CollectionNavigatorTester.cs @@ -4,10 +4,7 @@ namespace UICatalog.Scenarios; -[ScenarioMetadata ( - "Collection Navigator", - "Demonstrates keyboard navigation in ListView & TreeView (CollectionNavigator)." - )] +[ScenarioMetadata ("Collection Navigator", "Demonstrates keyboard navigation in ListView & TreeView (CollectionNavigator).")] [ScenarioCategory ("Controls")] [ScenarioCategory ("ListView")] [ScenarioCategory ("TreeView")] @@ -15,8 +12,7 @@ namespace UICatalog.Scenarios; [ScenarioCategory ("Mouse and Keyboard")] public class CollectionNavigatorTester : Scenario { - private ObservableCollection _items = new ( - [ + private ObservableCollection _items = new ([ "a", "b", "bb", @@ -70,8 +66,7 @@ public class CollectionNavigatorTester : Scenario "q", "quit", "quitter" - ] - ); + ]); private Window? _top; private ListView? _listView; @@ -85,102 +80,46 @@ public override void Main () using IApplication app = Application.Create (); app.Init (); - using Window top = new () - { - SchemeName = "Base" - }; + using Window top = new (); + top.SchemeName = "Base"; _top = top; // MenuBar MenuBar menu = new (); - _allowMarkingCheckBox = new () - { - Title = "Allow _Marking" - }; + _allowMarkingCheckBox = new CheckBox { Title = "Allow _Marking" }; _allowMarkingCheckBox.ValueChanged += (_, _) => - { - if (_listView is not null) - { - _listView.ShowMarks = _allowMarkingCheckBox.Value == CheckState.Checked; - } - - if (_allowMultiSelectionCheckBox is not null) - { - _allowMultiSelectionCheckBox.Enabled = _allowMarkingCheckBox.Value == CheckState.Checked; - } - }; - - _allowMultiSelectionCheckBox = new () - { - Title = "Allow Multi _Selection", - Enabled = false - }; + { + _listView?.ShowMarks = _allowMarkingCheckBox.Value == CheckState.Checked; - _allowMultiSelectionCheckBox.ValueChanged += (_, _) => - { - if (_listView is not null) - { - _listView.MarkMultiple = - _allowMultiSelectionCheckBox.Value == CheckState.Checked; - } - }; - - menu.Add ( - new MenuBarItem ( - "_Configure", - [ - new MenuItem - { - CommandView = _allowMarkingCheckBox - }, - new MenuItem - { - CommandView = _allowMultiSelectionCheckBox - }, - new MenuItem - { - Title = Strings.cmdQuit, - Key = Application.GetDefaultKey (Command.Quit), - Action = Quit - } - ] - ) - ); - - menu.Add ( - new MenuBarItem ( - Strings.cmdQuit, + _allowMultiSelectionCheckBox?.Enabled = _allowMarkingCheckBox.Value == CheckState.Checked; + }; + + _allowMultiSelectionCheckBox = new CheckBox { Title = "Allow Multi _Selection", Enabled = false }; + + _allowMultiSelectionCheckBox.ValueChanged += (_, _) => { _listView?.MarkMultiple = _allowMultiSelectionCheckBox.Value == CheckState.Checked; }; + + menu.Add (new MenuBarItem ("_Configure", [ - new MenuItem - { - Title = Strings.cmdQuit, - Key = Application.GetDefaultKey (Command.Quit), - Action = Quit - } - ] - ) - ); + new MenuItem { CommandView = _allowMarkingCheckBox }, + new MenuItem { CommandView = _allowMultiSelectionCheckBox }, + new MenuItem { Title = Strings.cmdQuit, Key = Application.GetDefaultKey (Command.Quit), Action = Quit } + ])); + + menu.Add (new MenuBarItem (Strings.cmdQuit, [new MenuItem { Title = Strings.cmdQuit, Key = Application.GetDefaultKey (Command.Quit), Action = Quit }])); top.Add (menu); - _items = new (_items.OrderBy (i => i, StringComparer.OrdinalIgnoreCase)); + _items = new ObservableCollection (_items.OrderBy (i => i, StringComparer.OrdinalIgnoreCase)); CreateListView (); - Line vsep = new () - { - Orientation = Orientation.Vertical, - X = Pos.Right (_listView!), - Y = 1, - Height = Dim.Fill () - }; + Line vsep = new () { Orientation = Orientation.Vertical, X = Pos.Right (_listView!), Y = 1, Height = Dim.Fill () }; top.Add (vsep); CreateTreeView (); app.Run (top); - top.Dispose (); } private void CreateListView () @@ -201,7 +140,7 @@ private void CreateListView () }; _top.Add (label); - _listView = new () + _listView = new ListView { X = 0, Y = Pos.Bottom (label), @@ -214,7 +153,7 @@ private void CreateListView () _listView.SetSource (_items); - _listView.KeystrokeNavigator.SearchStringChanged += (_, e) => { label.Text = $"ListView: {e.SearchString}"; }; + _listView.KeystrokeNavigator?.SearchStringChanged += (_, e) => { label.Text = $"ListView: {e.SearchString}"; }; } private void CreateTreeView () @@ -235,29 +174,15 @@ private void CreateTreeView () }; _top.Add (label); - _treeView = new TreeView - { - X = Pos.Right (_listView) + 1, - Y = Pos.Bottom (label), - Width = Dim.Fill (), - Height = Dim.Fill () - }; + _treeView = new TreeView { X = Pos.Right (_listView) + 1, Y = Pos.Bottom (label), Width = Dim.Fill (), Height = Dim.Fill () }; _treeView.Style.HighlightModelTextOnly = true; _top.Add (_treeView); - TreeNode root = new () { Text = "IsLetterOrDigit examples" }; + TreeNode root = new () { Text = "IsLetterOrDigit examples", Children = _items.Where (i => char.IsLetterOrDigit (i [0])).Select (i => new TreeNode { Text = i }).Cast ().ToList () }; - root.Children = _items.Where (i => char.IsLetterOrDigit (i [0])) - .Select (i => new TreeNode () { Text = i }) - .Cast () - .ToList (); _treeView.AddObject (root); - root = new () { Text = "Non-IsLetterOrDigit examples" }; + root = new TreeNode { Text = "Non-IsLetterOrDigit examples", Children = _items.Where (i => !char.IsLetterOrDigit (i [0])).Select (i => new TreeNode { Text = i }).Cast ().ToList () }; - root.Children = _items.Where (i => !char.IsLetterOrDigit (i [0])) - .Select (i => new TreeNode () { Text = i }) - .Cast () - .ToList (); _treeView.AddObject (root); _treeView.ExpandAll (); _treeView.GoToFirst (); @@ -265,5 +190,5 @@ private void CreateTreeView () _treeView.KeystrokeNavigator.SearchStringChanged += (_, e) => { label.Text = $"TreeView: {e.SearchString}"; }; } - private void Quit () { _top?.RequestStop (); } + private void Quit () => _top?.RequestStop (); } diff --git a/Examples/UICatalog/UICatalogRunnable.cs b/Examples/UICatalog/UICatalogRunnable.cs index 5ca32173d1..b028f414f1 100644 --- a/Examples/UICatalog/UICatalogRunnable.cs +++ b/Examples/UICatalog/UICatalogRunnable.cs @@ -181,15 +181,19 @@ View [] CreateThemeMenuItems () { List menuItems = []; - _force16ColorsMenuItemCb = new CheckBox { Title = "Force _16 Colors", Value = Driver.Force16Colors ? CheckState.Checked : CheckState.UnChecked }; + _force16ColorsMenuItemCb = new CheckBox { Title = "Force _16 Colors", Value = Driver is { Force16Colors: true } ? CheckState.Checked : CheckState.UnChecked }; menuItems.Add (new MenuItem { CommandView = _force16ColorsMenuItemCb, Action = () => { + if (Driver is null) + { + return; + } Driver.Force16Colors = !Driver.Force16Colors; - _force16ColorsShortcutCb?.Value = Driver.Force16Colors ? CheckState.Checked : CheckState.UnChecked; + _force16ColorsShortcutCb?.Value = Driver is { Force16Colors: true } ? CheckState.Checked : CheckState.UnChecked; SetNeedsDraw (); } }); @@ -818,7 +822,7 @@ private StatusBar CreateStatusBar () _force16ColorsShortcutCb = new CheckBox { - Title = "16 color mode", Value = Driver.Force16Colors ? CheckState.Checked : CheckState.UnChecked, CanFocus = true + Title = "16 color mode", Value = Driver is { Force16Colors: true } ? CheckState.Checked : CheckState.UnChecked, CanFocus = true }; Shortcut force16ColorsShortcut = new () @@ -830,6 +834,10 @@ private StatusBar CreateStatusBar () Key = GetFirstUnboundFKey ([statusBarShortcut.Key]), Action = () => { + if (Driver is null) + { + return; + } Driver.Force16Colors = !Driver.Force16Colors; _force16ColorsMenuItemCb?.Value = Driver.Force16Colors ? CheckState.Checked : CheckState.UnChecked; SetNeedsDraw (); @@ -877,7 +885,7 @@ private void ConfigApplied () _shQuit?.Key = Application.GetDefaultKey (Command.Quit); _disableMouseCb?.Value = App?.Mouse.IsMouseDisabled == true ? CheckState.Checked : CheckState.UnChecked; - _force16ColorsShortcutCb?.Value = Driver.Force16Colors ? CheckState.Checked : CheckState.UnChecked; + _force16ColorsShortcutCb?.Value = Driver is { Force16Colors: true } ? CheckState.Checked : CheckState.UnChecked; App?.TopRunnableView?.SetNeedsDraw (); } diff --git a/Terminal.Gui/Views/ListView/ListView.cs b/Terminal.Gui/Views/ListView/ListView.cs index 9f26c97e02..abf824fe8a 100644 --- a/Terminal.Gui/Views/ListView/ListView.cs +++ b/Terminal.Gui/Views/ListView/ListView.cs @@ -58,7 +58,8 @@ namespace Terminal.Gui.Views; /// Home / End Moves to the first or last item. /// /// -/// Shift+<movement> Extends the selection in the given direction. +/// Shift+<movement> +/// Extends the selection in the given direction. /// /// /// Ctrl+A Selects all items. @@ -76,7 +77,8 @@ namespace Terminal.Gui.Views; /// Click Activates (selects) the clicked item. /// /// -/// Double-Click Accepts the clicked item (). +/// Double-Click +/// Accepts the clicked item (). /// /// /// Wheel Up / Down Scrolls the list. @@ -96,6 +98,22 @@ public ListView () SetupBindingsAndCommands (); } + /// + public bool EnableForDesign () + { + ListWrapper source = new (["List Item 1", "List Item two", "List Item 3", "List Item Quattro", "Last List Item"]); + Source = source; + + return true; + } + + /// + protected override void Dispose (bool disposing) + { + Source?.Dispose (); + base.Dispose (disposing); + } + #region IListDataSource /// @@ -166,7 +184,7 @@ public IListDataSource? Source { field.CollectionChanged += SourceOnCollectionChanged; SetContentSize (new Size (EffectiveMaxItemLength, field?.Count ?? Viewport.Height)); - KeystrokeNavigator.Collection = field?.ToList (); + KeystrokeNavigator?.Collection = field?.ToList (); } SelectedItem = null; @@ -240,11 +258,30 @@ protected virtual void OnSourceChanged () { } #region Keystroke Navigation + private IListCollectionNavigator? _keystrokeNavigator = new CollectionNavigator (); + /// - /// Gets the that searches the collection as the - /// user types. + /// Gets or sets the that searches the collection as + /// the user types. The default implementation is a that uses the string + /// representation of the items in the collection. Set to to disable keystroke navigation. /// - public IListCollectionNavigator KeystrokeNavigator { get; } = new CollectionNavigator (); + /// + /// When a new navigator is assigned, its is automatically + /// synchronized with the current . + /// + public IListCollectionNavigator? KeystrokeNavigator + { + get => _keystrokeNavigator; + set + { + _keystrokeNavigator = value; + + if (_keystrokeNavigator is { } && Source is { }) + { + _keystrokeNavigator.Collection = Source.ToList (); + } + } + } /// protected override bool OnKeyDown (Key key) @@ -257,7 +294,7 @@ protected override bool OnKeyDown (Key key) } // Enable user to find & select an item by typing text - if (!KeystrokeNavigator.Matcher.IsCompatibleKey (key)) + if (KeystrokeNavigator is null || !KeystrokeNavigator.Matcher.IsCompatibleKey (key)) { return false; } @@ -277,20 +314,4 @@ protected override bool OnKeyDown (Key key) } #endregion Keystroke Navigation - - /// - public bool EnableForDesign () - { - ListWrapper source = new (["List Item 1", "List Item two", "List Item 3", "List Item Quattro", "Last List Item"]); - Source = source; - - return true; - } - - /// - protected override void Dispose (bool disposing) - { - Source?.Dispose (); - base.Dispose (disposing); - } } diff --git a/Tests/UnitTestsParallelizable/Views/ListViewTests.cs b/Tests/UnitTestsParallelizable/Views/ListViewTests.cs index 337f8ba74f..f3c09fa7b1 100644 --- a/Tests/UnitTestsParallelizable/Views/ListViewTests.cs +++ b/Tests/UnitTestsParallelizable/Views/ListViewTests.cs @@ -582,7 +582,7 @@ public void ListViewCollectionNavigatorMatcher_IgnoreKeys () matchNone.Setup (m => m.IsCompatibleKey (It.IsAny ())).Returns (false); - lv.KeystrokeNavigator.Matcher = matchNone.Object; + lv.KeystrokeNavigator?.Matcher = matchNone.Object; // Keys are ignored because IsCompatibleKey returned false i.e. don't use these keys for navigation Assert.False (lv.NewKeyDownEvent (Key.B)); @@ -593,6 +593,51 @@ public void ListViewCollectionNavigatorMatcher_IgnoreKeys () matchNone.Verify (m => m.IsMatch (It.IsAny (), It.IsAny ()), Times.Never ()); } + // Copilot + [Fact] + public void KeystrokeNavigator_SetNull_DisablesKeystrokeNavigation () + { + ObservableCollection source = ["apricot", "arm", "bat", "batman", "bates hotel", "candle"]; + ListView lv = new () { Source = new ListWrapper (source) }; + + lv.SetFocus (); + + // Verify keystroke navigation works by default + Assert.NotNull (lv.KeystrokeNavigator); + Assert.True (lv.NewKeyDownEvent (Key.B)); + Assert.Equal (2, lv.SelectedItem); // "bat" + + // Disable keystroke navigation + lv.KeystrokeNavigator = null; + + // Reset selection + lv.SelectedItem = 0; + + // Typing should no longer navigate — key events are not consumed + Assert.False (lv.NewKeyDownEvent (Key.C)); + Assert.Equal (0, lv.SelectedItem); // unchanged + Assert.False (lv.NewKeyDownEvent (Key.A)); + Assert.Equal (0, lv.SelectedItem); // unchanged + } + + // Copilot + [Fact] + public void KeystrokeNavigator_ReassignAfterSource_SyncsCollection () + { + ObservableCollection source = ["apricot", "arm", "bat", "batman", "bates hotel", "candle"]; + ListView lv = new () { Source = new ListWrapper (source) }; + + lv.SetFocus (); + + // Disable then re-enable with a fresh navigator + lv.KeystrokeNavigator = null; + lv.KeystrokeNavigator = new CollectionNavigator (); + + // The new navigator should have been synced with Source automatically + Assert.True (lv.NewKeyDownEvent (Key.C)); + Assert.Equal (5, lv.SelectedItem); // "candle" + } + [Fact] public void ListViewCollectionNavigatorMatcher_OverrideMatching () { @@ -607,7 +652,7 @@ public void ListViewCollectionNavigatorMatcher_OverrideMatching () matchNone.Setup (m => m.IsMatch (It.IsAny (), It.IsAny ())) .Returns ((string s, object key) => s.StartsWith ('B') && key?.ToString () == "candle"); - lv.KeystrokeNavigator.Matcher = matchNone.Object; + lv.KeystrokeNavigator?.Matcher = matchNone.Object; // Keys are consumed during navigation Assert.True (lv.NewKeyDownEvent (Key.B));