diff --git a/Terminal.Gui/View/View.Mouse.cs b/Terminal.Gui/View/View.Mouse.cs index 837ecbfff..a8812a1f5 100644 --- a/Terminal.Gui/View/View.Mouse.cs +++ b/Terminal.Gui/View/View.Mouse.cs @@ -371,8 +371,8 @@ public partial class View // Mouse APIs // Post-conditions // Always invoke Select command on MouseClick - // By default, this will raise Select/OnSelect - Subclasses can override this via AddCommand (Command.Select ...). - args.Handled = InvokeCommand (Command.Select) == true; + // By default, this will raise Selected/OnSelected - Subclasses can override this via AddCommand (Command.Select ...). + args.Handled = InvokeCommand (Command.Select, null, new KeyBinding ([Command.Select], scope: KeyBindingScope.Focused, boundView: this, context: args.MouseEvent)) == true; return args.Handled; } @@ -399,7 +399,7 @@ public partial class View // Mouse APIs if (SetPressedHighlight (HighlightStyle.None)) { - // BUGBUG: If we retrun true here we never generate a moues click! + // BUGBUG: If we return true here we never generate a mouse click! return true; } diff --git a/Terminal.Gui/Views/Label.cs b/Terminal.Gui/Views/Label.cs index 2aebe0712..0294111ab 100644 --- a/Terminal.Gui/Views/Label.cs +++ b/Terminal.Gui/Views/Label.cs @@ -65,6 +65,11 @@ public class Label : View private bool? InvokeHotKeyOnNext (CommandContext context) { + if (CanFocus) + { + return SetFocus (); + } + int me = SuperView?.Subviews.IndexOf (this) ?? -1; if (me != -1 && me < SuperView?.Subviews.Count - 1) diff --git a/Terminal.Gui/Views/RadioGroup.cs b/Terminal.Gui/Views/RadioGroup.cs index e89fd8089..47673ad30 100644 --- a/Terminal.Gui/Views/RadioGroup.cs +++ b/Terminal.Gui/Views/RadioGroup.cs @@ -16,7 +16,96 @@ public class RadioGroup : View, IDesignable, IOrientation Width = Dim.Auto (DimAutoStyle.Content); Height = Dim.Auto (DimAutoStyle.Content); - // Things this view knows how to do + + // Select (Space key or mouse click) - The default implementation sets focus. RadioGroup does not. + AddCommand ( + Command.Select, + () => + { + bool cursorChanged = false; + if (SelectedItem == Cursor) + { + cursorChanged = MoveDownRight (); + if (!cursorChanged) + { + cursorChanged = MoveHome (); + } + } + + bool selectedItemChanged = false; + if (SelectedItem != Cursor) + { + selectedItemChanged = ChangeSelectedItem (Cursor); + } + + if (cursorChanged || selectedItemChanged) + { + if (RaiseSelected () == true) + { + return true; + } + } + + return cursorChanged || selectedItemChanged; + }); + + // Accept (Enter key) - Raise Accept event - DO NOT advance state + AddCommand (Command.Accept, () => RaiseAccepted()); + + // Hotkey - ctx may indicate a radio item hotkey was pressed. Beahvior depends on HasFocus + // If HasFocus and it's this.HotKey invoke Select command - DO NOT raise Accept + // If it's a radio item HotKey select that item and raise Seelcted event - DO NOT raise Accept + // If nothing is selected, select first and raise Selected event - DO NOT raise Accept + AddCommand (Command.HotKey, + ctx => + { + var item = ctx.KeyBinding?.Context as int?; + + if (HasFocus) + { + if (ctx is { KeyBinding: { } } && (ctx.KeyBinding.Value.BoundView != this || HotKey == ctx.Key?.NoAlt.NoCtrl.NoShift)) + { + // It's this.HotKey OR Another View (Label?) forwarded the hotkey command to us - Act just like `Space` (Select) + return InvokeCommand (Command.Select, ctx.Key, ctx.KeyBinding); + } + } + + if (item is { } && item < _radioLabels.Count) + { + if (item.Value == SelectedItem) + { + return true; + } + + // If a RadioItem.HotKey is pressed we always set the selected item - never SetFocus + bool selectedItemChanged = ChangeSelectedItem (item.Value); + + if (selectedItemChanged) + { + // Doesn't matter if it's handled + RaiseSelected (); + return true; + } + + + return false; + } + + if (SelectedItem == -1 && ChangeSelectedItem (0)) + { + if (RaiseSelected () == true) + { + return true; + } + return false; + } + + // Default Command.Hotkey sets focus + SetFocus (); + + return true; + }); + AddCommand ( Command.Up, () => @@ -73,84 +162,6 @@ public class RadioGroup : View, IDesignable, IOrientation } ); - // Select (Space key or mouse click) - The default implementation sets focus. RadioGroup does not. - AddCommand ( - Command.Select, - () => - { - bool cursorChanged = false; - if (SelectedItem == Cursor) - { - cursorChanged = MoveDownRight (); - if (!cursorChanged) - { - cursorChanged = MoveHome (); - } - } - - if (SelectedItem != Cursor) - { - if (ChangeSelectedItem (Cursor) && RaiseSelected () == true) - { - return true; - } - } - - return cursorChanged; - }); - - // Accept (Enter key) - Raise Accept event - DO NOT advance state - AddCommand (Command.Accept, RaiseAccepted); - - // Hotkey - ctx may indicate a radio item hotkey was pressed. Beahvior depends on HasFocus - // If HasFocus and it's this.HotKey invoke Select command - DO NOT raise Accept - // If it's a radio item HotKey select that item and raise Seelcted event - DO NOT raise Accept - // If nothing is selected, select first and raise Selected event - DO NOT raise Accept - AddCommand (Command.HotKey, - ctx => - { - var item = ctx.KeyBinding?.Context as int?; - - if (HasFocus) - { - if (ctx is { KeyBinding: { } } && (ctx.KeyBinding.Value.BoundView != this || HotKey == ctx.Key?.NoAlt.NoCtrl.NoShift)) - { - // It's this.HotKey OR Another View (Label?) forwarded the hotkey command to us - Act just like `Space` (Select) - return InvokeCommand (Command.Select, ctx.Key, ctx.KeyBinding); - } - } - - if (item is { } && item < _radioLabels.Count) - { - if (item.Value == SelectedItem) - { - return true; - } - - // If a RadioItem.HotKey is pressed we always set the selected item - never SetFocus - if (ChangeSelectedItem (item.Value) && RaiseSelected () == true) - { - return true; - } - - return false; - } - - if (SelectedItem == -1 && ChangeSelectedItem (0)) - { - if (RaiseSelected () == true) - { - return true; - } - return false; - } - - // Default Command.Hotkey sets focus - SetFocus (); - - return true; - }); - // ReSharper disable once UseObjectOrCollectionInitializer _orientationHelper = new (this); _orientationHelper.Orientation = Orientation.Vertical; @@ -225,30 +236,25 @@ public class RadioGroup : View, IDesignable, IOrientation if (c > -1) { - if (ChangeSelectedItem (c)) - { - Cursor = c; - e.Handled = true; - } + // Just like the user pressing the items' hotkey + e.Handled = InvokeCommand (Command.HotKey, null, new KeyBinding ([Command.HotKey], KeyBindingScope.HotKey, boundView: this, context: c)) == true; } } + + return; } if (DoubleClickAccepts && e.MouseEvent.Flags.HasFlag (MouseFlags.Button1DoubleClicked)) { - int savedSelectedItem = SelectedItem; + // NOTE: Drivers ALWAYS generate a Button1Clicked event before Button1DoubleClicked + // NOTE: So, we've already selected an item. - if (RaiseAccepted () == true) - { - e.Handled = false; - _selected = savedSelectedItem; - } - - if (SuperView?.InvokeCommand (Command.Accept) is false or null) - { - e.Handled = true; - } + // Just like the user pressing `Enter` + InvokeCommand (Command.Accept); } + + // HACK: Always eat so Select is not invoked by base + e.Handled = true; } private List<(int pos, int length)>? _horizontal; diff --git a/Terminal.Gui/Views/Shortcut.cs b/Terminal.Gui/Views/Shortcut.cs index 533427529..6324d84b7 100644 --- a/Terminal.Gui/Views/Shortcut.cs +++ b/Terminal.Gui/Views/Shortcut.cs @@ -386,16 +386,6 @@ public class Shortcut : View, IOrientation, IDesignable } } - private bool? OnSelect (CommandContext ctx) - { - if (CommandView.GetSupportedCommands ().Contains (Command.Select)) - { - return CommandView.InvokeCommand (Command.Select, ctx.Key, ctx.KeyBinding); - } - - return false; - } - private void Shortcut_MouseClick (object sender, MouseEventEventArgs e) { // When the Shortcut is clicked, we want to invoke the Command and Set focus diff --git a/Terminal.Gui/Views/Slider.cs b/Terminal.Gui/Views/Slider.cs index f0b0ce4c5..a5d2c9acc 100644 --- a/Terminal.Gui/Views/Slider.cs +++ b/Terminal.Gui/Views/Slider.cs @@ -1496,8 +1496,13 @@ public class Slider : View, IOrientation OnOptionsChanged (); } - private void SetFocusedOption () + private bool SetFocusedOption () { + if (_options.Count == 0) + { + return false; + } + bool changed = false; switch (_config._type) { case SliderType.Single: @@ -1530,6 +1535,7 @@ public class Slider : View, IOrientation // Raise slider changed event. OnOptionsChanged (); + changed = true; break; case SliderType.Multiple: @@ -1550,6 +1556,7 @@ public class Slider : View, IOrientation } OnOptionsChanged (); + changed = true; break; @@ -1683,11 +1690,14 @@ public class Slider : View, IOrientation // Raise Slider Option Changed Event. OnOptionsChanged (); + changed = true; break; default: throw new ArgumentOutOfRangeException (_config._type.ToString ()); } + + return changed; } internal bool ExtendPlus () @@ -1772,9 +1782,7 @@ public class Slider : View, IOrientation internal bool Select () { - SetFocusedOption (); - - return true; + return SetFocusedOption (); } internal new bool Accept () diff --git a/Terminal.Gui/Views/TableView/TableView.cs b/Terminal.Gui/Views/TableView/TableView.cs index 8b49c2f22..f330be744 100644 --- a/Terminal.Gui/Views/TableView/TableView.cs +++ b/Terminal.Gui/Views/TableView/TableView.cs @@ -238,24 +238,18 @@ public class TableView : View } ); - AddCommand ( - Command.Accept, - () => - { - // BUGBUG: This should return false if the event is not handled - OnCellActivated (new CellActivatedEventArgs (Table, SelectedColumn, SelectedRow)); - - return true; - } - ); + AddCommand (Command.Accept, () => OnCellActivated (new CellActivatedEventArgs (Table, SelectedColumn, SelectedRow))); AddCommand ( Command.Select, // was Command.ToggleChecked () => { - ToggleCurrentCellSelection (); + if (ToggleCurrentCellSelection () is true) + { + return RaiseSelected () is true; + } - return true; + return false; } ); @@ -1250,7 +1244,12 @@ public class TableView : View /// Invokes the event /// - protected virtual void OnCellActivated (CellActivatedEventArgs args) { CellActivated?.Invoke (this, args); } + /// if the CellActivated event was raised. + protected virtual bool OnCellActivated (CellActivatedEventArgs args) + { + CellActivated?.Invoke (this, args); + return CellActivated is { }; + } /// Invokes the event /// @@ -2047,19 +2046,19 @@ public class TableView : View ); } - private void ToggleCurrentCellSelection () + private bool? ToggleCurrentCellSelection () { var e = new CellToggledEventArgs (Table, selectedColumn, selectedRow); OnCellToggled (e); if (e.Cancel) { - return; + return false; } if (!MultiSelect) { - return; + return null; } TableSelection [] regions = GetMultiSelectedRegionsContaining (selectedColumn, selectedRow).ToArray (); @@ -2104,6 +2103,8 @@ public class TableView : View ); } } + + return true; } /// diff --git a/Terminal.Gui/Views/TextView.cs b/Terminal.Gui/Views/TextView.cs index 4b54ccaf5..346f5585c 100644 --- a/Terminal.Gui/Views/TextView.cs +++ b/Terminal.Gui/Views/TextView.cs @@ -2010,6 +2010,10 @@ public class TextView : View LayoutComplete += TextView_LayoutComplete; // Things this view knows how to do + + // Note - NewLine is only bound to Enter if Multiline is true + AddCommand (Command.NewLine, () => ProcessEnterKey ()); + AddCommand ( Command.PageDown, () => @@ -2276,8 +2280,6 @@ public class TextView : View } ); - AddCommand (Command.Accept, () => ProcessEnterKey ()); - AddCommand ( Command.End, () => @@ -2404,7 +2406,9 @@ public class TextView : View } ); - // Default keybindings for this view + KeyBindings.Remove (Key.Enter); + KeyBindings.Add (Key.Enter, Multiline ? Command.NewLine : Command.Accept); + KeyBindings.Add (Key.PageDown, Command.PageDown); KeyBindings.Add (Key.V.WithCtrl, Command.PageDown); @@ -2701,6 +2705,9 @@ public class TextView : View Height = _savedHeight; SetNeedsDisplay (); } + + KeyBindings.Remove (Key.Enter); + KeyBindings.Add (Key.Enter, Multiline ? Command.NewLine : Command.Accept); } } diff --git a/UICatalog/Scenarios/FileDialogExamples.cs b/UICatalog/Scenarios/FileDialogExamples.cs index 069013b42..484799b36 100644 --- a/UICatalog/Scenarios/FileDialogExamples.cs +++ b/UICatalog/Scenarios/FileDialogExamples.cs @@ -2,6 +2,7 @@ using System.IO; using System.IO.Abstractions; using System.Linq; +using System.Text; using Terminal.Gui; namespace UICatalog.Scenarios; @@ -33,25 +34,25 @@ public class FileDialogExamples : Scenario var x = 1; var win = new Window { Title = GetQuitKeyAndName () }; - _cbMustExist = new CheckBox { CheckedState = CheckState.Checked, Y = y++, X = x, Text = "Must Exist" }; + _cbMustExist = new CheckBox { CheckedState = CheckState.Checked, Y = y++, X = x, Text = "Must E_xist" }; win.Add (_cbMustExist); - _cbUseColors = new CheckBox { CheckedState = FileDialogStyle.DefaultUseColors ? CheckState.Checked : CheckState.UnChecked, Y = y++, X = x, Text = "Use Colors" }; + _cbUseColors = new CheckBox { CheckedState = FileDialogStyle.DefaultUseColors ? CheckState.Checked : CheckState.UnChecked, Y = y++, X = x, Text = "_Use Colors" }; win.Add (_cbUseColors); - _cbCaseSensitive = new CheckBox { CheckedState = CheckState.UnChecked, Y = y++, X = x, Text = "Case Sensitive Search" }; + _cbCaseSensitive = new CheckBox { CheckedState = CheckState.UnChecked, Y = y++, X = x, Text = "_Case Sensitive Search" }; win.Add (_cbCaseSensitive); - _cbAllowMultipleSelection = new CheckBox { CheckedState = CheckState.UnChecked, Y = y++, X = x, Text = "Multiple" }; + _cbAllowMultipleSelection = new CheckBox { CheckedState = CheckState.UnChecked, Y = y++, X = x, Text = "_Multiple" }; win.Add (_cbAllowMultipleSelection); - _cbShowTreeBranchLines = new CheckBox { CheckedState = CheckState.Checked, Y = y++, X = x, Text = "Tree Branch Lines" }; + _cbShowTreeBranchLines = new CheckBox { CheckedState = CheckState.Checked, Y = y++, X = x, Text = "Tree Branch _Lines" }; win.Add (_cbShowTreeBranchLines); - _cbAlwaysTableShowHeaders = new CheckBox { CheckedState = CheckState.Checked, Y = y++, X = x, Text = "Always Show Headers" }; + _cbAlwaysTableShowHeaders = new CheckBox { CheckedState = CheckState.Checked, Y = y++, X = x, Text = "Always Show _Headers" }; win.Add (_cbAlwaysTableShowHeaders); - _cbDrivesOnlyInTree = new CheckBox { CheckedState = CheckState.UnChecked, Y = y++, X = x, Text = "Only Show Drives" }; + _cbDrivesOnlyInTree = new CheckBox { CheckedState = CheckState.UnChecked, Y = y++, X = x, Text = "Only Show _Drives" }; win.Add (_cbDrivesOnlyInTree); y = 0; @@ -63,7 +64,7 @@ public class FileDialogExamples : Scenario win.Add (new Label { X = x++, Y = y++, Text = "Caption" }); _rgCaption = new RadioGroup { X = x, Y = y }; - _rgCaption.RadioLabels = new [] { "Ok", "Open", "Save" }; + _rgCaption.RadioLabels = new [] { "_Ok", "O_pen", "_Save" }; win.Add (_rgCaption); y = 0; @@ -75,7 +76,7 @@ public class FileDialogExamples : Scenario win.Add (new Label { X = x++, Y = y++, Text = "OpenMode" }); _rgOpenMode = new RadioGroup { X = x, Y = y }; - _rgOpenMode.RadioLabels = new [] { "File", "Directory", "Mixed" }; + _rgOpenMode.RadioLabels = new [] { "_File", "D_irectory", "_Mixed" }; win.Add (_rgOpenMode); y = 0; @@ -87,7 +88,7 @@ public class FileDialogExamples : Scenario win.Add (new Label { X = x++, Y = y++, Text = "Icons" }); _rgIcons = new RadioGroup { X = x, Y = y }; - _rgIcons.RadioLabels = new [] { "None", "Unicode", "Nerd*" }; + _rgIcons.RadioLabels = new [] { "_None", "_Unicode", "Nerd_*" }; win.Add (_rgIcons); win.Add (new Label { Y = Pos.AnchorEnd (2), Text = "* Requires installing Nerd fonts" }); @@ -102,7 +103,7 @@ public class FileDialogExamples : Scenario win.Add (new Label { X = x++, Y = y++, Text = "Allowed" }); _rgAllowedTypes = new RadioGroup { X = x, Y = y }; - _rgAllowedTypes.RadioLabels = new [] { "Any", "Csv (Recommended)", "Csv (Strict)" }; + _rgAllowedTypes.RadioLabels = new [] { "An_y", "Cs_v (Recommended)", "Csv (S_trict)" }; win.Add (_rgAllowedTypes); y = 5; @@ -113,18 +114,32 @@ public class FileDialogExamples : Scenario ); win.Add (new Label { X = x++, Y = y++, Text = "Buttons" }); - win.Add (new Label { X = x, Y = y++, Text = "Ok Text:" }); + win.Add (new Label { X = x, Y = y++, Text = "O_k Text:" }); _tbOkButton = new TextField { X = x, Y = y++, Width = 12 }; win.Add (_tbOkButton); - win.Add (new Label { X = x, Y = y++, Text = "Cancel Text:" }); + win.Add (new Label { X = x, Y = y++, Text = "_Cancel Text:" }); _tbCancelButton = new TextField { X = x, Y = y++, Width = 12 }; win.Add (_tbCancelButton); - _cbFlipButtonOrder = new CheckBox { X = x, Y = y++, Text = "Flip Order" }; + _cbFlipButtonOrder = new CheckBox { X = x, Y = y++, Text = "Flip Ord_er" }; win.Add (_cbFlipButtonOrder); - var btn = new Button { X = 1, Y = 9, Text = "Run Dialog" }; + var btn = new Button { X = 1, Y = 9, IsDefault = true, Text = "Run Dialog" }; - SetupHandler (btn); + win.Accepted += (s, e) => + { + try + { + CreateDialog (); + } + catch (Exception ex) + { + MessageBox.ErrorQuery ("Error", ex.ToString (), "_Ok"); + } + finally + { + e.Handled = true; + } + }; win.Add (btn); Application.Run (win); @@ -138,7 +153,7 @@ public class FileDialogExamples : Scenario { if (File.Exists (e.Dialog.Path)) { - int result = MessageBox.Query ("Overwrite?", "File already exists", "Yes", "No"); + int result = MessageBox.Query ("Overwrite?", "File already exists", "_Yes", "_No"); e.Cancel = result == 1; } } @@ -149,13 +164,13 @@ public class FileDialogExamples : Scenario var fd = new FileDialog { OpenMode = Enum.Parse ( - _rgOpenMode.RadioLabels [_rgOpenMode.SelectedItem] + _rgOpenMode.RadioLabels.Select (l => TextFormatter.RemoveHotKeySpecifier(l, 0, _rgOpenMode.HotKeySpecifier)).ToArray() [_rgOpenMode.SelectedItem] ), MustExist = _cbMustExist.CheckedState == CheckState.Checked, AllowsMultipleSelection = _cbAllowMultipleSelection.CheckedState == CheckState.Checked }; - fd.Style.OkButtonText = _rgCaption.RadioLabels [_rgCaption.SelectedItem]; + fd.Style.OkButtonText = _rgCaption.RadioLabels.Select (l => TextFormatter.RemoveHotKeySpecifier(l, 0, _rgCaption.HotKeySpecifier)).ToArray() [_rgCaption.SelectedItem]; // If Save style dialog then give them an overwrite prompt if (_rgCaption.SelectedItem == 2) @@ -224,6 +239,7 @@ public class FileDialogExamples : Scenario "You canceled navigation and did not pick anything", "Ok" ); + } else if (_cbAllowMultipleSelection.CheckedState == CheckState.Checked) { @@ -243,21 +259,6 @@ public class FileDialogExamples : Scenario } } - private void SetupHandler (Button btn) - { - btn.Accepted += (s, e) => - { - try - { - CreateDialog (); - } - catch (Exception ex) - { - MessageBox.ErrorQuery ("Error", ex.ToString (), "Ok"); - } - }; - } - private class CaseSensitiveSearchMatcher : ISearchMatcher { private string _terms; diff --git a/UnitTests/Views/AllViewsTests.cs b/UnitTests/Views/AllViewsTests.cs index e32e126d1..e55b5eade 100644 --- a/UnitTests/Views/AllViewsTests.cs +++ b/UnitTests/Views/AllViewsTests.cs @@ -24,6 +24,11 @@ public class AllViewsTests (ITestOutputHelper output) : TestsAllViews return; } + if (view is IDesignable designable) + { + designable.EnableForDesign (); + } + view.X = Pos.Center (); view.Y = Pos.Center (); @@ -61,17 +66,6 @@ public class AllViewsTests (ITestOutputHelper output) : TestsAllViews Assert.True (Test_All_Constructors_Of_Type (viewType)); } - //[Fact] - //public void AllViews_HotKey_Works () - //{ - // foreach (var type in GetAllViewClasses ()) { - // _output.WriteLine ($"Testing {type.Name}"); - // var view = GetTypeInitializer (type, type.GetConstructor (Array.Empty ())); - // view.HotKeySpecifier = (Rune)'^'; - // view.Text = "^text"; - // Assert.Equal(Key.T, view.HotKey); - // } - //} public bool Test_All_Constructors_Of_Type (Type type) { @@ -87,4 +81,86 @@ public class AllViewsTests (ITestOutputHelper output) : TestsAllViews return true; } + + //[Fact] + //public void AllViews_HotKey_Works () + //{ + // foreach (var type in GetAllViewClasses ()) { + // _output.WriteLine ($"Testing {type.Name}"); + // var view = GetTypeInitializer (type, type.GetConstructor (Array.Empty ())); + // view.HotKeySpecifier = (Rune)'^'; + // view.Text = "^text"; + // Assert.Equal(Key.T, view.HotKey); + // } + //} + + [Theory] + [MemberData (nameof (AllViewTypes))] + public void AllViews_Command_Select_Raises_Selected (Type viewType) + { + var view = (View)CreateInstanceIfNotGeneric (viewType); + + if (view == null) + { + output.WriteLine ($"Ignoring {viewType} - It's a Generic"); + + return; + } + + if (view is IDesignable designable) + { + designable.EnableForDesign (); + } + + var selectedCount = 0; + view.Selected += (s, e) => selectedCount++; + + var acceptedCount = 0; + view.Accepted += (s, e) => + { + acceptedCount++; + }; + + + if (view.InvokeCommand(Command.Select) == true) + { + Assert.Equal(1, selectedCount); + Assert.Equal (0, acceptedCount); + } + } + + [Theory] + [MemberData (nameof (AllViewTypes))] + public void AllViews_Command_Accept_Raises_Accepted (Type viewType) + { + var view = (View)CreateInstanceIfNotGeneric (viewType); + + if (view == null) + { + output.WriteLine ($"Ignoring {viewType} - It's a Generic"); + + return; + } + + if (view is IDesignable designable) + { + designable.EnableForDesign (); + } + + var selectedCount = 0; + view.Selected += (s, e) => selectedCount++; + + var acceptedCount = 0; + view.Accepted += (s, e) => + { + acceptedCount++; + }; + + + if (view.InvokeCommand (Command.Accept) == true) + { + Assert.Equal (0, selectedCount); + Assert.Equal (1, acceptedCount); + } + } } diff --git a/UnitTests/Views/LabelTests.cs b/UnitTests/Views/LabelTests.cs index 02f18f447..78ddb8f2d 100644 --- a/UnitTests/Views/LabelTests.cs +++ b/UnitTests/Views/LabelTests.cs @@ -1316,10 +1316,56 @@ e super.Dispose (); } + [Fact] - public void Label_CanFocus_True_Get_Focus_By_Keyboard () + public void CanFocus_False_HotKey_SetsFocus_Next () { - Label label = new () { Text = "label" }; + View otherView = new () { Text = "otherView", CanFocus = true }; + Label label = new () { Text = "_label" }; + View nextView = new () { Text = "nextView", CanFocus = true }; + Application.Navigation = new (); + Application.Top = new (); + Application.Top.Add (otherView, label, nextView); + + Application.Top.SetFocus (); + Assert.True (otherView.HasFocus); + + // No focused view accepts Tab, and there's no other view to focus, so OnKeyDown returns false + Assert.True (Application.OnKeyDown (label.HotKey)); + Assert.False (otherView.HasFocus); + Assert.False (label.HasFocus); + Assert.True (nextView.HasFocus); + + Application.Top.Dispose (); + Application.ResetState (); + } + + + [Fact] + public void CanFocus_False_MouseClick_SetsFocus_Next () + { + View otherView = new () { X = 0, Y = 0, Width = 1, Height = 1, Id = "otherView", CanFocus = true }; + Label label = new () { X = 0, Y = 1, Text = "_label" }; + View nextView = new () { X = Pos.Right (label), Y = Pos.Top (label), Width = 1, Height = 1, Id = "nextView", CanFocus = true }; + Application.Navigation = new (); + Application.Top = new (); + Application.Top.Add (otherView, label, nextView); + + Application.Top.SetFocus (); + + // click on label + Application.OnMouseEvent (new () { ScreenPosition = label.Frame.Location, Flags = MouseFlags.Button1Clicked }); + Assert.False (label.HasFocus); + Assert.True (nextView.HasFocus); + + Application.Top.Dispose (); + Application.ResetState (); + } + + [Fact] + public void CanFocus_True_HotKey_SetsFocus () + { + Label label = new () { Text = "_label" }; View view = new () { Text = "view", CanFocus = true }; Application.Navigation = new (); Application.Top = new (); @@ -1333,21 +1379,7 @@ e Assert.True (view.HasFocus); // No focused view accepts Tab, and there's no other view to focus, so OnKeyDown returns false - Assert.False (Application.OnKeyDown (Key.Tab)); - Assert.False (label.HasFocus); - Assert.True (view.HasFocus); - - // Set label CanFocus to true - label.CanFocus = true; - Assert.False (label.HasFocus); - Assert.True (view.HasFocus); - - // No focused view accepts Tab, but label can now be focused, so focus should move to it. - Assert.True (Application.OnKeyDown (Key.Tab)); - Assert.True (label.HasFocus); - Assert.False (view.HasFocus); - - Assert.True (Application.OnKeyDown (Key.Tab)); + Assert.True (Application.OnKeyDown (label.HotKey)); Assert.False (label.HasFocus); Assert.True (view.HasFocus); @@ -1357,15 +1389,17 @@ e [Fact] - public void Label_CanFocus_True_Get_Focus_By_Mouse () + public void CanFocus_True_MouseClick_Focuses () { + Application.Navigation = new (); Label label = new () { Text = "label", X = 0, - Y = 0 + Y = 0, + CanFocus = true }; - View view = new () + View otherView = new () { Text = "view", X = 0, @@ -1379,34 +1413,26 @@ e Width = 10, Height = 10 }; - Application.Top.Add (label, view); - + Application.Top.Add (label, otherView); Application.Top.SetFocus (); - Assert.Equal (view, Application.Top.MostFocused); - Assert.False (label.CanFocus); - Assert.False (label.HasFocus); - Assert.True (view.CanFocus); - Assert.True (view.HasFocus); - // label can't focus so clicking on it has no effect - Application.OnMouseEvent (new () { Position = new (0, 0), Flags = MouseFlags.Button1Clicked }); - Assert.False (label.HasFocus); - Assert.True (view.HasFocus); + Assert.True (label.CanFocus); + Assert.True (label.HasFocus); + Assert.True (otherView.CanFocus); + Assert.False (otherView.HasFocus); - // Set label CanFocus to true - label.CanFocus = true; - Assert.False (label.HasFocus); - Assert.True (view.HasFocus); + otherView.SetFocus (); + Assert.True (otherView.HasFocus); // label can focus, so clicking on it set focus - Application.OnMouseEvent (new () { Position = new (0, 0), Flags = MouseFlags.Button1Clicked }); + Application.OnMouseEvent (new () { ScreenPosition = new (0, 0), Flags = MouseFlags.Button1Clicked }); Assert.True (label.HasFocus); - Assert.False (view.HasFocus); + Assert.False (otherView.HasFocus); // click on view - Application.OnMouseEvent (new () { Position = new (0, 1), Flags = MouseFlags.Button1Clicked }); + Application.OnMouseEvent (new () { ScreenPosition = new (0, 1), Flags = MouseFlags.Button1Clicked }); Assert.False (label.HasFocus); - Assert.True (view.HasFocus); + Assert.True (otherView.HasFocus); Application.Top.Dispose (); Application.ResetState (); diff --git a/UnitTests/Views/RadioGroupTests.cs b/UnitTests/Views/RadioGroupTests.cs index 79a0e5a9b..bbf4aaa1a 100644 --- a/UnitTests/Views/RadioGroupTests.cs +++ b/UnitTests/Views/RadioGroupTests.cs @@ -90,17 +90,17 @@ public class RadioGroupTests (ITestOutputHelper output) var selectedItemChangedCount = 0; rg.SelectedItemChanged += (s, e) => selectedItemChangedCount++; - var selectCount = 0; - rg.Selected += (s, e) => selectCount++; + var selectedCount = 0; + rg.Selected += (s, e) => selectedCount++; - var acceptCount = 0; - rg.Accepted += (s, e) => acceptCount++; + var acceptedCount = 0; + rg.Accepted += (s, e) => acceptedCount++; // By default the first item is selected Assert.Equal (0, rg.SelectedItem); Assert.Equal (0, selectedItemChangedCount); - Assert.Equal (0, selectCount); - Assert.Equal (0, acceptCount); + Assert.Equal (0, selectedCount); + Assert.Equal (0, acceptedCount); Assert.Equal (Key.Empty, rg.HotKey); // With HasFocus @@ -109,38 +109,38 @@ public class RadioGroupTests (ITestOutputHelper output) Assert.Equal (0, rg.SelectedItem); Assert.Equal (0, rg.Cursor); Assert.Equal (0, selectedItemChangedCount); - Assert.Equal (0, selectCount); - Assert.Equal (0, acceptCount); + Assert.Equal (0, selectedCount); + Assert.Equal (0, acceptedCount); Assert.True (Application.OnKeyDown (Key.CursorDown)); Assert.Equal (0, rg.SelectedItem); // Cursor changed, but selection didnt Assert.Equal (1, rg.Cursor); Assert.Equal (0, selectedItemChangedCount); - Assert.Equal (0, selectCount); - Assert.Equal (0, acceptCount); + Assert.Equal (0, selectedCount); + Assert.Equal (0, acceptedCount); Assert.False (Application.OnKeyDown (Key.CursorDown)); // Should not change selection (should focus next view if there was one, which there isn't) Assert.Equal (0, rg.SelectedItem); Assert.Equal (1, rg.Cursor); Assert.Equal (0, selectedItemChangedCount); - Assert.Equal (0, selectCount); - Assert.Equal (0, acceptCount); + Assert.Equal (0, selectedCount); + Assert.Equal (0, acceptedCount); - // Test Select (Space) when Cursor != SelectedItem + // Test Select (Space) when Cursor != SelectedItem - Should select cursor Assert.True (Application.OnKeyDown (Key.Space)); Assert.Equal (1, rg.SelectedItem); Assert.Equal (1, rg.Cursor); Assert.Equal (1, selectedItemChangedCount); - Assert.Equal (1, selectCount); - Assert.Equal (0, acceptCount); + Assert.Equal (1, selectedCount); + Assert.Equal (0, acceptedCount); - // Now test Select (Space) when Cursor == SelectedItem - Should cycle + // Test Select (Space) when Cursor == SelectedItem - Should cycle Assert.True (Application.OnKeyDown (Key.Space)); Assert.Equal (0, rg.SelectedItem); Assert.Equal (0, rg.Cursor); Assert.Equal (2, selectedItemChangedCount); - Assert.Equal (2, selectCount); - Assert.Equal (0, acceptCount); + Assert.Equal (2, selectedCount); + Assert.Equal (0, acceptedCount); Assert.True (Application.OnKeyDown (Key.Space)); Assert.Equal (1, rg.SelectedItem); @@ -166,8 +166,8 @@ public class RadioGroupTests (ITestOutputHelper output) Assert.Equal (1, rg.SelectedItem); Assert.Equal (1, rg.Cursor); Assert.Equal (7, selectedItemChangedCount); - Assert.Equal (7, selectCount); - Assert.Equal (0, acceptCount); + Assert.Equal (7, selectedCount); + Assert.Equal (0, acceptedCount); // Test HotKey // Selected == Cursor (1) - Advance state and raise Select event - DO NOT raise Accept @@ -178,8 +178,8 @@ public class RadioGroupTests (ITestOutputHelper output) Assert.Equal (0, rg.SelectedItem); Assert.Equal (0, rg.Cursor); Assert.Equal (8, selectedItemChangedCount); - Assert.Equal (8, selectCount); - Assert.Equal (0, acceptCount); + Assert.Equal (8, selectedCount); + Assert.Equal (0, acceptedCount); // Make Selected != Cursor Assert.True (Application.OnKeyDown (Key.CursorDown)); @@ -191,8 +191,8 @@ public class RadioGroupTests (ITestOutputHelper output) Assert.Equal (1, rg.SelectedItem); Assert.Equal (1, rg.Cursor); Assert.Equal (9, selectedItemChangedCount); - Assert.Equal (9, selectCount); - Assert.Equal (0, acceptCount); + Assert.Equal (9, selectedCount); + Assert.Equal (0, acceptedCount); Application.ResetState (true); } @@ -372,7 +372,6 @@ public class RadioGroupTests (ITestOutputHelper output) Assert.NotEmpty (rg.KeyBindings.GetCommands (KeyCode.L | KeyCode.ShiftMask)); Assert.NotEmpty (rg.KeyBindings.GetCommands (KeyCode.L | KeyCode.AltMask)); - // BUGBUG: These tests only test that RG works on it's own, not if it's a subview Assert.True (Application.OnKeyDown (Key.T)); Assert.Equal (2, rg.SelectedItem); Assert.True (Application.OnKeyDown (Key.L)); @@ -463,7 +462,7 @@ public class RadioGroupTests (ITestOutputHelper output) group.NewKeyDownEvent (Key.G.WithAlt); Assert.Equal (0, group.SelectedItem); - Assert.True (group.HasFocus); + Assert.False (group.HasFocus); } [Fact] @@ -619,18 +618,18 @@ public class RadioGroupTests (ITestOutputHelper output) { var radioGroup = new RadioGroup { - RadioLabels = ["_1", "__2"] + RadioLabels = ["_1", "_2"] }; Assert.True (radioGroup.CanFocus); var selectedItemChanged = 0; radioGroup.SelectedItemChanged += (s, e) => selectedItemChanged++; - var selectCount = 0; - radioGroup.Selected += (s, e) => selectCount++; + var selectedCount = 0; + radioGroup.Selected += (s, e) => selectedCount++; - var acceptCount = 0; - radioGroup.Accepted += (s, e) => acceptCount++; + var acceptedCount = 0; + radioGroup.Accepted += (s, e) => acceptedCount++; Assert.Equal (Orientation.Vertical, radioGroup.Orientation); @@ -638,26 +637,29 @@ public class RadioGroupTests (ITestOutputHelper output) Assert.True (radioGroup.HasFocus); Assert.Equal (0, radioGroup.SelectedItem); Assert.Equal (0, selectedItemChanged); - Assert.Equal (0, selectCount); - Assert.Equal (0, acceptCount); + Assert.Equal (0, selectedCount); + Assert.Equal (0, acceptedCount); - Assert.False (radioGroup.NewMouseEvent (new () { Position = new (0, 0), Flags = MouseFlags.Button1Clicked })); + // Click on the first item, which is already selected + Assert.True (radioGroup.NewMouseEvent (new () { Position = new (0, 0), Flags = MouseFlags.Button1Clicked })); Assert.Equal (0, radioGroup.SelectedItem); Assert.Equal (0, selectedItemChanged); - Assert.Equal (0, selectCount); - Assert.Equal (0, acceptCount); + Assert.Equal (0, selectedCount); + Assert.Equal (0, acceptedCount); + // Click on the second item Assert.True (radioGroup.NewMouseEvent (new () { Position = new (0, 1), Flags = MouseFlags.Button1Clicked })); Assert.Equal (1, radioGroup.SelectedItem); Assert.Equal (1, selectedItemChanged); - Assert.Equal (1, selectCount); - Assert.Equal (0, acceptCount); + Assert.Equal (1, selectedCount); + Assert.Equal (0, acceptedCount); + // Click on the first item Assert.True (radioGroup.NewMouseEvent (new () { Position = new (0, 0), Flags = MouseFlags.Button1Clicked })); Assert.Equal (0, radioGroup.SelectedItem); Assert.Equal (2, selectedItemChanged); - Assert.Equal (2, selectCount); - Assert.Equal (0, acceptCount); + Assert.Equal (2, selectedCount); + Assert.Equal (0, acceptedCount); } [Fact] @@ -673,16 +675,16 @@ public class RadioGroupTests (ITestOutputHelper output) var selectedItemChanged = 0; radioGroup.SelectedItemChanged += (s, e) => selectedItemChanged++; - var selectCount = 0; - radioGroup.Selected += (s, e) => selectCount++; + var selectedCount = 0; + radioGroup.Selected += (s, e) => selectedCount++; - var acceptCount = 0; - var handleAccept = false; + var acceptedCount = 0; + var handleAccepted = false; radioGroup.Accepted += (s, e) => { - acceptCount++; - e.Handled = handleAccept; + acceptedCount++; + e.Handled = handleAccepted; }; Assert.True (radioGroup.DoubleClickAccepts); @@ -692,25 +694,32 @@ public class RadioGroupTests (ITestOutputHelper output) Assert.True (radioGroup.HasFocus); Assert.Equal (0, radioGroup.SelectedItem); Assert.Equal (0, selectedItemChanged); - Assert.Equal (0, selectCount); - Assert.Equal (0, acceptCount); + Assert.Equal (0, selectedCount); + Assert.Equal (0, acceptedCount); + // NOTE: Drivers ALWAYS generate a Button1Clicked event before Button1DoubleClicked + // NOTE: We need to do the same + + Assert.True (radioGroup.NewMouseEvent (new () { Position = new (0, 0), Flags = MouseFlags.Button1Clicked })); Assert.True (radioGroup.NewMouseEvent (new () { Position = new (0, 0), Flags = MouseFlags.Button1DoubleClicked })); Assert.Equal (0, radioGroup.SelectedItem); Assert.Equal (0, selectedItemChanged); - Assert.Equal (0, selectCount); - Assert.Equal (1, acceptCount); + Assert.Equal (0, selectedCount); + Assert.Equal (1, acceptedCount); + Assert.True (radioGroup.NewMouseEvent (new () { Position = new (0, 1), Flags = MouseFlags.Button1Clicked })); Assert.True (radioGroup.NewMouseEvent (new () { Position = new (0, 1), Flags = MouseFlags.Button1Clicked })); Assert.Equal (1, radioGroup.SelectedItem); Assert.Equal (1, selectedItemChanged); - Assert.Equal (1, selectCount); - Assert.Equal (1, acceptCount); + Assert.Equal (1, selectedCount); + Assert.Equal (1, acceptedCount); + + Assert.True (radioGroup.NewMouseEvent (new () { Position = new (0, 1), Flags = MouseFlags.Button1Clicked })); Assert.True (radioGroup.NewMouseEvent (new () { Position = new (0, 1), Flags = MouseFlags.Button1DoubleClicked })); Assert.Equal (1, radioGroup.SelectedItem); Assert.Equal (1, selectedItemChanged); - Assert.Equal (1, selectCount); - Assert.Equal (2, acceptCount); + Assert.Equal (1, selectedCount); + Assert.Equal (2, acceptedCount); View superView = new () { Id = "superView", CanFocus = true }; superView.Add (radioGroup); @@ -719,8 +728,8 @@ public class RadioGroupTests (ITestOutputHelper output) Assert.True (radioGroup.NewMouseEvent (new () { Position = new (0, 0), Flags = MouseFlags.Button1Clicked })); Assert.Equal (0, radioGroup.SelectedItem); Assert.Equal (2, selectedItemChanged); - Assert.Equal (2, selectCount); - Assert.Equal (2, acceptCount); + Assert.Equal (2, selectedCount); + Assert.Equal (2, acceptedCount); var superViewAcceptCount = 0; @@ -732,24 +741,28 @@ public class RadioGroupTests (ITestOutputHelper output) Assert.Equal (0, superViewAcceptCount); - handleAccept = true; - Assert.False (radioGroup.NewMouseEvent (new () { Position = new (0, 0), Flags = MouseFlags.Button1DoubleClicked })); + // By handling the event, we're cancelling it. So the radio group should not change. + handleAccepted = true; + Assert.True (radioGroup.NewMouseEvent (new () { Position = new (0, 0), Flags = MouseFlags.Button1Clicked })); + Assert.True (radioGroup.NewMouseEvent (new () { Position = new (0, 0), Flags = MouseFlags.Button1DoubleClicked })); Assert.Equal (0, radioGroup.SelectedItem); Assert.Equal (2, selectedItemChanged); - Assert.Equal (2, selectCount); - Assert.Equal (3, acceptCount); - Assert.Equal (1, superViewAcceptCount); + Assert.Equal (2, selectedCount); + Assert.Equal (3, acceptedCount); + Assert.Equal (0, superViewAcceptCount); - handleAccept = false; - Assert.False (radioGroup.NewMouseEvent (new () { Position = new (0, 0), Flags = MouseFlags.Button1DoubleClicked })); + handleAccepted = false; + Assert.True (radioGroup.NewMouseEvent (new () { Position = new (0, 0), Flags = MouseFlags.Button1Clicked })); + Assert.True (radioGroup.NewMouseEvent (new () { Position = new (0, 0), Flags = MouseFlags.Button1DoubleClicked })); Assert.Equal (0, radioGroup.SelectedItem); Assert.Equal (2, selectedItemChanged); - Assert.Equal (2, selectCount); - Assert.Equal (4, acceptCount); - Assert.Equal (3, superViewAcceptCount); // Accept bubbles up to superview + Assert.Equal (2, selectedCount); + Assert.Equal (4, acceptedCount); + Assert.Equal (1, superViewAcceptCount); // Accept bubbles up to superview radioGroup.DoubleClickAccepts = false; - Assert.False (radioGroup.NewMouseEvent (new () { Position = new (0, 1), Flags = MouseFlags.Button1DoubleClicked })); + Assert.True (radioGroup.NewMouseEvent (new () { Position = new (0, 1), Flags = MouseFlags.Button1Clicked })); + Assert.True (radioGroup.NewMouseEvent (new () { Position = new (0, 1), Flags = MouseFlags.Button1DoubleClicked })); } #endregion Mouse Tests