diff --git a/Terminal.Gui/Application/Application.cs b/Terminal.Gui/Application/Application.cs index e3912475f..fe1a5cd55 100644 --- a/Terminal.Gui/Application/Application.cs +++ b/Terminal.Gui/Application/Application.cs @@ -26,6 +26,79 @@ public static partial class Application /// Gets all cultures supported by the application without the invariant language. public static List? SupportedCultures { get; private set; } + /// + /// Gets a string representation of the Application as rendered by . + /// + /// A string representation of the Application + public new static string ToString () + { + ConsoleDriver driver = Driver; + + if (driver is null) + { + return string.Empty; + } + + return ToString (driver); + } + + /// + /// Gets a string representation of the Application rendered by the provided . + /// + /// The driver to use to render the contents. + /// A string representation of the Application + public static string ToString (ConsoleDriver driver) + { + var sb = new StringBuilder (); + + Cell [,] contents = driver.Contents; + + for (var r = 0; r < driver.Rows; r++) + { + for (var c = 0; c < driver.Cols; c++) + { + Rune rune = contents [r, c].Rune; + + if (rune.DecodeSurrogatePair (out char [] sp)) + { + sb.Append (sp); + } + else + { + sb.Append ((char)rune.Value); + } + + if (rune.GetColumns () > 1) + { + c++; + } + + // See Issue #2616 + //foreach (var combMark in contents [r, c].CombiningMarks) { + // sb.Append ((char)combMark.Value); + //} + } + + sb.AppendLine (); + } + + return sb.ToString (); + } + + internal static List GetAvailableCulturesFromEmbeddedResources () + { + ResourceManager rm = new (typeof (Strings)); + + CultureInfo [] cultures = CultureInfo.GetCultures (CultureTypes.AllCultures); + + return cultures.Where ( + cultureInfo => + !cultureInfo.Equals (CultureInfo.InvariantCulture) + && rm.GetResourceSet (cultureInfo, true, false) is { } + ) + .ToList (); + } + internal static List GetSupportedCultures () { CultureInfo [] cultures = CultureInfo.GetCultures (CultureTypes.AllCultures); @@ -54,20 +127,6 @@ public static partial class Application return GetAvailableCulturesFromEmbeddedResources (); } - internal static List GetAvailableCulturesFromEmbeddedResources () - { - ResourceManager rm = new (typeof (Strings)); - - CultureInfo [] cultures = CultureInfo.GetCultures (CultureTypes.AllCultures); - - return cultures.Where ( - cultureInfo => - !cultureInfo.Equals (CultureInfo.InvariantCulture) - && rm.GetResourceSet (cultureInfo, true, false) is { } - ) - .ToList (); - } - // IMPORTANT: Ensure all property/fields are reset here. See Init_ResetState_Resets_Properties unit test. // Encapsulate all setting of initial state for Application; Having // this in a function like this ensures we don't make mistakes in @@ -159,70 +218,5 @@ public static partial class Application SynchronizationContext.SetSynchronizationContext (null); } -#nullable enable -#nullable restore - -#nullable enable - // Only return true if the Current has changed. -#nullable restore - - /// - /// Gets a string representation of the Application as rendered by . - /// - /// A string representation of the Application - public new static string ToString () - { - ConsoleDriver driver = Driver; - - if (driver is null) - { - return string.Empty; - } - - return ToString (driver); - } - - /// - /// Gets a string representation of the Application rendered by the provided . - /// - /// The driver to use to render the contents. - /// A string representation of the Application - public static string ToString (ConsoleDriver driver) - { - var sb = new StringBuilder (); - - Cell [,] contents = driver.Contents; - - for (var r = 0; r < driver.Rows; r++) - { - for (var c = 0; c < driver.Cols; c++) - { - Rune rune = contents [r, c].Rune; - - if (rune.DecodeSurrogatePair (out char [] sp)) - { - sb.Append (sp); - } - else - { - sb.Append ((char)rune.Value); - } - - if (rune.GetColumns () > 1) - { - c++; - } - - // See Issue #2616 - //foreach (var combMark in contents [r, c].CombiningMarks) { - // sb.Append ((char)combMark.Value); - //} - } - - sb.AppendLine (); - } - - return sb.ToString (); - } } diff --git a/Terminal.Gui/Input/KeyBindings.cs b/Terminal.Gui/Input/KeyBindings.cs index 5dd69a260..4df4b6e32 100644 --- a/Terminal.Gui/Input/KeyBindings.cs +++ b/Terminal.Gui/Input/KeyBindings.cs @@ -1,12 +1,9 @@ #nullable enable -using System.Diagnostics; -using Microsoft.CodeAnalysis; - namespace Terminal.Gui; /// -/// Provides a collection of objects bound to a . +/// Provides a collection of objects bound to a . /// public class KeyBindings { @@ -19,19 +16,6 @@ public class KeyBindings /// Initializes a new instance bound to . public KeyBindings (View boundView) { BoundView = boundView; } - /// - /// The view that the are bound to. - /// - /// - /// If , the are not bound to a . This is used for Application.KeyBindings. - /// - public View? BoundView { get; } - - // TODO: Add a dictionary comparer that ignores Scope - // TODO: This should not be public! - /// The collection of objects. - public Dictionary Bindings { get; } = new (); - /// Adds a to the collection. /// /// @@ -45,21 +29,21 @@ public class KeyBindings if (TryGet (key, out KeyBinding _)) { - throw new InvalidOperationException(@$"A key binding for {key} exists ({binding})."); + throw new InvalidOperationException (@$"A key binding for {key} exists ({binding})."); + //Bindings [key] = binding; } + + if (BoundView is { }) + { + binding.BoundView = BoundView; + } else { - if (BoundView is { }) - { - binding.BoundView = BoundView; - } - else - { - binding.BoundView = boundViewForAppScope; - } - Bindings.Add (key, binding); + binding.BoundView = boundViewForAppScope; } + + Bindings.Add (key, binding); } /// @@ -102,14 +86,12 @@ public class KeyBindings if (TryGet (key, out KeyBinding binding)) { throw new InvalidOperationException (@$"A key binding for {key} exists ({binding})."); + //Bindings [key] = new (commands, scope, BoundView); } - else - { - Add (key, new KeyBinding (commands, scope, BoundView), boundViewForAppScope); - } - } + Add (key, new KeyBinding (commands, scope, BoundView), boundViewForAppScope); + } /// /// Adds a new key combination that will trigger the commands in . @@ -129,7 +111,7 @@ public class KeyBindings /// multiple commands are provided,they will be applied in sequence. The bound strike will be /// consumed if any took effect. /// - public void Add (Key key, KeyBindingScope scope, params Command [] commands) + public void Add (Key key, KeyBindingScope scope, params Command [] commands) { if (BoundView is { } && scope.FastHasFlags (KeyBindingScope.Application)) { @@ -150,10 +132,8 @@ public class KeyBindings { throw new InvalidOperationException (@$"A key binding for {key} exists ({binding})."); } - else - { - Add (key, new KeyBinding (commands, scope, BoundView), null); - } + + Add (key, new KeyBinding (commands, scope, BoundView)); } /// @@ -162,7 +142,8 @@ public class KeyBindings /// View - see ). /// /// - /// This is a helper function for . If used for a View ( is set), the scope will be set to . + /// This is a helper function for . If used for a View ( + /// is set), the scope will be set to . /// Otherwise, it will be set to . /// /// @@ -185,8 +166,9 @@ public class KeyBindings { if (BoundView is null && boundViewForAppScope is null) { - throw new ArgumentException (@"Application scoped KeyBindings must provide a bound view to Add.", nameof(boundViewForAppScope)); + throw new ArgumentException (@"Application scoped KeyBindings must provide a bound view to Add.", nameof (boundViewForAppScope)); } + Add (key, BoundView is { } ? KeyBindingScope.Focused : KeyBindingScope.Application, boundViewForAppScope, commands); } @@ -196,7 +178,8 @@ public class KeyBindings /// View - see ). /// /// - /// This is a helper function for . If used for a View ( is set), the scope will be set to . + /// This is a helper function for . If used for a View ( + /// is set), the scope will be set to . /// Otherwise, it will be set to . /// /// @@ -220,14 +203,26 @@ public class KeyBindings { throw new ArgumentException (@"Application scoped KeyBindings must provide a boundViewForAppScope to Add."); } + Add (key, BoundView is { } ? KeyBindingScope.Focused : KeyBindingScope.Application, null, commands); } + // TODO: Add a dictionary comparer that ignores Scope + // TODO: This should not be public! + /// The collection of objects. + public Dictionary Bindings { get; } = new (); + + /// + /// The view that the are bound to. + /// + /// + /// If , the are not bound to a . This is used for + /// Application.KeyBindings. + /// + public View? BoundView { get; } + /// Removes all objects from the collection. - public void Clear () - { - Bindings.Clear (); - } + public void Clear () { Bindings.Clear (); } /// /// Removes all key bindings that trigger the given command set. Views can have multiple different keys bound to @@ -255,6 +250,7 @@ public class KeyBindings { return binding; } + throw new InvalidOperationException ($"Key {key} is not bound."); } @@ -268,6 +264,7 @@ public class KeyBindings { return binding; } + throw new InvalidOperationException ($"Key {key}/{scope} is not bound."); } @@ -299,7 +296,6 @@ public class KeyBindings /// Optional View for bindings. public void Remove (Key key, View? boundViewForAppScope = null) { - if (!TryGet (key, out KeyBinding binding)) { return; @@ -308,6 +304,26 @@ public class KeyBindings Bindings.Remove (key); } + /// Replaces the commands already bound to a key. + /// + /// + /// If the key is not already bound, it will be added. + /// + /// + /// The key bound to the command to be replaced. + /// The set of commands to replace the old ones with. + public void ReplaceCommands (Key key, params Command [] commands) + { + if (TryGet (key, out KeyBinding binding)) + { + binding.Commands = commands; + } + else + { + Add (key, commands); + } + } + /// Replaces a key combination already bound to a set of s. /// /// The key to be replaced. @@ -329,26 +345,6 @@ public class KeyBindings Add (newKey, value); } - /// Replaces the commands already bound to a key. - /// - /// - /// If the key is not already bound, it will be added. - /// - /// - /// The key bound to the command to be replaced. - /// The set of commands to replace the old ones with. - public void ReplaceCommands (Key key, params Command [] commands) - { - if (TryGet (key, out KeyBinding binding)) - { - binding.Commands = commands; - } - else - { - Add (key, commands); - } - } - /// Gets the commands bound with the specified Key. /// /// The key to check. @@ -360,6 +356,7 @@ public class KeyBindings public bool TryGet (Key key, out KeyBinding binding) { binding = new (Array.Empty (), KeyBindingScope.Disabled, null); + if (key.IsValid) { return Bindings.TryGetValue (key, out binding); @@ -380,6 +377,7 @@ public class KeyBindings public bool TryGet (Key key, KeyBindingScope scope, out KeyBinding binding) { binding = new (Array.Empty (), KeyBindingScope.Disabled, null); + if (key.IsValid && Bindings.TryGetValue (key, out binding)) { if (scope.HasFlag (binding.Scope)) diff --git a/Terminal.Gui/Resources/config.json b/Terminal.Gui/Resources/config.json index a80d8334e..d001326a0 100644 --- a/Terminal.Gui/Resources/config.json +++ b/Terminal.Gui/Resources/config.json @@ -31,8 +31,9 @@ "Dialog.DefaultButtonAlignmentModes": "AddSpaceBetweenItems", "FrameView.DefaultBorderStyle": "Single", "Window.DefaultBorderStyle": "Single", - "Dialog.DefaultBorderStyle": "Single", - "MessageBox.DefaultBorderStyle": "Double", + "Dialog.DefaultBorderStyle": "Heavy", + "MessageBox.DefaultButtonAlignment": "Center", + "MessageBox.DefaultBorderStyle": "Heavy", "Button.DefaultShadow": "None", "ColorSchemes": [ { diff --git a/Terminal.Gui/Views/Dialog.cs b/Terminal.Gui/Views/Dialog.cs index 7b4da9355..c1f18e8fb 100644 --- a/Terminal.Gui/Views/Dialog.cs +++ b/Terminal.Gui/Views/Dialog.cs @@ -20,7 +20,7 @@ public class Dialog : Window /// This property can be set in a Theme. [SerializableConfigurationProperty (Scope = typeof (ThemeScope))] [JsonConverter (typeof (JsonStringEnumConverter))] - public static Alignment DefaultButtonAlignment { get; set; } = Alignment.End; + public static Alignment DefaultButtonAlignment { get; set; } = Alignment.End; // Default is set in config.json /// The default for . /// This property can be set in a Theme. @@ -48,7 +48,7 @@ public class Dialog : Window /// [SerializableConfigurationProperty (Scope = typeof (ThemeScope))] [JsonConverter (typeof (JsonStringEnumConverter))] - public new static ShadowStyle DefaultShadow { get; set; } = ShadowStyle.None; + public new static ShadowStyle DefaultShadow { get; set; } = ShadowStyle.None; // Default is set in config.json /// /// Defines the default border styling for . Can be configured via @@ -57,7 +57,7 @@ public class Dialog : Window [SerializableConfigurationProperty (Scope = typeof (ThemeScope))] [JsonConverter (typeof (JsonStringEnumConverter))] - public new static LineStyle DefaultBorderStyle { get; set; } = LineStyle.Single; + public new static LineStyle DefaultBorderStyle { get; set; } = LineStyle.Single; // Default is set in config.json private readonly List [SerializableConfigurationProperty (Scope = typeof (ThemeScope))] [JsonConverter (typeof (JsonStringEnumConverter))] - public static LineStyle DefaultBorderStyle { get; set; } = LineStyle.Single; + public static LineStyle DefaultBorderStyle { get; set; } = LineStyle.Single; // Default is set in config.json + + /// The default for . + /// This property can be set in a Theme. + [SerializableConfigurationProperty (Scope = typeof (ThemeScope))] + [JsonConverter (typeof (JsonStringEnumConverter))] + public static Alignment DefaultButtonAlignment { get; set; } = Alignment.Center; // Default is set in config.json /// /// Defines the default minimum MessageBox width, as a percentage of the screen width. Can be configured via @@ -365,10 +371,10 @@ public static class MessageBox var d = new Dialog { Title = title, - Buttons = buttonList.ToArray (), - ButtonAlignment = Alignment.Center, + ButtonAlignment = MessageBox.DefaultButtonAlignment, ButtonAlignmentModes = AlignmentModes.StartToEnd | AlignmentModes.AddSpaceBetweenItems, BorderStyle = MessageBox.DefaultBorderStyle, + Buttons = buttonList.ToArray (), }; d.Width = Dim.Auto (DimAutoStyle.Auto, diff --git a/Terminal.Gui/Views/Shortcut.cs b/Terminal.Gui/Views/Shortcut.cs index 6530d71bc..ff1d4b21f 100644 --- a/Terminal.Gui/Views/Shortcut.cs +++ b/Terminal.Gui/Views/Shortcut.cs @@ -74,7 +74,7 @@ public class Shortcut : View, IOrientation, IDesignable CommandView = new () { Width = Dim.Auto (), - Height = Dim.Auto (DimAutoStyle.Auto, minimumContentDim: 1) + Height = Dim.Auto (1) }; HelpView.Id = "_helpView"; diff --git a/UICatalog/Scenarios/AdornmentsEditor.cs b/UICatalog/Scenarios/AdornmentsEditor.cs index 472ed4897..2f389552a 100644 --- a/UICatalog/Scenarios/AdornmentsEditor.cs +++ b/UICatalog/Scenarios/AdornmentsEditor.cs @@ -9,23 +9,10 @@ namespace UICatalog.Scenarios; /// public class AdornmentsEditor : View { - private View _viewToEdit; - - private Label _lblView; // Text describing the vi - - private MarginEditor _marginEditor; - private BorderEditor _borderEditor; - private PaddingEditor _paddingEditor; - - // TODO: Move Diagnostics to a separate Editor class (DiagnosticsEditor?). - private CheckBox _diagPaddingCheckBox; - private CheckBox _diagRulerCheckBox; - private readonly ViewDiagnosticFlags _savedDiagnosticFlags = Diagnostics; - public AdornmentsEditor () { //ColorScheme = Colors.ColorSchemes ["Dialog"]; - Title = $"AdornmentsEditor"; + Title = "AdornmentsEditor"; Width = Dim.Auto (DimAutoStyle.Content); Height = Dim.Auto (DimAutoStyle.Content); @@ -39,34 +26,57 @@ public class AdornmentsEditor : View Initialized += AdornmentsEditor_Initialized; } - private void ApplicationNavigationOnFocusedChanged (object sender, EventArgs e) - { - if (ApplicationNavigation.IsInHierarchy (this, Application.Navigation!.GetFocused ())) - { - return; - } + private readonly ViewDiagnosticFlags _savedDiagnosticFlags = Diagnostics; + private View _viewToEdit; - if (Application.Navigation!.GetFocused () is Adornment adornment) + private Label _lblView; // Text describing the vi + + private MarginEditor _marginEditor; + private BorderEditor _borderEditor; + private PaddingEditor _paddingEditor; + + // TODO: Move Diagnostics to a separate Editor class (DiagnosticsEditor?). + private CheckBox _diagPaddingCheckBox; + private CheckBox _diagRulerCheckBox; + + /// + /// Gets or sets whether the AdornmentsEditor should automatically select the View to edit when the mouse is clicked + /// anywhere outside the editor. + /// + public bool AutoSelectViewToEdit { get; set; } + + public View ViewToEdit + { + get => _viewToEdit; + set { - ViewToEdit = adornment.Parent; - } - else - { - ViewToEdit = Application.Navigation.GetFocused (); + if (_viewToEdit == value) + { + return; + } + + _viewToEdit = value; + + _marginEditor.AdornmentToEdit = _viewToEdit?.Margin ?? null; + _borderEditor.AdornmentToEdit = _viewToEdit?.Border ?? null; + _paddingEditor.AdornmentToEdit = _viewToEdit?.Padding ?? null; + + _lblView.Text = $"{_viewToEdit?.GetType ().Name}: {_viewToEdit?.Id}" ?? string.Empty; } } - /// - /// Gets or sets whether the AdornmentsEditor should automatically select the View to edit when the mouse is clicked - /// anywhere outside the editor. - /// - public bool AutoSelectViewToEdit { get; set; } + /// + protected override void Dispose (bool disposing) + { + Diagnostics = _savedDiagnosticFlags; + base.Dispose (disposing); + } private void AdornmentsEditor_Initialized (object sender, EventArgs e) { BorderStyle = LineStyle.Dotted; - ExpanderButton expandButton = new ExpanderButton () + var expandButton = new ExpanderButton { Orientation = Orientation.Horizontal }; @@ -76,7 +86,7 @@ public class AdornmentsEditor : View { X = 0, Y = 0, - Height = 2, + Height = 2 }; _lblView.TextFormatter.WordWrap = true; _lblView.TextFormatter.MultiLine = true; @@ -113,16 +123,16 @@ public class AdornmentsEditor : View _diagPaddingCheckBox.State = Diagnostics.FastHasFlags (ViewDiagnosticFlags.Padding) ? CheckState.Checked : CheckState.UnChecked; _diagPaddingCheckBox.Toggle += (s, e) => - { - if (e.NewValue == CheckState.Checked) - { - Diagnostics |= ViewDiagnosticFlags.Padding; - } - else - { - Diagnostics &= ~ViewDiagnosticFlags.Padding; - } - }; + { + if (e.NewValue == CheckState.Checked) + { + Diagnostics |= ViewDiagnosticFlags.Padding; + } + else + { + Diagnostics &= ~ViewDiagnosticFlags.Padding; + } + }; Add (_diagPaddingCheckBox); _diagPaddingCheckBox.Y = Pos.Bottom (_paddingEditor); @@ -131,16 +141,16 @@ public class AdornmentsEditor : View _diagRulerCheckBox.State = Diagnostics.FastHasFlags (ViewDiagnosticFlags.Ruler) ? CheckState.Checked : CheckState.UnChecked; _diagRulerCheckBox.Toggle += (s, e) => - { - if (e.NewValue == CheckState.Checked) - { - Diagnostics |= ViewDiagnosticFlags.Ruler; - } - else - { - Diagnostics &= ~ViewDiagnosticFlags.Ruler; - } - }; + { + if (e.NewValue == CheckState.Checked) + { + Diagnostics |= ViewDiagnosticFlags.Ruler; + } + else + { + Diagnostics &= ~ViewDiagnosticFlags.Ruler; + } + }; Add (_diagRulerCheckBox); _diagRulerCheckBox.Y = Pos.Bottom (_diagPaddingCheckBox); @@ -154,7 +164,8 @@ public class AdornmentsEditor : View } // TODO: Add a setting (property) so only subviews of a specified view are considered. - var view = e.View; + View view = e.View; + if (view is { } && e.Flags == MouseFlags.Button1Clicked) { if (view is Adornment adornment) @@ -168,33 +179,20 @@ public class AdornmentsEditor : View } } - /// - protected override void Dispose (bool disposing) + private void ApplicationNavigationOnFocusedChanged (object sender, EventArgs e) { - View.Diagnostics = _savedDiagnosticFlags; - base.Dispose (disposing); - } - - public View ViewToEdit - { - get => _viewToEdit; - set + if (ApplicationNavigation.IsInHierarchy (this, Application.Navigation!.GetFocused ())) { - if (_viewToEdit == value) - { - return; - } - - _viewToEdit = value; - - - _marginEditor.AdornmentToEdit = _viewToEdit?.Margin ?? null; - _borderEditor.AdornmentToEdit = _viewToEdit?.Border ?? null; - _paddingEditor.AdornmentToEdit = _viewToEdit?.Padding ?? null; - - _lblView.Text = $"{_viewToEdit?.GetType ().Name}: {_viewToEdit?.Id}" ?? string.Empty; - return; } + + if (Application.Navigation!.GetFocused () is Adornment adornment) + { + ViewToEdit = adornment.Parent; + } + else + { + ViewToEdit = Application.Navigation.GetFocused (); + } } -} \ No newline at end of file +} diff --git a/UnitTests/Application/ApplicationTests.cs b/UnitTests/Application/ApplicationTests.cs index b84dc8ae8..b0383e986 100644 --- a/UnitTests/Application/ApplicationTests.cs +++ b/UnitTests/Application/ApplicationTests.cs @@ -6,8 +6,6 @@ namespace Terminal.Gui.ApplicationTests; public class ApplicationTests { - private readonly ITestOutputHelper _output; - public ApplicationTests (ITestOutputHelper output) { _output = output; @@ -19,6 +17,127 @@ public class ApplicationTests #endif } + private readonly ITestOutputHelper _output; + + private object _timeoutLock; + + [Fact] + public void AddTimeout_Fires () + { + Assert.Null (_timeoutLock); + _timeoutLock = new (); + + uint timeoutTime = 250; + var initialized = false; + var iteration = 0; + var shutdown = false; + object timeout = null; + var timeoutCount = 0; + + Application.InitializedChanged += OnApplicationOnInitializedChanged; + + Application.Init (new FakeDriver ()); + Assert.True (initialized); + Assert.False (shutdown); + + _output.WriteLine ("Application.Run ().Dispose ().."); + Application.Run ().Dispose (); + _output.WriteLine ("Back from Application.Run ().Dispose ()"); + + Assert.True (initialized); + Assert.False (shutdown); + + Assert.Equal (1, timeoutCount); + Application.Shutdown (); + + Application.InitializedChanged -= OnApplicationOnInitializedChanged; + + lock (_timeoutLock) + { + if (timeout is { }) + { + Application.RemoveTimeout (timeout); + timeout = null; + } + } + + Assert.True (initialized); + Assert.True (shutdown); + +#if DEBUG_IDISPOSABLE + Assert.Empty (Responder.Instances); +#endif + lock (_timeoutLock) + { + _timeoutLock = null; + } + + return; + + void OnApplicationOnInitializedChanged (object s, EventArgs a) + { + if (a.CurrentValue) + { + Application.Iteration += OnApplicationOnIteration; + initialized = true; + + lock (_timeoutLock) + { + _output.WriteLine ($"Setting timeout for {timeoutTime}ms"); + timeout = Application.AddTimeout (TimeSpan.FromMilliseconds (timeoutTime), TimeoutCallback); + } + } + else + { + Application.Iteration -= OnApplicationOnIteration; + shutdown = true; + } + } + + bool TimeoutCallback () + { + lock (_timeoutLock) + { + _output.WriteLine ($"TimeoutCallback. Count: {++timeoutCount}. Application Iteration: {iteration}"); + + if (timeout is { }) + { + _output.WriteLine (" Nulling timeout."); + timeout = null; + } + } + + // False means "don't re-do timer and remove it" + return false; + } + + void OnApplicationOnIteration (object s, IterationEventArgs a) + { + lock (_timeoutLock) + { + if (timeoutCount > 0) + { + _output.WriteLine ($"Iteration #{iteration} - Timeout fired. Calling Application.RequestStop."); + Application.RequestStop (); + + return; + } + } + + iteration++; + + // Simulate a delay + Thread.Sleep ((int)timeoutTime / 10); + + // Worst case scenario - something went wrong + if (Application.IsInitialized && iteration > 25) + { + _output.WriteLine ($"Too many iterations ({iteration}): Calling Application.RequestStop."); + Application.RequestStop (); + } + } + } + [Fact] public void Begin_Null_Toplevel_Throws () { @@ -194,7 +313,7 @@ public class ApplicationTests // Internal properties Assert.False (Application.IsInitialized); Assert.Equal (Application.GetSupportedCultures (), Application.SupportedCultures); - Assert.Equal (Application.GetAvailableCulturesFromEmbeddedResources(), Application.SupportedCultures); + Assert.Equal (Application.GetAvailableCulturesFromEmbeddedResources (), Application.SupportedCultures); Assert.False (Application._forceFakeConsole); Assert.Equal (-1, Application.MainThreadId); Assert.Empty (Application.TopLevels); @@ -285,8 +404,8 @@ public class ApplicationTests [InlineData (typeof (CursesDriver))] public void Init_Shutdown_Fire_InitializedChanged (Type driverType) { - bool initialized = false; - bool shutdown = false; + var initialized = false; + var shutdown = false; Application.InitializedChanged += OnApplicationOnInitializedChanged; @@ -315,34 +434,6 @@ public class ApplicationTests } } - - [Fact] - public void Run_Iteration_Fires () - { - int iteration = 0; - - Application.Init (new FakeDriver ()); - - Application.Iteration += Application_Iteration; - Application.Run ().Dispose (); - - Assert.Equal (1, iteration); - Application.Shutdown (); - - return; - - void Application_Iteration (object sender, IterationEventArgs e) - { - if (iteration > 0) - { - Assert.Fail (); - } - iteration++; - Application.RequestStop (); - } - } - - [Fact] public void Init_Unbalanced_Throws () { @@ -449,6 +540,33 @@ public class ApplicationTests Application.Shutdown (); } + [Fact] + public void Run_Iteration_Fires () + { + var iteration = 0; + + Application.Init (new FakeDriver ()); + + Application.Iteration += Application_Iteration; + Application.Run ().Dispose (); + + Assert.Equal (1, iteration); + Application.Shutdown (); + + return; + + void Application_Iteration (object sender, IterationEventArgs e) + { + if (iteration > 0) + { + Assert.Fail (); + } + + iteration++; + Application.RequestStop (); + } + } + [Fact] [AutoInitShutdown] public void SetCurrentAsTop_Run_A_Not_Modal_Toplevel_Make_It_The_Current_Application_Top () @@ -899,15 +1017,15 @@ public class ApplicationTests RunState rs = Application.Begin (w); // Don't use visuals to test as style of border can change over time. - Assert.Equal (new Point (0, 0), w.Frame.Location); + Assert.Equal (new (0, 0), w.Frame.Location); Application.OnMouseEvent (new () { Flags = MouseFlags.Button1Pressed }); Assert.Equal (w.Border, Application.MouseGrabView); - Assert.Equal (new Point (0, 0), w.Frame.Location); + Assert.Equal (new (0, 0), w.Frame.Location); // Move down and to the right. Application.OnMouseEvent (new () { Position = new (1, 1), Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition }); - Assert.Equal (new Point (1, 1), w.Frame.Location); + Assert.Equal (new (1, 1), w.Frame.Location); Application.End (rs); w.Dispose (); @@ -1042,6 +1160,7 @@ public class ApplicationTests Assert.Throws (() => Application.Run (new Toplevel ())); Application.Init (driver); + Application.Iteration += (s, e) => { Assert.NotNull (Application.Top); @@ -1101,123 +1220,4 @@ public class ApplicationTests } #endregion - - - private object _timeoutLock; - - [Fact] - public void AddTimeout_Fires () - { - Assert.Null (_timeoutLock); - _timeoutLock = new object (); - - uint timeoutTime = 250; - bool initialized = false; - int iteration = 0; - bool shutdown = false; - object timeout = null; - int timeoutCount = 0; - - Application.InitializedChanged += OnApplicationOnInitializedChanged; - - Application.Init (new FakeDriver ()); - Assert.True (initialized); - Assert.False (shutdown); - - _output.WriteLine ("Application.Run ().Dispose ().."); - Application.Run ().Dispose (); - _output.WriteLine ("Back from Application.Run ().Dispose ()"); - - Assert.True (initialized); - Assert.False (shutdown); - - Assert.Equal (1, timeoutCount); - Application.Shutdown (); - - Application.InitializedChanged -= OnApplicationOnInitializedChanged; - - lock (_timeoutLock) - { - if (timeout is { }) - { - Application.RemoveTimeout (timeout); - timeout = null; - } - } - - Assert.True (initialized); - Assert.True (shutdown); - -#if DEBUG_IDISPOSABLE - Assert.Empty (Responder.Instances); -#endif - lock (_timeoutLock) - { - _timeoutLock = null; - } - - return; - - void OnApplicationOnInitializedChanged (object s, EventArgs a) - { - if (a.CurrentValue) - { - Application.Iteration += OnApplicationOnIteration; - initialized = true; - - lock (_timeoutLock) - { - _output.WriteLine ($"Setting timeout for {timeoutTime}ms"); - timeout = Application.AddTimeout (TimeSpan.FromMilliseconds (timeoutTime), TimeoutCallback); - } - - } - else - { - Application.Iteration -= OnApplicationOnIteration; - shutdown = true; - } - } - - bool TimeoutCallback () - { - lock (_timeoutLock) - { - _output.WriteLine ($"TimeoutCallback. Count: {++timeoutCount}. Application Iteration: {iteration}"); - if (timeout is { }) - { - _output.WriteLine ($" Nulling timeout."); - timeout = null; - } - } - - // False means "don't re-do timer and remove it" - return false; - } - - void OnApplicationOnIteration (object s, IterationEventArgs a) - { - lock (_timeoutLock) - { - if (timeoutCount > 0) - { - _output.WriteLine ($"Iteration #{iteration} - Timeout fired. Calling Application.RequestStop."); - Application.RequestStop (); - - return; - } - } - iteration++; - - // Simulate a delay - Thread.Sleep ((int)timeoutTime / 10); - - // Worst case scenario - something went wrong - if (Application.IsInitialized && iteration > 25) - { - _output.WriteLine ($"Too many iterations ({iteration}): Calling Application.RequestStop."); - Application.RequestStop (); - } - } - } } diff --git a/UnitTests/Dialogs/DialogTests.cs b/UnitTests/Dialogs/DialogTests.cs index d07b627ee..582cf659a 100644 --- a/UnitTests/Dialogs/DialogTests.cs +++ b/UnitTests/Dialogs/DialogTests.cs @@ -26,6 +26,10 @@ public class DialogTests int width = $@"{CM.Glyphs.VLine} {btn1} {btn2} {CM.Glyphs.VLine}".Length; d.SetBufferSize (width, 1); + // Override CM + Dialog.DefaultButtonAlignment = Alignment.Center; + Dialog.DefaultBorderStyle = LineStyle.Single; + // Default (center) var dlg = new Dialog { @@ -151,6 +155,7 @@ public class DialogTests int width = buttonRow.Length; d.SetBufferSize (buttonRow.Length, 3); + // Default - Center (runstate, Dialog dlg) = RunButtonTestDialog ( title, @@ -874,6 +879,11 @@ public class DialogTests { ((FakeDriver)Driver).SetBufferSize (20, 5); + // Override CM + Window.DefaultBorderStyle = LineStyle.Single; + Dialog.DefaultButtonAlignment = Alignment.Center; + Dialog.DefaultBorderStyle = LineStyle.Single; + var win = new Window (); var iterations = 0; @@ -889,7 +899,6 @@ public class DialogTests win.Loaded += (s, a) => { - Dialog.DefaultButtonAlignment = Alignment.Center; var dlg = new Dialog { Width = 18, Height = 3, Buttons = [new () { Text = "Ok" }] }; dlg.Loaded += (s, a) => @@ -975,7 +984,10 @@ public class DialogTests var win = new Window (); int iterations = -1; + + // Override CM Dialog.DefaultButtonAlignment = Alignment.Center; + Dialog.DefaultBorderStyle = LineStyle.Single; Iteration += (s, a) => { @@ -1012,7 +1024,10 @@ public class DialogTests public void Dialog_Opened_From_Another_Dialog () { ((FakeDriver)Driver).SetBufferSize (30, 10); + + // Override CM Dialog.DefaultButtonAlignment = Alignment.Center; + Dialog.DefaultBorderStyle = LineStyle.Single; var btn1 = new Button { Text = "press me 1" }; Button btn2 = null; @@ -1159,6 +1174,11 @@ public class DialogTests [AutoInitShutdown] public void Location_When_Application_Top_Not_Default () { + // Override CM + Window.DefaultBorderStyle = LineStyle.Single; + Dialog.DefaultButtonAlignment = Alignment.Center; + Dialog.DefaultBorderStyle = LineStyle.Single; + var expected = 5; var d = new Dialog { X = expected, Y = expected, Height = 5, Width = 5 }; Begin (d); @@ -1188,6 +1208,11 @@ public class DialogTests int iterations = -1; + // Override CM + Window.DefaultBorderStyle = LineStyle.Single; + Dialog.DefaultButtonAlignment = Alignment.Center; + Dialog.DefaultBorderStyle = LineStyle.Single; + Iteration += (s, a) => { iterations++; @@ -1361,6 +1386,10 @@ public class DialogTests params Button [] btns ) { + // Override CM + Dialog.DefaultButtonAlignment = Alignment.Center; + Dialog.DefaultBorderStyle = LineStyle.Single; + var dlg = new Dialog { Title = title, diff --git a/UnitTests/Dialogs/MessageBoxTests.cs b/UnitTests/Dialogs/MessageBoxTests.cs index de94732ad..c4e501f80 100644 --- a/UnitTests/Dialogs/MessageBoxTests.cs +++ b/UnitTests/Dialogs/MessageBoxTests.cs @@ -174,6 +174,10 @@ public class MessageBoxTests var btn = $"{CM.Glyphs.LeftBracket}{CM.Glyphs.LeftDefaultIndicator} btn {CM.Glyphs.RightDefaultIndicator}{CM.Glyphs.RightBracket}"; + // Override CM + MessageBox.DefaultButtonAlignment = Alignment.End; + MessageBox.DefaultBorderStyle = LineStyle.Double; + Application.Iteration += (s, a) => { iterations++; @@ -239,6 +243,10 @@ public class MessageBoxTests var btn = $"{CM.Glyphs.LeftBracket}{CM.Glyphs.LeftDefaultIndicator} btn {CM.Glyphs.RightDefaultIndicator}{CM.Glyphs.RightBracket}"; + // Override CM + MessageBox.DefaultButtonAlignment = Alignment.End; + MessageBox.DefaultBorderStyle = LineStyle.Double; + Application.Iteration += (s, a) => { iterations++; @@ -415,6 +423,10 @@ public class MessageBoxTests int iterations = -1; ((FakeDriver)Application.Driver).SetBufferSize (70, 15); + // Override CM + MessageBox.DefaultButtonAlignment = Alignment.End; + MessageBox.DefaultBorderStyle = LineStyle.Double; + Application.Iteration += (s, a) => { iterations++; diff --git a/UnitTests/FileServices/FileDialogTests.cs b/UnitTests/FileServices/FileDialogTests.cs index 86cb50481..1c0954168 100644 --- a/UnitTests/FileServices/FileDialogTests.cs +++ b/UnitTests/FileServices/FileDialogTests.cs @@ -572,6 +572,11 @@ public class FileDialogTests (ITestOutputHelper output) private FileDialog GetInitializedFileDialog () { + + Window.DefaultBorderStyle = LineStyle.Single; + Dialog.DefaultButtonAlignment = Alignment.Center; + Dialog.DefaultBorderStyle = LineStyle.Single; + var dlg = new FileDialog (); Begin (dlg); @@ -580,6 +585,10 @@ public class FileDialogTests (ITestOutputHelper output) private FileDialog GetLinuxDialog () { + Window.DefaultBorderStyle = LineStyle.Single; + Dialog.DefaultButtonAlignment = Alignment.Center; + Dialog.DefaultBorderStyle = LineStyle.Single; + // Arrange var fileSystem = new MockFileSystem (new Dictionary (), "/"); fileSystem.MockTime (() => new (2010, 01, 01, 11, 12, 43)); @@ -623,6 +632,11 @@ public class FileDialogTests (ITestOutputHelper output) private FileDialog GetWindowsDialog () { + // Override CM + Window.DefaultBorderStyle = LineStyle.Single; + Dialog.DefaultButtonAlignment = Alignment.Center; + Dialog.DefaultBorderStyle = LineStyle.Single; + // Arrange var fileSystem = new MockFileSystem (new Dictionary (), @"c:\"); fileSystem.MockTime (() => new (2010, 01, 01, 11, 12, 43)); diff --git a/UnitTests/Input/KeyBindingTests.cs b/UnitTests/Input/KeyBindingTests.cs index e78ec5730..9ef3da07e 100644 --- a/UnitTests/Input/KeyBindingTests.cs +++ b/UnitTests/Input/KeyBindingTests.cs @@ -4,17 +4,8 @@ namespace Terminal.Gui.InputTests; public class KeyBindingTests { - private readonly ITestOutputHelper _output; public KeyBindingTests (ITestOutputHelper output) { _output = output; } - - [Fact] - public void Add_No_Commands_Throws () - { - var keyBindings = new KeyBindings (); - List commands = new (); - Assert.Throws (() => keyBindings.Add (Key.A, commands.ToArray ())); - - } + private readonly ITestOutputHelper _output; [Fact] public void Add_Invalid_Key_Throws () @@ -41,6 +32,14 @@ public class KeyBindingTests Assert.Contains (Command.Left, resultCommands); } + [Fact] + public void Add_No_Commands_Throws () + { + var keyBindings = new KeyBindings (); + List commands = new (); + Assert.Throws (() => keyBindings.Add (Key.A, commands.ToArray ())); + } + [Fact] public void Add_Single_Adds () { @@ -54,6 +53,39 @@ public class KeyBindingTests Assert.Contains (Command.HotKey, resultCommands); } + // Add should not allow duplicates + [Fact] + public void Add_Throws_If_Exists () + { + var keyBindings = new KeyBindings (); + keyBindings.Add (Key.A, KeyBindingScope.Application, Command.HotKey); + Assert.Throws (() => keyBindings.Add (Key.A, KeyBindingScope.Application, Command.Accept)); + + Command [] resultCommands = keyBindings.GetCommands (Key.A); + Assert.Contains (Command.HotKey, resultCommands); + + keyBindings = new (); + keyBindings.Add (Key.A, KeyBindingScope.Focused, Command.HotKey); + Assert.Throws (() => keyBindings.Add (Key.A, KeyBindingScope.Focused, Command.Accept)); + + resultCommands = keyBindings.GetCommands (Key.A); + Assert.Contains (Command.HotKey, resultCommands); + + keyBindings = new (); + keyBindings.Add (Key.A, KeyBindingScope.HotKey, Command.HotKey); + Assert.Throws (() => keyBindings.Add (Key.A, KeyBindingScope.Focused, Command.Accept)); + + resultCommands = keyBindings.GetCommands (Key.A); + Assert.Contains (Command.HotKey, resultCommands); + + keyBindings = new (); + keyBindings.Add (Key.A, new KeyBinding (new [] { Command.HotKey }, KeyBindingScope.HotKey)); + Assert.Throws (() => keyBindings.Add (Key.A, new KeyBinding (new [] { Command.Accept }, KeyBindingScope.HotKey))); + + resultCommands = keyBindings.GetCommands (Key.A); + Assert.Contains (Command.HotKey, resultCommands); + } + // Clear [Fact] public void Clear_Clears () @@ -74,6 +106,14 @@ public class KeyBindingTests Assert.Throws (() => keyBindings.GetKeyFromCommands (Command.Accept)); } + [Fact] + public void Get_Binding_Not_Found_Throws () + { + var keyBindings = new KeyBindings (); + Assert.Throws (() => keyBindings.Get (Key.A)); + Assert.Throws (() => keyBindings.Get (Key.B, KeyBindingScope.Application)); + } + // GetCommands [Fact] public void GetCommands_Unknown_ReturnsEmpty () @@ -168,39 +208,6 @@ public class KeyBindingTests Assert.Equal (Key.A, resultKey); } - // Add should not allow duplicates - [Fact] - public void Add_Throws_If_Exists () - { - var keyBindings = new KeyBindings (); - keyBindings.Add (Key.A, KeyBindingScope.Application, Command.HotKey); - Assert.Throws (() => keyBindings.Add (Key.A, KeyBindingScope.Application, Command.Accept)); - - Command [] resultCommands = keyBindings.GetCommands (Key.A); - Assert.Contains (Command.HotKey, resultCommands); - - keyBindings = new (); - keyBindings.Add (Key.A, KeyBindingScope.Focused, Command.HotKey); - Assert.Throws (() => keyBindings.Add (Key.A, KeyBindingScope.Focused, Command.Accept)); - - resultCommands = keyBindings.GetCommands (Key.A); - Assert.Contains (Command.HotKey, resultCommands); - - keyBindings = new (); - keyBindings.Add (Key.A, KeyBindingScope.HotKey, Command.HotKey); - Assert.Throws (() => keyBindings.Add (Key.A, KeyBindingScope.Focused, Command.Accept)); - - resultCommands = keyBindings.GetCommands (Key.A); - Assert.Contains (Command.HotKey, resultCommands); - - keyBindings = new (); - keyBindings.Add (Key.A, new KeyBinding (new [] { Command.HotKey }, KeyBindingScope.HotKey)); - Assert.Throws (() => keyBindings.Add (Key.A, new KeyBinding (new [] { Command.Accept }, KeyBindingScope.HotKey))); - - resultCommands = keyBindings.GetCommands (Key.A); - Assert.Contains (Command.HotKey, resultCommands); - } - [Fact] public void ReplaceKey_Replaces () { @@ -227,6 +234,18 @@ public class KeyBindingTests Assert.Contains (Command.HotKey, keyBindings.GetCommands (Key.H)); } + [Fact] + public void ReplaceKey_Replaces_Leaves_Old_Binding () + { + var keyBindings = new KeyBindings (); + keyBindings.Add (Key.A, KeyBindingScope.Application, Command.Accept); + keyBindings.Add (Key.B, KeyBindingScope.Application, Command.HotKey); + + keyBindings.ReplaceKey (keyBindings.GetKeyFromCommands (Command.Accept), Key.C); + Assert.Empty (keyBindings.GetCommands (Key.A)); + Assert.Contains (Command.Accept, keyBindings.GetCommands (Key.C)); + } + [Fact] public void ReplaceKey_Throws_If_DoesNotContain_Old () { @@ -242,20 +261,6 @@ public class KeyBindingTests Assert.Throws (() => keyBindings.ReplaceKey (Key.A, Key.Empty)); } - [Fact] - public void ReplaceKey_Replaces_Leaves_Old_Binding () - { - var keyBindings = new KeyBindings (); - keyBindings.Add (Key.A, KeyBindingScope.Application, Command.Accept); - keyBindings.Add (Key.B, KeyBindingScope.Application, Command.HotKey); - - keyBindings.ReplaceKey (keyBindings.GetKeyFromCommands(Command.Accept), Key.C); - Assert.Empty (keyBindings.GetCommands (Key.A)); - Assert.Contains (Command.Accept, keyBindings.GetCommands (Key.C)); - - } - - // Add with scope does the right things [Theory] [InlineData (KeyBindingScope.Focused)] @@ -301,14 +306,6 @@ public class KeyBindingTests Assert.Contains (Command.Left, binding.Commands); } - [Fact] - public void Get_Binding_Not_Found_Throws () - { - var keyBindings = new KeyBindings (); - Assert.Throws (() => keyBindings.Get (Key.A)); - Assert.Throws (() => keyBindings.Get (Key.B, KeyBindingScope.Application)); - } - [Theory] [InlineData (KeyBindingScope.Focused)] [InlineData (KeyBindingScope.HotKey)] @@ -355,6 +352,4 @@ public class KeyBindingTests Assert.True (result); Assert.Contains (Command.HotKey, bindings.Commands); } - - } diff --git a/UnitTests/View/NavigationTests.cs b/UnitTests/View/NavigationTests.cs index 59245d3c8..488bed57f 100644 --- a/UnitTests/View/NavigationTests.cs +++ b/UnitTests/View/NavigationTests.cs @@ -4,6 +4,331 @@ namespace Terminal.Gui.ViewTests; public class NavigationTests (ITestOutputHelper _output) : TestsAllViews { + [Theory] + [MemberData (nameof (AllViewTypes))] + public void AllViews_AtLeastOneNavKey_Leaves (Type viewType) + { + View view = CreateInstanceIfNotGeneric (viewType); + + if (view == null) + { + _output.WriteLine ($"Ignoring {viewType} - It's a Generic"); + + return; + } + + if (!view.CanFocus) + { + _output.WriteLine ($"Ignoring {viewType} - It can't focus."); + + return; + } + + Application.Init (new FakeDriver ()); + + Toplevel top = new (); + + View otherView = new () + { + Id = "otherView", + CanFocus = true, + TabStop = view.TabStop + }; + + top.Add (view, otherView); + Application.Begin (top); + + // Start with the focus on our test view + view.SetFocus (); + + Key [] navKeys = { Key.Tab, Key.Tab.WithShift, Key.CursorUp, Key.CursorDown, Key.CursorLeft, Key.CursorRight }; + + if (view.TabStop == TabBehavior.TabGroup) + { + navKeys = new [] { Key.F6, Key.F6.WithShift }; + } + + var left = false; + + foreach (Key key in navKeys) + { + switch (view.TabStop) + { + case TabBehavior.TabStop: + case TabBehavior.NoStop: + case TabBehavior.TabGroup: + Application.OnKeyDown (key); + + break; + default: + Application.OnKeyDown (Key.Tab); + + break; + } + + if (!view.HasFocus) + { + left = true; + _output.WriteLine ($"{view.GetType ().Name} - {key} Left."); + view.SetFocus (); + } + else + { + _output.WriteLine ($"{view.GetType ().Name} - {key} did not Leave."); + } + } + + top.Dispose (); + Application.Shutdown (); + + Assert.True (left); + } + + [Theory] + [MemberData (nameof (AllViewTypes))] + public void AllViews_Enter_Leave_Events (Type viewType) + { + View view = CreateInstanceIfNotGeneric (viewType); + + if (view == null) + { + _output.WriteLine ($"Ignoring {viewType} - It's a Generic"); + + return; + } + + if (!view.CanFocus) + { + _output.WriteLine ($"Ignoring {viewType} - It can't focus."); + + return; + } + + if (view is Toplevel && ((Toplevel)view).Modal) + { + _output.WriteLine ($"Ignoring {viewType} - It's a Modal Toplevel"); + + return; + } + + Application.Init (new FakeDriver ()); + + Toplevel top = new () + { + Height = 10, + Width = 10 + }; + + View otherView = new () + { + Id = "otherView", + X = 0, Y = 0, + Height = 1, + Width = 1, + CanFocus = true, + TabStop = view.TabStop + }; + + view.X = Pos.Right (otherView); + view.Y = 0; + view.Width = 10; + view.Height = 1; + + var nEnter = 0; + var nLeave = 0; + + view.Enter += (s, e) => nEnter++; + view.Leave += (s, e) => nLeave++; + + top.Add (view, otherView); + Application.Begin (top); + + // Start with the focus on our test view + view.SetFocus (); + + //Assert.Equal (1, nEnter); + //Assert.Equal (0, nLeave); + + // Use keyboard to navigate to next view (otherView). + if (view is TextView) + { + Application.OnKeyDown (Key.F6); + } + else + { + var tries = 0; + + while (view.HasFocus) + { + if (++tries > 10) + { + Assert.Fail ($"{view} is not leaving."); + } + + switch (view.TabStop) + { + case TabBehavior.NoStop: + Application.OnKeyDown (Key.Tab); + + break; + case TabBehavior.TabStop: + Application.OnKeyDown (Key.Tab); + + break; + case TabBehavior.TabGroup: + Application.OnKeyDown (Key.F6); + + break; + case null: + Application.OnKeyDown (Key.Tab); + + break; + default: + throw new ArgumentOutOfRangeException (); + } + } + } + + //Assert.Equal (1, nEnter); + //Assert.Equal (1, nLeave); + + //Assert.False (view.HasFocus); + //Assert.True (otherView.HasFocus); + + // Now navigate back to our test view + switch (view.TabStop) + { + case TabBehavior.NoStop: + view.SetFocus (); + + break; + case TabBehavior.TabStop: + Application.OnKeyDown (Key.Tab); + + break; + case TabBehavior.TabGroup: + Application.OnKeyDown (Key.F6); + + break; + case null: + Application.OnKeyDown (Key.Tab); + + break; + default: + throw new ArgumentOutOfRangeException (); + } + + // Cache state because Shutdown has side effects. + // Also ensures other tests can continue running if there's a fail + bool otherViewHasFocus = otherView.HasFocus; + bool viewHasFocus = view.HasFocus; + + int enterCount = nEnter; + int leaveCount = nLeave; + + top.Dispose (); + Application.Shutdown (); + + Assert.False (otherViewHasFocus); + Assert.True (viewHasFocus); + + Assert.Equal (2, enterCount); + Assert.Equal (1, leaveCount); + } + + [Theory] + [MemberData (nameof (AllViewTypes))] + public void AllViews_Enter_Leave_Events_Visible_False (Type viewType) + { + View view = CreateInstanceIfNotGeneric (viewType); + + if (view == null) + { + _output.WriteLine ($"Ignoring {viewType} - It's a Generic"); + + return; + } + + if (!view.CanFocus) + { + _output.WriteLine ($"Ignoring {viewType} - It can't focus."); + + return; + } + + if (view is Toplevel && ((Toplevel)view).Modal) + { + _output.WriteLine ($"Ignoring {viewType} - It's a Modal Toplevel"); + + return; + } + + Application.Init (new FakeDriver ()); + + Toplevel top = new () + { + Height = 10, + Width = 10 + }; + + View otherView = new () + { + X = 0, Y = 0, + Height = 1, + Width = 1, + CanFocus = true + }; + + view.Visible = false; + view.X = Pos.Right (otherView); + view.Y = 0; + view.Width = 10; + view.Height = 1; + + var nEnter = 0; + var nLeave = 0; + + view.Enter += (s, e) => nEnter++; + view.Leave += (s, e) => nLeave++; + + top.Add (view, otherView); + Application.Begin (top); + + // Start with the focus on our test view + view.SetFocus (); + + Assert.Equal (0, nEnter); + Assert.Equal (0, nLeave); + + // Use keyboard to navigate to next view (otherView). + if (view is TextView) + { + Application.OnKeyDown (Key.F6); + } + else if (view is DatePicker) + { + for (var i = 0; i < 4; i++) + { + Application.OnKeyDown (Key.F6); + } + } + else + { + Application.OnKeyDown (Key.Tab); + } + + Assert.Equal (0, nEnter); + Assert.Equal (0, nLeave); + + top.NewKeyDownEvent (Key.Tab); + + Assert.Equal (0, nEnter); + Assert.Equal (0, nLeave); + + top.Dispose (); + Application.Shutdown (); + } + [Fact] public void BringSubviewForward_Subviews_vs_TabIndexes () { @@ -240,6 +565,23 @@ public class NavigationTests (ITestOutputHelper _output) : TestsAllViews Application.Shutdown (); } + [Fact] + public void CanFocus_False_Set_HasFocus_To_False () + { + var view = new View { CanFocus = true }; + var view2 = new View { CanFocus = true }; + view2.Add (view); + + Assert.True (view.CanFocus); + + view.SetFocus (); + Assert.True (view.HasFocus); + + view.CanFocus = false; + Assert.False (view.CanFocus); + Assert.False (view.HasFocus); + } + [Fact] public void CanFocus_Set_Changes_TabIndex_And_TabStop () { @@ -283,23 +625,6 @@ public class NavigationTests (ITestOutputHelper _output) : TestsAllViews r.Dispose (); } - [Fact] - public void CanFocus_False_Set_HasFocus_To_False () - { - var view = new View { CanFocus = true }; - var view2 = new View { CanFocus = true }; - view2.Add (view); - - Assert.True (view.CanFocus); - - view.SetFocus (); - Assert.True (view.HasFocus); - - view.CanFocus = false; - Assert.False (view.CanFocus); - Assert.False (view.HasFocus); - } - [Fact] [AutoInitShutdown] public void CanFocus_Sets_To_False_On_Single_View_Focus_View_On_Another_Toplevel () @@ -443,7 +768,7 @@ public class NavigationTests (ITestOutputHelper _output) : TestsAllViews view.NewKeyDownEvent (Key.Space); Assert.True (wasClicked); - view.NewMouseEvent (new MouseEvent { Flags = MouseFlags.Button1Clicked }); + view.NewMouseEvent (new() { Flags = MouseFlags.Button1Clicked }); Assert.False (wasClicked); Assert.True (view.Enabled); Assert.True (view.CanFocus); @@ -452,7 +777,7 @@ public class NavigationTests (ITestOutputHelper _output) : TestsAllViews view.Enabled = false; view.NewKeyDownEvent (Key.Space); Assert.False (wasClicked); - view.NewMouseEvent (new MouseEvent { Flags = MouseFlags.Button1Clicked }); + view.NewMouseEvent (new() { Flags = MouseFlags.Button1Clicked }); Assert.False (wasClicked); Assert.False (view.Enabled); Assert.True (view.CanFocus); @@ -482,7 +807,7 @@ public class NavigationTests (ITestOutputHelper _output) : TestsAllViews win.NewKeyDownEvent (Key.Enter); Assert.True (wasClicked); - button.NewMouseEvent (new MouseEvent { Flags = MouseFlags.Button1Clicked }); + button.NewMouseEvent (new() { Flags = MouseFlags.Button1Clicked }); Assert.False (wasClicked); Assert.True (button.Enabled); Assert.True (button.CanFocus); @@ -494,7 +819,7 @@ public class NavigationTests (ITestOutputHelper _output) : TestsAllViews win.Enabled = false; button.NewKeyDownEvent (Key.Enter); Assert.False (wasClicked); - button.NewMouseEvent (new MouseEvent { Flags = MouseFlags.Button1Clicked }); + button.NewMouseEvent (new() { Flags = MouseFlags.Button1Clicked }); Assert.False (wasClicked); Assert.False (button.Enabled); Assert.True (button.CanFocus); @@ -523,6 +848,22 @@ public class NavigationTests (ITestOutputHelper _output) : TestsAllViews top.Dispose (); } + // View.Focused & View.MostFocused tests + + // View.Focused - No subviews + [Fact] + [Trait ("BUGBUG", "Fix in Issue #3444")] + public void Focused_NoSubviews () + { + var view = new View (); + Assert.Null (view.Focused); + + view.CanFocus = true; + view.SetFocus (); + Assert.True (view.HasFocus); + Assert.Null (view.Focused); // BUGBUG: Should be view + } + [Fact] [AutoInitShutdown] public void FocusNearestView_Ensure_Focus_Ordered () @@ -586,7 +927,7 @@ public class NavigationTests (ITestOutputHelper _output) : TestsAllViews if (!removed) { removed = true; - view3 = new View { Id = "view3", Y = 1, Width = 10, Height = 5 }; + view3 = new() { Id = "view3", Y = 1, Width = 10, Height = 5 }; Application.Current.Add (view3); Application.Current.BringSubviewToFront (view3); Assert.False (view3.HasFocus); @@ -623,6 +964,20 @@ public class NavigationTests (ITestOutputHelper _output) : TestsAllViews top1.Dispose (); } + // View.MostFocused - No subviews + [Fact] + [Trait ("BUGBUG", "Fix in Issue #3444")] + public void Most_Focused_NoSubviews () + { + var view = new View (); + Assert.Null (view.Focused); + + view.CanFocus = true; + view.SetFocus (); + Assert.True (view.HasFocus); + Assert.Null (view.MostFocused); // BUGBUG: Should be view + } + // [Fact] // [AutoInitShutdown] // public void HotKey_Will_Invoke_KeyPressed_Only_For_The_MostFocused_With_Top_KeyPress_Event () @@ -763,7 +1118,7 @@ public class NavigationTests (ITestOutputHelper _output) : TestsAllViews top.Ready += (s, e) => { Assert.Null (top.Focused); }; // Keyboard navigation with tab - FakeConsole.MockKeyPresses.Push (new ConsoleKeyInfo ('\t', ConsoleKey.Tab, false, false, false)); + FakeConsole.MockKeyPresses.Push (new ('\t', ConsoleKey.Tab, false, false, false)); Application.Iteration += (s, a) => Application.RequestStop (); @@ -833,13 +1188,13 @@ public class NavigationTests (ITestOutputHelper _output) : TestsAllViews Application.Begin (top); Assert.Equal (Application.Current, top); - Assert.Equal (new Rectangle (0, 0, 80, 25), new Rectangle (0, 0, View.Driver.Cols, View.Driver.Rows)); - Assert.Equal (new Rectangle (0, 0, View.Driver.Cols, View.Driver.Rows), top.Frame); - Assert.Equal (new Rectangle (0, 0, 80, 25), top.Frame); + Assert.Equal (new (0, 0, 80, 25), new Rectangle (0, 0, View.Driver.Cols, View.Driver.Rows)); + Assert.Equal (new (0, 0, View.Driver.Cols, View.Driver.Rows), top.Frame); + Assert.Equal (new (0, 0, 80, 25), top.Frame); ((FakeDriver)Application.Driver!).SetBufferSize (20, 10); - Assert.Equal (new Rectangle (0, 0, View.Driver.Cols, View.Driver.Rows), top.Frame); - Assert.Equal (new Rectangle (0, 0, 20, 10), top.Frame); + Assert.Equal (new (0, 0, View.Driver.Cols, View.Driver.Rows), top.Frame); + Assert.Equal (new (0, 0, 20, 10), top.Frame); _ = TestHelpers.AssertDriverContentsWithFrameAre ( @" @@ -858,7 +1213,7 @@ public class NavigationTests (ITestOutputHelper _output) : TestsAllViews // top Assert.Equal (Point.Empty, top.ScreenToFrame (new (0, 0))); - var screen = top.Margin.ViewportToScreen (new Point (0, 0)); + Point screen = top.Margin.ViewportToScreen (new Point (0, 0)); Assert.Equal (0, screen.X); Assert.Equal (0, screen.Y); screen = top.Border.ViewportToScreen (new Point (0, 0)); @@ -878,24 +1233,27 @@ public class NavigationTests (ITestOutputHelper _output) : TestsAllViews Assert.Equal (0, found.Frame.X); Assert.Equal (0, found.Frame.Y); - Assert.Equal (new Point (3, 2), top.ScreenToFrame (new (3, 2))); + Assert.Equal (new (3, 2), top.ScreenToFrame (new (3, 2))); screen = top.ViewportToScreen (new Point (3, 2)); Assert.Equal (4, screen.X); Assert.Equal (3, screen.Y); found = View.FindDeepestView (top, new (screen.X, screen.Y)); Assert.Equal (view, found); + //Assert.Equal (0, found.FrameToScreen ().X); //Assert.Equal (0, found.FrameToScreen ().Y); found = View.FindDeepestView (top, new (3, 2)); Assert.Equal (top, found); + //Assert.Equal (3, found.FrameToScreen ().X); //Assert.Equal (2, found.FrameToScreen ().Y); - Assert.Equal (new Point (13, 2), top.ScreenToFrame (new (13, 2))); + Assert.Equal (new (13, 2), top.ScreenToFrame (new (13, 2))); screen = top.ViewportToScreen (new Point (12, 2)); Assert.Equal (13, screen.X); Assert.Equal (3, screen.Y); found = View.FindDeepestView (top, new (screen.X, screen.Y)); Assert.Equal (view, found); + //Assert.Equal (9, found.FrameToScreen ().X); //Assert.Equal (0, found.FrameToScreen ().Y); screen = top.ViewportToScreen (new Point (13, 2)); @@ -903,19 +1261,21 @@ public class NavigationTests (ITestOutputHelper _output) : TestsAllViews Assert.Equal (3, screen.Y); found = View.FindDeepestView (top, new (13, 2)); Assert.Equal (top, found); + //Assert.Equal (13, found.FrameToScreen ().X); //Assert.Equal (2, found.FrameToScreen ().Y); - Assert.Equal (new Point (14, 3), top.ScreenToFrame (new (14, 3))); + Assert.Equal (new (14, 3), top.ScreenToFrame (new (14, 3))); screen = top.ViewportToScreen (new Point (14, 3)); Assert.Equal (15, screen.X); Assert.Equal (4, screen.Y); found = View.FindDeepestView (top, new (14, 3)); Assert.Equal (top, found); + //Assert.Equal (14, found.FrameToScreen ().X); //Assert.Equal (3, found.FrameToScreen ().Y); // view - Assert.Equal (new Point (-4, -3), view.ScreenToFrame (new (0, 0))); + Assert.Equal (new (-4, -3), view.ScreenToFrame (new (0, 0))); screen = view.Margin.ViewportToScreen (new Point (-3, -2)); Assert.Equal (1, screen.X); Assert.Equal (1, screen.Y); @@ -934,21 +1294,21 @@ public class NavigationTests (ITestOutputHelper _output) : TestsAllViews found = View.FindDeepestView (top, new (0, 0)); Assert.Equal (top.Border, found); - Assert.Equal (new Point (-1, -1), view.ScreenToFrame (new (3, 2))); + Assert.Equal (new (-1, -1), view.ScreenToFrame (new (3, 2))); screen = view.ViewportToScreen (new Point (0, 0)); Assert.Equal (4, screen.X); Assert.Equal (3, screen.Y); found = View.FindDeepestView (top, new (4, 3)); Assert.Equal (view, found); - Assert.Equal (new Point (9, -1), view.ScreenToFrame (new (13, 2))); + Assert.Equal (new (9, -1), view.ScreenToFrame (new (13, 2))); screen = view.ViewportToScreen (new Point (10, 0)); Assert.Equal (14, screen.X); Assert.Equal (3, screen.Y); found = View.FindDeepestView (top, new (14, 3)); Assert.Equal (top, found); - Assert.Equal (new Point (10, 0), view.ScreenToFrame (new (14, 3))); + Assert.Equal (new (10, 0), view.ScreenToFrame (new (14, 3))); screen = view.ViewportToScreen (new Point (11, 1)); Assert.Equal (15, screen.X); Assert.Equal (4, screen.Y); @@ -983,17 +1343,17 @@ public class NavigationTests (ITestOutputHelper _output) : TestsAllViews Application.Begin (top); Assert.Equal (Application.Current, top); - Assert.Equal (new Rectangle (0, 0, 80, 25), new Rectangle (0, 0, View.Driver.Cols, View.Driver.Rows)); - Assert.NotEqual (new Rectangle (0, 0, View.Driver.Cols, View.Driver.Rows), top.Frame); - Assert.Equal (new Rectangle (3, 2, 20, 10), top.Frame); + Assert.Equal (new (0, 0, 80, 25), new Rectangle (0, 0, View.Driver.Cols, View.Driver.Rows)); + Assert.NotEqual (new (0, 0, View.Driver.Cols, View.Driver.Rows), top.Frame); + Assert.Equal (new (3, 2, 20, 10), top.Frame); ((FakeDriver)Application.Driver!).SetBufferSize (30, 20); - Assert.Equal (new Rectangle (0, 0, 30, 20), new Rectangle (0, 0, View.Driver.Cols, View.Driver.Rows)); - Assert.NotEqual (new Rectangle (0, 0, View.Driver.Cols, View.Driver.Rows), top.Frame); - Assert.Equal (new Rectangle (3, 2, 20, 10), top.Frame); + Assert.Equal (new (0, 0, 30, 20), new Rectangle (0, 0, View.Driver.Cols, View.Driver.Rows)); + Assert.NotEqual (new (0, 0, View.Driver.Cols, View.Driver.Rows), top.Frame); + Assert.Equal (new (3, 2, 20, 10), top.Frame); Rectangle frame = TestHelpers.AssertDriverContentsWithFrameAre ( - @" + @" ┌──────────────────┐ │ │ │ │ @@ -1004,16 +1364,16 @@ public class NavigationTests (ITestOutputHelper _output) : TestsAllViews │ │ │ │ └──────────────────┘", - _output - ); + _output + ); // mean the output started at col 3 and line 2 // which result with a width of 23 and a height of 10 on the output - Assert.Equal (new Rectangle (3, 2, 23, 10), frame); + Assert.Equal (new (3, 2, 23, 10), frame); // top - Assert.Equal (new Point (-3, -2), top.ScreenToFrame (new (0, 0))); - var screen = top.Margin.ViewportToScreen (new Point (-3, -2)); + Assert.Equal (new (-3, -2), top.ScreenToFrame (new (0, 0))); + Point screen = top.Margin.ViewportToScreen (new Point (-3, -2)); Assert.Equal (0, screen.X); Assert.Equal (0, screen.Y); screen = top.Border.ViewportToScreen (new Point (-3, -2)); @@ -1035,23 +1395,25 @@ public class NavigationTests (ITestOutputHelper _output) : TestsAllViews Assert.Equal (4, screen.X); Assert.Equal (3, screen.Y); Assert.Equal (top.Border, View.FindDeepestView (top, new (3, 2))); + //Assert.Equal (0, found.FrameToScreen ().X); //Assert.Equal (0, found.FrameToScreen ().Y); - Assert.Equal (new Point (10, 0), top.ScreenToFrame (new (13, 2))); + Assert.Equal (new (10, 0), top.ScreenToFrame (new (13, 2))); screen = top.ViewportToScreen (new Point (10, 0)); Assert.Equal (14, screen.X); Assert.Equal (3, screen.Y); Assert.Equal (top.Border, View.FindDeepestView (top, new (13, 2))); + //Assert.Equal (10, found.FrameToScreen ().X); //Assert.Equal (0, found.FrameToScreen ().Y); - Assert.Equal (new Point (11, 1), top.ScreenToFrame (new (14, 3))); + Assert.Equal (new (11, 1), top.ScreenToFrame (new (14, 3))); screen = top.ViewportToScreen (new Point (11, 1)); Assert.Equal (15, screen.X); Assert.Equal (4, screen.Y); Assert.Equal (top, View.FindDeepestView (top, new (14, 3))); // view - Assert.Equal (new Point (-7, -5), view.ScreenToFrame (new (0, 0))); + Assert.Equal (new (-7, -5), view.ScreenToFrame (new (0, 0))); screen = view.Margin.ViewportToScreen (new Point (-6, -4)); Assert.Equal (1, screen.X); Assert.Equal (1, screen.Y); @@ -1065,27 +1427,27 @@ public class NavigationTests (ITestOutputHelper _output) : TestsAllViews Assert.Equal (1, screen.X); Assert.Equal (1, screen.Y); Assert.Null (View.FindDeepestView (top, new (1, 1))); - Assert.Equal (new Point (-4, -3), view.ScreenToFrame (new (3, 2))); + Assert.Equal (new (-4, -3), view.ScreenToFrame (new (3, 2))); screen = view.ViewportToScreen (new Point (-3, -2)); Assert.Equal (4, screen.X); Assert.Equal (3, screen.Y); Assert.Equal (top, View.FindDeepestView (top, new (4, 3))); - Assert.Equal (new Point (-1, -1), view.ScreenToFrame (new (6, 4))); + Assert.Equal (new (-1, -1), view.ScreenToFrame (new (6, 4))); screen = view.ViewportToScreen (new Point (0, 0)); Assert.Equal (7, screen.X); Assert.Equal (5, screen.Y); Assert.Equal (view, View.FindDeepestView (top, new (7, 5))); - Assert.Equal (new Point (6, -1), view.ScreenToFrame (new (13, 4))); + Assert.Equal (new (6, -1), view.ScreenToFrame (new (13, 4))); screen = view.ViewportToScreen (new Point (7, 0)); Assert.Equal (14, screen.X); Assert.Equal (5, screen.Y); Assert.Equal (view, View.FindDeepestView (top, new (14, 5))); - Assert.Equal (new Point (7, -2), view.ScreenToFrame (new (14, 3))); + Assert.Equal (new (7, -2), view.ScreenToFrame (new (14, 3))); screen = view.ViewportToScreen (new Point (8, -1)); Assert.Equal (15, screen.X); Assert.Equal (4, screen.Y); Assert.Equal (top, View.FindDeepestView (top, new (15, 4))); - Assert.Equal (new Point (16, -2), view.ScreenToFrame (new (23, 3))); + Assert.Equal (new (16, -2), view.ScreenToFrame (new (23, 3))); screen = view.ViewportToScreen (new Point (17, -1)); Assert.Equal (24, screen.X); Assert.Equal (4, screen.Y); @@ -1211,6 +1573,80 @@ public class NavigationTests (ITestOutputHelper _output) : TestsAllViews r.Dispose (); } + [Fact] + public void TabIndex_Invert_Order () + { + var r = new View (); + var v1 = new View { Id = "1", CanFocus = true }; + var v2 = new View { Id = "2", CanFocus = true }; + var v3 = new View { Id = "3", CanFocus = true }; + + r.Add (v1, v2, v3); + + v1.TabIndex = 2; + v2.TabIndex = 1; + v3.TabIndex = 0; + Assert.True (r.TabIndexes.IndexOf (v1) == 2); + Assert.True (r.TabIndexes.IndexOf (v2) == 1); + Assert.True (r.TabIndexes.IndexOf (v3) == 0); + + Assert.True (r.Subviews.IndexOf (v1) == 0); + Assert.True (r.Subviews.IndexOf (v2) == 1); + Assert.True (r.Subviews.IndexOf (v3) == 2); + } + + [Fact] + public void TabIndex_Invert_Order_Added_One_By_One_Does_Not_Do_What_Is_Expected () + { + var r = new View (); + var v1 = new View { Id = "1", CanFocus = true }; + r.Add (v1); + v1.TabIndex = 2; + var v2 = new View { Id = "2", CanFocus = true }; + r.Add (v2); + v2.TabIndex = 1; + var v3 = new View { Id = "3", CanFocus = true }; + r.Add (v3); + v3.TabIndex = 0; + + Assert.False (r.TabIndexes.IndexOf (v1) == 2); + Assert.True (r.TabIndexes.IndexOf (v1) == 1); + Assert.False (r.TabIndexes.IndexOf (v2) == 1); + Assert.True (r.TabIndexes.IndexOf (v2) == 2); + + // Only the last is in the expected index + Assert.True (r.TabIndexes.IndexOf (v3) == 0); + + Assert.True (r.Subviews.IndexOf (v1) == 0); + Assert.True (r.Subviews.IndexOf (v2) == 1); + Assert.True (r.Subviews.IndexOf (v3) == 2); + } + + [Fact] + public void TabIndex_Invert_Order_Mixed () + { + var r = new View (); + var vl1 = new View { Id = "vl1" }; + var v1 = new View { Id = "v1", CanFocus = true }; + var vl2 = new View { Id = "vl2" }; + var v2 = new View { Id = "v2", CanFocus = true }; + var vl3 = new View { Id = "vl3" }; + var v3 = new View { Id = "v3", CanFocus = true }; + + r.Add (vl1, v1, vl2, v2, vl3, v3); + + v1.TabIndex = 2; + v2.TabIndex = 1; + v3.TabIndex = 0; + Assert.True (r.TabIndexes.IndexOf (v1) == 4); + Assert.True (r.TabIndexes.IndexOf (v2) == 2); + Assert.True (r.TabIndexes.IndexOf (v3) == 0); + + Assert.True (r.Subviews.IndexOf (v1) == 1); + Assert.True (r.Subviews.IndexOf (v2) == 3); + Assert.True (r.Subviews.IndexOf (v3) == 5); + } + [Fact] public void TabIndex_Set_CanFocus_False () { @@ -1299,117 +1735,23 @@ public class NavigationTests (ITestOutputHelper _output) : TestsAllViews } [Fact] - public void TabIndex_Invert_Order () + public void TabStop_And_CanFocus_Are_All_True () { var r = new View (); - var v1 = new View () { Id = "1", CanFocus = true }; - var v2 = new View () { Id = "2", CanFocus = true }; - var v3 = new View () { Id = "3", CanFocus = true }; + var v1 = new View { CanFocus = true }; + var v2 = new View { CanFocus = true }; + var v3 = new View { CanFocus = true }; r.Add (v1, v2, v3); - v1.TabIndex = 2; - v2.TabIndex = 1; - v3.TabIndex = 0; - Assert.True (r.TabIndexes.IndexOf (v1) == 2); - Assert.True (r.TabIndexes.IndexOf (v2) == 1); - Assert.True (r.TabIndexes.IndexOf (v3) == 0); - - Assert.True (r.Subviews.IndexOf (v1) == 0); - Assert.True (r.Subviews.IndexOf (v2) == 1); - Assert.True (r.Subviews.IndexOf (v3) == 2); - } - - [Fact] - public void TabIndex_Invert_Order_Added_One_By_One_Does_Not_Do_What_Is_Expected () - { - var r = new View (); - var v1 = new View () { Id = "1", CanFocus = true }; - r.Add (v1); - v1.TabIndex = 2; - var v2 = new View () { Id = "2", CanFocus = true }; - r.Add (v2); - v2.TabIndex = 1; - var v3 = new View () { Id = "3", CanFocus = true }; - r.Add (v3); - v3.TabIndex = 0; - - Assert.False (r.TabIndexes.IndexOf (v1) == 2); - Assert.True (r.TabIndexes.IndexOf (v1) == 1); - Assert.False (r.TabIndexes.IndexOf (v2) == 1); - Assert.True (r.TabIndexes.IndexOf (v2) == 2); - // Only the last is in the expected index - Assert.True (r.TabIndexes.IndexOf (v3) == 0); - - Assert.True (r.Subviews.IndexOf (v1) == 0); - Assert.True (r.Subviews.IndexOf (v2) == 1); - Assert.True (r.Subviews.IndexOf (v3) == 2); - } - - [Fact] - public void TabIndex_Invert_Order_Mixed () - { - var r = new View (); - var vl1 = new View () { Id = "vl1" }; - var v1 = new View () { Id = "v1", CanFocus = true }; - var vl2 = new View () { Id = "vl2" }; - var v2 = new View () { Id = "v2", CanFocus = true }; - var vl3 = new View () { Id = "vl3" }; - var v3 = new View () { Id = "v3", CanFocus = true }; - - r.Add (vl1, v1, vl2, v2, vl3, v3); - - v1.TabIndex = 2; - v2.TabIndex = 1; - v3.TabIndex = 0; - Assert.True (r.TabIndexes.IndexOf (v1) == 4); - Assert.True (r.TabIndexes.IndexOf (v2) == 2); - Assert.True (r.TabIndexes.IndexOf (v3) == 0); - - Assert.True (r.Subviews.IndexOf (v1) == 1); - Assert.True (r.Subviews.IndexOf (v2) == 3); - Assert.True (r.Subviews.IndexOf (v3) == 5); - } - - [Fact] - public void TabStop_NoStop_Prevents_Stop () - { - var r = new View (); - var v1 = new View { CanFocus = true, TabStop = TabBehavior.NoStop }; - var v2 = new View { CanFocus = true, TabStop = TabBehavior.NoStop }; - var v3 = new View { CanFocus = true, TabStop = TabBehavior.NoStop }; - - r.Add (v1, v2, v3); - - r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); - Assert.False (v1.HasFocus); - Assert.False (v2.HasFocus); - Assert.False (v3.HasFocus); - } - - [Fact] - public void TabStop_NoStop_Change_Enables_Stop () - { - var r = new View (); - var v1 = new View { CanFocus = true, TabStop = TabBehavior.NoStop }; - var v2 = new View { CanFocus = true, TabStop = TabBehavior.NoStop }; - var v3 = new View { CanFocus = true, TabStop = TabBehavior.NoStop }; - - r.Add (v1, v2, v3); - - v1.TabStop = TabBehavior.TabStop; r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); Assert.True (v1.HasFocus); Assert.False (v2.HasFocus); Assert.False (v3.HasFocus); - - v2.TabStop = TabBehavior.TabStop; r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); Assert.False (v1.HasFocus); Assert.True (v2.HasFocus); Assert.False (v3.HasFocus); - - v3.TabStop = TabBehavior.TabStop; r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); Assert.False (v1.HasFocus); Assert.False (v2.HasFocus); @@ -1417,6 +1759,41 @@ public class NavigationTests (ITestOutputHelper _output) : TestsAllViews r.Dispose (); } + [Theory] + [CombinatorialData] + public void TabStop_And_CanFocus_Are_Decoupled (bool canFocus, TabBehavior tabStop) + { + var view = new View { CanFocus = canFocus, TabStop = tabStop }; + + Assert.Equal (canFocus, view.CanFocus); + Assert.Equal (tabStop, view.TabStop); + } + + [Fact] + public void TabStop_And_CanFocus_Mixed () + { + var r = new View (); + var v1 = new View { CanFocus = true, TabStop = TabBehavior.NoStop }; + var v2 = new View { CanFocus = false, TabStop = TabBehavior.TabStop }; + var v3 = new View { CanFocus = false, TabStop = TabBehavior.NoStop }; + + r.Add (v1, v2, v3); + + r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); + Assert.False (v1.HasFocus); + Assert.False (v2.HasFocus); + Assert.False (v3.HasFocus); + r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); + Assert.False (v1.HasFocus); + Assert.False (v2.HasFocus); + Assert.False (v3.HasFocus); + r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); + Assert.False (v1.HasFocus); + Assert.False (v2.HasFocus); + Assert.False (v3.HasFocus); + r.Dispose (); + } + [Theory] [CombinatorialData] public void TabStop_Change_CanFocus_Works ([CombinatorialValues (TabBehavior.NoStop, TabBehavior.TabStop, TabBehavior.TabGroup)] TabBehavior behavior) @@ -1459,56 +1836,6 @@ public class NavigationTests (ITestOutputHelper _output) : TestsAllViews r.Dispose (); } - [Fact] - public void TabStop_And_CanFocus_Are_All_True () - { - var r = new View (); - var v1 = new View { CanFocus = true }; - var v2 = new View { CanFocus = true }; - var v3 = new View { CanFocus = true }; - - r.Add (v1, v2, v3); - - r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); - Assert.True (v1.HasFocus); - Assert.False (v2.HasFocus); - Assert.False (v3.HasFocus); - r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); - Assert.False (v1.HasFocus); - Assert.True (v2.HasFocus); - Assert.False (v3.HasFocus); - r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); - Assert.False (v1.HasFocus); - Assert.False (v2.HasFocus); - Assert.True (v3.HasFocus); - r.Dispose (); - } - - [Fact] - public void TabStop_And_CanFocus_Mixed () - { - var r = new View (); - var v1 = new View { CanFocus = true, TabStop = TabBehavior.NoStop }; - var v2 = new View { CanFocus = false, TabStop = TabBehavior.TabStop }; - var v3 = new View { CanFocus = false, TabStop = TabBehavior.NoStop }; - - r.Add (v1, v2, v3); - - r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); - Assert.False (v1.HasFocus); - Assert.False (v2.HasFocus); - Assert.False (v3.HasFocus); - r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); - Assert.False (v1.HasFocus); - Assert.False (v2.HasFocus); - Assert.False (v3.HasFocus); - r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); - Assert.False (v1.HasFocus); - Assert.False (v2.HasFocus); - Assert.False (v3.HasFocus); - r.Dispose (); - } - [Fact] public void TabStop_NoStop_And_CanFocus_True_No_Focus () { @@ -1534,6 +1861,52 @@ public class NavigationTests (ITestOutputHelper _output) : TestsAllViews r.Dispose (); } + [Fact] + public void TabStop_NoStop_Change_Enables_Stop () + { + var r = new View (); + var v1 = new View { CanFocus = true, TabStop = TabBehavior.NoStop }; + var v2 = new View { CanFocus = true, TabStop = TabBehavior.NoStop }; + var v3 = new View { CanFocus = true, TabStop = TabBehavior.NoStop }; + + r.Add (v1, v2, v3); + + v1.TabStop = TabBehavior.TabStop; + r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); + Assert.True (v1.HasFocus); + Assert.False (v2.HasFocus); + Assert.False (v3.HasFocus); + + v2.TabStop = TabBehavior.TabStop; + r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); + Assert.False (v1.HasFocus); + Assert.True (v2.HasFocus); + Assert.False (v3.HasFocus); + + v3.TabStop = TabBehavior.TabStop; + r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); + Assert.False (v1.HasFocus); + Assert.False (v2.HasFocus); + Assert.True (v3.HasFocus); + r.Dispose (); + } + + [Fact] + public void TabStop_NoStop_Prevents_Stop () + { + var r = new View (); + var v1 = new View { CanFocus = true, TabStop = TabBehavior.NoStop }; + var v2 = new View { CanFocus = true, TabStop = TabBehavior.NoStop }; + var v3 = new View { CanFocus = true, TabStop = TabBehavior.NoStop }; + + r.Add (v1, v2, v3); + + r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); + Assert.False (v1.HasFocus); + Assert.False (v2.HasFocus); + Assert.False (v3.HasFocus); + } + [Fact] public void TabStop_Null_And_CanFocus_False_No_Advance () { @@ -1561,16 +1934,6 @@ public class NavigationTests (ITestOutputHelper _output) : TestsAllViews r.Dispose (); } - [Theory] - [CombinatorialData] - public void TabStop_And_CanFocus_Are_Decoupled (bool canFocus, TabBehavior tabStop) - { - var view = new View { CanFocus = canFocus, TabStop = tabStop }; - - Assert.Equal (canFocus, view.CanFocus); - Assert.Equal (tabStop, view.TabStop); - } - [Fact (Skip = "Causes crash on Ubuntu in Github Action. Bogus test anyway.")] public void WindowDispose_CanFocusProblem () { @@ -1591,348 +1954,4 @@ public class NavigationTests (ITestOutputHelper _output) : TestsAllViews // Assert does Not throw NullReferenceException top.SetFocus (); } - - // View.Focused & View.MostFocused tests - - // View.Focused - No subviews - [Fact, Trait ("BUGBUG", "Fix in Issue #3444")] - public void Focused_NoSubviews () - { - var view = new View (); - Assert.Null (view.Focused); - - view.CanFocus = true; - view.SetFocus (); - Assert.True (view.HasFocus); - Assert.Null (view.Focused); // BUGBUG: Should be view - } - - // View.MostFocused - No subviews - [Fact, Trait ("BUGBUG", "Fix in Issue #3444")] - public void Most_Focused_NoSubviews () - { - var view = new View (); - Assert.Null (view.Focused); - - view.CanFocus = true; - view.SetFocus (); - Assert.True (view.HasFocus); - Assert.Null (view.MostFocused); // BUGBUG: Should be view - } - - - [Theory] - [MemberData (nameof (AllViewTypes))] - - public void AllViews_Enter_Leave_Events (Type viewType) - { - var view = CreateInstanceIfNotGeneric (viewType); - - if (view == null) - { - _output.WriteLine ($"Ignoring {viewType} - It's a Generic"); - return; - } - - if (!view.CanFocus) - { - _output.WriteLine ($"Ignoring {viewType} - It can't focus."); - - return; - } - - if (view is Toplevel && ((Toplevel)view).Modal) - { - _output.WriteLine ($"Ignoring {viewType} - It's a Modal Toplevel"); - - return; - } - - Application.Init (new FakeDriver ()); - - Toplevel top = new () - { - Height = 10, - Width = 10 - }; - - View otherView = new () - { - Id = "otherView", - X = 0, Y = 0, - Height = 1, - Width = 1, - CanFocus = true, - TabStop = view.TabStop - }; - - view.X = Pos.Right (otherView); - view.Y = 0; - view.Width = 10; - view.Height = 1; - - var nEnter = 0; - var nLeave = 0; - - view.Enter += (s, e) => nEnter++; - view.Leave += (s, e) => nLeave++; - - top.Add (view, otherView); - Application.Begin (top); - - // Start with the focus on our test view - view.SetFocus (); - - //Assert.Equal (1, nEnter); - //Assert.Equal (0, nLeave); - - // Use keyboard to navigate to next view (otherView). - if (view is TextView) - { - Application.OnKeyDown (Key.F6); - } - else - { - int tries = 0; - while (view.HasFocus) - { - if (++tries > 10) - { - Assert.Fail ($"{view} is not leaving."); - } - - switch (view.TabStop) - { - case TabBehavior.NoStop: - Application.OnKeyDown (Key.Tab); - break; - case TabBehavior.TabStop: - Application.OnKeyDown (Key.Tab); - break; - case TabBehavior.TabGroup: - Application.OnKeyDown (Key.F6); - break; - case null: - Application.OnKeyDown (Key.Tab); - break; - default: - throw new ArgumentOutOfRangeException (); - } - } - } - - //Assert.Equal (1, nEnter); - //Assert.Equal (1, nLeave); - - //Assert.False (view.HasFocus); - //Assert.True (otherView.HasFocus); - - // Now navigate back to our test view - switch (view.TabStop) - { - case TabBehavior.NoStop: - view.SetFocus (); - break; - case TabBehavior.TabStop: - Application.OnKeyDown (Key.Tab); - break; - case TabBehavior.TabGroup: - Application.OnKeyDown (Key.F6); - break; - case null: - Application.OnKeyDown (Key.Tab); - break; - default: - throw new ArgumentOutOfRangeException (); - } - - // Cache state because Shutdown has side effects. - // Also ensures other tests can continue running if there's a fail - bool otherViewHasFocus = otherView.HasFocus; - bool viewHasFocus = view.HasFocus; - - int enterCount = nEnter; - int leaveCount = nLeave; - - top.Dispose (); - Application.Shutdown (); - - Assert.False (otherViewHasFocus); - Assert.True (viewHasFocus); - - Assert.Equal (2, enterCount); - Assert.Equal (1, leaveCount); - } - - - [Theory] - [MemberData (nameof (AllViewTypes))] - - public void AllViews_Enter_Leave_Events_Visible_False (Type viewType) - { - var view = CreateInstanceIfNotGeneric (viewType); - - if (view == null) - { - _output.WriteLine ($"Ignoring {viewType} - It's a Generic"); - return; - } - - if (!view.CanFocus) - { - _output.WriteLine ($"Ignoring {viewType} - It can't focus."); - - return; - } - - if (view is Toplevel && ((Toplevel)view).Modal) - { - _output.WriteLine ($"Ignoring {viewType} - It's a Modal Toplevel"); - - return; - } - - Application.Init (new FakeDriver ()); - - Toplevel top = new () - { - Height = 10, - Width = 10 - }; - - View otherView = new () - { - X = 0, Y = 0, - Height = 1, - Width = 1, - CanFocus = true, - }; - - view.Visible = false; - view.X = Pos.Right (otherView); - view.Y = 0; - view.Width = 10; - view.Height = 1; - - var nEnter = 0; - var nLeave = 0; - - view.Enter += (s, e) => nEnter++; - view.Leave += (s, e) => nLeave++; - - top.Add (view, otherView); - Application.Begin (top); - - // Start with the focus on our test view - view.SetFocus (); - - Assert.Equal (0, nEnter); - Assert.Equal (0, nLeave); - - // Use keyboard to navigate to next view (otherView). - if (view is TextView) - { - Application.OnKeyDown (Key.F6); - } - else if (view is DatePicker) - { - for (var i = 0; i < 4; i++) - { - Application.OnKeyDown (Key.F6); - } - } - else - { - Application.OnKeyDown (Key.Tab); - } - - Assert.Equal (0, nEnter); - Assert.Equal (0, nLeave); - - top.NewKeyDownEvent (Key.Tab); - - Assert.Equal (0, nEnter); - Assert.Equal (0, nLeave); - - top.Dispose (); - Application.Shutdown (); - } - - - [Theory] - [MemberData (nameof (AllViewTypes))] - public void AllViews_AtLeastOneNavKey_Leaves (Type viewType) - { - var view = CreateInstanceIfNotGeneric (viewType); - - if (view == null) - { - _output.WriteLine ($"Ignoring {viewType} - It's a Generic"); - return; - } - - if (!view.CanFocus) - { - _output.WriteLine ($"Ignoring {viewType} - It can't focus."); - - return; - } - - Application.Init (new FakeDriver ()); - - Toplevel top = new (); - - View otherView = new () - { - Id = "otherView", - CanFocus = true, - TabStop = view.TabStop - }; - - top.Add (view, otherView); - Application.Begin (top); - - // Start with the focus on our test view - view.SetFocus (); - - Key [] navKeys = new Key [] { Key.Tab, Key.Tab.WithShift, Key.CursorUp, Key.CursorDown, Key.CursorLeft, Key.CursorRight }; - - if (view.TabStop == TabBehavior.TabGroup) - { - navKeys = new Key [] { Key.F6, Key.F6.WithShift }; - } - - bool left = false; - - foreach (Key key in navKeys) - { - switch (view.TabStop) - { - case TabBehavior.TabStop: - case TabBehavior.NoStop: - case TabBehavior.TabGroup: - Application.OnKeyDown (key); - break; - default: - Application.OnKeyDown (Key.Tab); - - break; - } - - if (!view.HasFocus) - { - left = true; - _output.WriteLine ($"{view.GetType ().Name} - {key} Left."); - view.SetFocus(); - } - else - { - _output.WriteLine ($"{view.GetType ().Name} - {key} did not Leave."); - } - } - top.Dispose (); - Application.Shutdown (); - - Assert.True (left); - } } diff --git a/UnitTests/Views/MenuBarTests.cs b/UnitTests/Views/MenuBarTests.cs index 76598902c..e8d5ad3dc 100644 --- a/UnitTests/Views/MenuBarTests.cs +++ b/UnitTests/Views/MenuBarTests.cs @@ -358,6 +358,11 @@ public class MenuBarTests (ITestOutputHelper output) [AutoInitShutdown] public void Draw_A_Menu_Over_A_Dialog () { + // Override CM + Window.DefaultBorderStyle = LineStyle.Single; + Dialog.DefaultButtonAlignment = Alignment.Center; + Dialog.DefaultBorderStyle = LineStyle.Single; + Toplevel top = new (); var win = new Window (); top.Add (win); @@ -586,7 +591,12 @@ public class MenuBarTests (ITestOutputHelper output) [AutoInitShutdown] public void Draw_A_Menu_Over_A_Top_Dialog () { - ((FakeDriver)Application.Driver!).SetBufferSize (40, 15); + // Override CM + Window.DefaultBorderStyle = LineStyle.Single; + Dialog.DefaultButtonAlignment = Alignment.Center; + Dialog.DefaultBorderStyle = LineStyle.Single; + + ((FakeDriver)Application.Driver).SetBufferSize (40, 15); Assert.Equal (new (0, 0, 40, 15), Application.Driver?.Clip); TestHelpers.AssertDriverContentsWithFrameAre (@"", output);