diff --git a/Terminal.Gui/Application.cs b/Terminal.Gui/Application.cs index 837881b44..8973d8edb 100644 --- a/Terminal.Gui/Application.cs +++ b/Terminal.Gui/Application.cs @@ -32,6 +32,77 @@ namespace Terminal.Gui; /// TODO: Flush this out. /// public static partial class Application { + + // 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 + // guaranteeing that the state of this singleton is deterministic when Init + // starts running and after Shutdown returns. + internal static void ResetState () + { + // Shutdown is the bookend for Init. As such it needs to clean up all resources + // Init created. Apps that do any threading will need to code defensively for this. + // e.g. see Issue #537 + foreach (var t in _topLevels) { + t.Running = false; + t.Dispose (); + } + _topLevels.Clear (); + Current = null; + Top?.Dispose (); + Top = null; + + // MainLoop stuff + MainLoop?.Dispose (); + MainLoop = null; + _mainThreadId = -1; + Iteration = null; + EndAfterFirstIteration = false; + + // Driver stuff + if (Driver != null) { + Driver.SizeChanged -= Driver_SizeChanged; + Driver.KeyDown -= Driver_KeyDown; + Driver.KeyUp -= Driver_KeyUp; + Driver.MouseEvent -= Driver_MouseEvent; + Driver?.End (); + Driver = null; + } + // Don't reset ForceDriver; it needs to be set before Init is called. + //ForceDriver = string.Empty; + Force16Colors = false; + _forceFakeConsole = false; + + // Run State stuff + NotifyNewRunState = null; + NotifyStopRunState = null; + MouseGrabView = null; + _initialized = false; + + // Mouse + _mouseEnteredView = null; + WantContinuousButtonPressedView = null; + MouseEvent = null; + GrabbedMouse = null; + UnGrabbingMouse = null; + GrabbedMouse = null; + UnGrabbedMouse = null; + + // Keyboard + AlternateBackwardKey = Key.Empty; + AlternateForwardKey = Key.Empty; + QuitKey = Key.Empty; + KeyDown = null; + KeyUp = null; + SizeChanging = null; + + // Reset synchronization context to allow the user to run async/await, + // as the main loop has been ended, the synchronization context from + // gui.cs does no longer process any callbacks. See #1084 for more details: + // (https://github.com/gui-cs/Terminal.Gui/issues/1084). + SynchronizationContext.SetSynchronizationContext (syncContext: null); + } + /// /// Gets the that has been selected. See also . /// @@ -66,7 +137,7 @@ public static partial class Application { /// public static List SupportedCultures => _cachedSupportedCultures; - static List GetSupportedCultures () + internal static List GetSupportedCultures () { var culture = CultureInfo.GetCultures (CultureTypes.AllCultures); @@ -241,55 +312,6 @@ public static partial class Application { ResetState (); PrintJsonErrors (); } - - // Encapsulate all setting of initial state for Application; Having - // this in a function like this ensures we don't make mistakes in - // guaranteeing that the state of this singleton is deterministic when Init - // starts running and after Shutdown returns. - static void ResetState () - { - // Shutdown is the bookend for Init. As such it needs to clean up all resources - // Init created. Apps that do any threading will need to code defensively for this. - // e.g. see Issue #537 - foreach (var t in _topLevels) { - t.Running = false; - t.Dispose (); - } - _topLevels.Clear (); - Current = null; - Top?.Dispose (); - Top = null; - - // BUGBUG: OverlappedTop is not cleared here, but it should be? - - MainLoop?.Dispose (); - MainLoop = null; - if (Driver != null) { - Driver.SizeChanged -= Driver_SizeChanged; - Driver.KeyDown -= Driver_KeyDown; - Driver.KeyUp -= Driver_KeyUp; - Driver.MouseEvent -= Driver_MouseEvent; - Driver?.End (); - Driver = null; - } - Iteration = null; - MouseEvent = null; - KeyDown = null; - KeyUp = null; - SizeChanging = null; - _mainThreadId = -1; - NotifyNewRunState = null; - NotifyStopRunState = null; - _initialized = false; - MouseGrabView = null; - _mouseEnteredView = null; - - // Reset synchronization context to allow the user to run async/await, - // as the main loop has been ended, the synchronization context from - // gui.cs does no longer process any callbacks. See #1084 for more details: - // (https://github.com/gui-cs/Terminal.Gui/issues/1084). - SynchronizationContext.SetSynchronizationContext (syncContext: null); - } #endregion Initialization (Init/Shutdown) #region Run (Begin, Run, End, Stop) @@ -881,7 +903,7 @@ public static partial class Application { /// // BUGBUG: Techncally, this is not the full lst of TopLevels. THere be dragons hwre. E.g. see how Toplevel.Id is used. What // about TopLevels that are just a SubView of another View? - static readonly Stack _topLevels = new Stack (); + internal static readonly Stack _topLevels = new Stack (); /// /// The object used for the application on startup () @@ -1141,7 +1163,7 @@ public static partial class Application { } // Used by OnMouseEvent to track the last view that was clicked on. - static View _mouseEnteredView; + internal static View _mouseEnteredView; /// /// Event fired when a mouse move or click occurs. Coordinates are screen relative. @@ -1333,7 +1355,7 @@ public static partial class Application { #endregion Mouse handling #region Keyboard handling - static Key _alternateForwardKey = new Key (KeyCode.PageDown | KeyCode.CtrlMask); + static Key _alternateForwardKey = Key.Empty; // Defined in config.json /// /// Alternative key to navigate forwards through views. Ctrl+Tab is the primary key. @@ -1358,7 +1380,7 @@ public static partial class Application { } } - static Key _alternateBackwardKey = new Key (KeyCode.PageUp | KeyCode.CtrlMask); + static Key _alternateBackwardKey = Key.Empty; // Defined in config.json /// /// Alternative key to navigate backwards through views. Shift+Ctrl+Tab is the primary key. @@ -1383,7 +1405,7 @@ public static partial class Application { } } - static Key _quitKey = new Key (KeyCode.Q | KeyCode.CtrlMask); + static Key _quitKey = Key.Empty; // Defined in config.json /// /// Gets or sets the key to quit the application. diff --git a/Terminal.Gui/Configuration/ConfigProperty.cs b/Terminal.Gui/Configuration/ConfigProperty.cs index 7ce4e7737..8800f4e4f 100644 --- a/Terminal.Gui/Configuration/ConfigProperty.cs +++ b/Terminal.Gui/Configuration/ConfigProperty.cs @@ -82,7 +82,9 @@ public class ConfigProperty { { if (PropertyValue != null) { try { - PropertyInfo?.SetValue (null, DeepMemberwiseCopy (PropertyValue, PropertyInfo?.GetValue (null))); + if (PropertyInfo?.GetValue (null) != null) { + PropertyInfo?.SetValue (null, DeepMemberwiseCopy (PropertyValue, PropertyInfo?.GetValue (null))); + } } catch (TargetInvocationException tie) { // Check if there is an inner exception if (tie.InnerException != null) { diff --git a/Terminal.Gui/Views/ToplevelOverlapped.cs b/Terminal.Gui/Views/ToplevelOverlapped.cs index 4593f7bed..77025948c 100644 --- a/Terminal.Gui/Views/ToplevelOverlapped.cs +++ b/Terminal.Gui/Views/ToplevelOverlapped.cs @@ -42,7 +42,7 @@ public static partial class Application { /// public static Toplevel OverlappedTop { get { - if (Top.IsOverlappedContainer) { + if (Top is { IsOverlappedContainer: true }) { return Top; } return null; diff --git a/UnitTests/Application/ApplicationTests.cs b/UnitTests/Application/ApplicationTests.cs index fd93680b7..6488ae1b2 100644 --- a/UnitTests/Application/ApplicationTests.cs +++ b/UnitTests/Application/ApplicationTests.cs @@ -1,7 +1,10 @@ using System; +using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; using System.IO; using System.Linq; +using System.Reflection; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; @@ -665,6 +668,90 @@ public class ApplicationTests { Assert.False (Application.MoveToOverlappedChild (Application.Top)); } + [Fact] + public void Init_ResetState_Resets_Properties () + { + ConfigurationManager.ThrowOnJsonErrors = true; + // For all the fields/properties of Application, check that they are reset to their default values + + // Set some values + + Application.Init (); + Application._initialized = true; + + // Reset + Application.ResetState (); + + void CheckReset () + { + // Check that all fields and properties are set to their default values + + // Public Properties + Assert.Null (Application.Top); + Assert.Null (Application.Current); + Assert.Null (Application.MouseGrabView); + Assert.Null (Application.WantContinuousButtonPressedView); + // Don't check Application.ForceDriver + // Assert.Empty (Application.ForceDriver); + Assert.False (Application.Force16Colors); + Assert.Null (Application.Driver); + Assert.Null (Application.MainLoop); + Assert.False (Application.EndAfterFirstIteration); + Assert.Equal (Key.Empty, Application.AlternateBackwardKey); + Assert.Equal (Key.Empty, Application.AlternateForwardKey); + Assert.Equal (Key.Empty, Application.QuitKey); + Assert.Null (Application.OverlappedChildren); + Assert.Null (Application.OverlappedTop); + + // Internal properties + Assert.False (Application._initialized); + Assert.Equal (Application.GetSupportedCultures (), Application.SupportedCultures); + Assert.False (Application._forceFakeConsole); + Assert.Equal (-1, Application._mainThreadId); + Assert.Empty (Application._topLevels); + Assert.Null (Application._mouseEnteredView); + + // Events - Can't check + //Assert.Null (Application.NotifyNewRunState); + //Assert.Null (Application.NotifyNewRunState); + //Assert.Null (Application.Iteration); + //Assert.Null (Application.SizeChanging); + //Assert.Null (Application.GrabbedMouse); + //Assert.Null (Application.UnGrabbingMouse); + //Assert.Null (Application.GrabbedMouse); + //Assert.Null (Application.UnGrabbedMouse); + //Assert.Null (Application.MouseEvent); + //Assert.Null (Application.KeyDown); + //Assert.Null (Application.KeyUp); + } + + CheckReset (); + + // Set the values that can be set + Application._initialized = true; + Application._forceFakeConsole = true; + Application._mainThreadId = 1; + //Application._topLevels = new List (); + Application._mouseEnteredView = new View (); + //Application.SupportedCultures = new List (); + Application.Force16Colors = true; + //Application.ForceDriver = "driver"; + Application.EndAfterFirstIteration = true; + Application.AlternateBackwardKey = Key.A; + Application.AlternateForwardKey = Key.B; + Application.QuitKey = Key.C; + //Application.OverlappedChildren = new List (); + //Application.OverlappedTop = + Application._mouseEnteredView = new View (); + //Application.WantContinuousButtonPressedView = new View (); + + Application.ResetState (); + CheckReset (); + + ConfigurationManager.ThrowOnJsonErrors = false; + + } + // Invoke Tests // TODO: Test with threading scenarios [Fact] diff --git a/UnitTests/Views/StatusBarTests.cs b/UnitTests/Views/StatusBarTests.cs index 946cafd82..5e022c73e 100644 --- a/UnitTests/Views/StatusBarTests.cs +++ b/UnitTests/Views/StatusBarTests.cs @@ -14,12 +14,14 @@ namespace Terminal.Gui.ViewsTests { [Fact] public void StatusItem_Constructor () { + Application.Init (); var si = new StatusItem (Application.QuitKey, $"{Application.QuitKey} to Quit", null); Assert.Equal (KeyCode.CtrlMask | KeyCode.Q, si.Shortcut); Assert.Equal ($"{Application.QuitKey} to Quit", si.Title); Assert.Null (si.Action); si = new StatusItem (Application.QuitKey, $"{Application.QuitKey} to Quit", () => { }); Assert.NotNull (si.Action); + Application.Shutdown (); } [Fact] diff --git a/pull_request_template.md b/pull_request_template.md index 26e706a53..8834dd8c1 100644 --- a/pull_request_template.md +++ b/pull_request_template.md @@ -1,4 +1,10 @@ -Fixes #_____ - Include a terse summary of the change or which issue is fixed. +## Fixes + +- Fixes #_____ + +## Proposed Changes/Todos + +- [ ] Todo 1 ## Pull Request checklist: