From f328f0c93fcd40b04f1cbddbd32d57a286349f69 Mon Sep 17 00:00:00 2001 From: Tig Date: Sun, 21 Jul 2024 16:52:18 -0600 Subject: [PATCH 01/33] Initial commit. --- UnitTests/Views/OverlappedTests.cs | 187 ++++++++++++++++++++++++++++ UnitTests/Views/ToplevelTests.cs | 189 +---------------------------- 2 files changed, 188 insertions(+), 188 deletions(-) diff --git a/UnitTests/Views/OverlappedTests.cs b/UnitTests/Views/OverlappedTests.cs index b4437b89d..9c7202d68 100644 --- a/UnitTests/Views/OverlappedTests.cs +++ b/UnitTests/Views/OverlappedTests.cs @@ -1015,4 +1015,191 @@ public class OverlappedTests { public Overlapped () { IsOverlappedContainer = true; } } + + [Fact] + [AutoInitShutdown] + public void KeyBindings_Command_With_OverlappedTop () + { + Toplevel top = new (); + Assert.Null (Application.OverlappedTop); + top.IsOverlappedContainer = true; + Application.Begin (top); + Assert.Equal (Application.Top, Application.OverlappedTop); + + var isRunning = true; + + var win1 = new Window { Id = "win1", Width = Dim.Percent (50), Height = Dim.Fill () }; + var lblTf1W1 = new Label { Text = "Enter text in TextField on Win1:" }; + var tf1W1 = new TextField { X = Pos.Right (lblTf1W1) + 1, Width = Dim.Fill (), Text = "Text1 on Win1" }; + var lblTvW1 = new Label { Y = Pos.Bottom (lblTf1W1) + 1, Text = "Enter text in TextView on Win1:" }; + + var tvW1 = new TextView + { + X = Pos.Left (tf1W1), Width = Dim.Fill (), Height = 2, Text = "First line Win1\nSecond line Win1" + }; + var lblTf2W1 = new Label { Y = Pos.Bottom (lblTvW1) + 1, Text = "Enter text in TextField on Win1:" }; + var tf2W1 = new TextField { X = Pos.Left (tf1W1), Width = Dim.Fill (), Text = "Text2 on Win1" }; + win1.Add (lblTf1W1, tf1W1, lblTvW1, tvW1, lblTf2W1, tf2W1); + + var win2 = new Window { Id = "win2", Width = Dim.Percent (50), Height = Dim.Fill () }; + var lblTf1W2 = new Label { Text = "Enter text in TextField on Win2:" }; + var tf1W2 = new TextField { X = Pos.Right (lblTf1W2) + 1, Width = Dim.Fill (), Text = "Text1 on Win2" }; + var lblTvW2 = new Label { Y = Pos.Bottom (lblTf1W2) + 1, Text = "Enter text in TextView on Win2:" }; + + var tvW2 = new TextView + { + X = Pos.Left (tf1W2), Width = Dim.Fill (), Height = 2, Text = "First line Win1\nSecond line Win2" + }; + var lblTf2W2 = new Label { Y = Pos.Bottom (lblTvW2) + 1, Text = "Enter text in TextField on Win2:" }; + var tf2W2 = new TextField { X = Pos.Left (tf1W2), Width = Dim.Fill (), Text = "Text2 on Win2" }; + win2.Add (lblTf1W2, tf1W2, lblTvW2, tvW2, lblTf2W2, tf2W2); + + win1.Closing += (s, e) => isRunning = false; + Assert.Null (top.Focused); + Assert.Equal (top, Application.Current); + Assert.True (top.IsCurrentTop); + Assert.Equal (top, Application.OverlappedTop); + Application.Begin (win1); + Assert.Equal (new (0, 0, 40, 25), win1.Frame); + Assert.NotEqual (top, Application.Current); + Assert.False (top.IsCurrentTop); + Assert.Equal (win1, Application.Current); + Assert.True (win1.IsCurrentTop); + Assert.True (win1.IsOverlapped); + Assert.Null (top.Focused); + Assert.Null (top.MostFocused); + Assert.Equal (tf1W1, win1.MostFocused); + Assert.True (win1.IsOverlapped); + Assert.Single (Application.OverlappedChildren); + Application.Begin (win2); + Assert.Equal (new (0, 0, 40, 25), win2.Frame); + Assert.NotEqual (top, Application.Current); + Assert.False (top.IsCurrentTop); + Assert.Equal (win2, Application.Current); + Assert.True (win2.IsCurrentTop); + Assert.True (win2.IsOverlapped); + Assert.Null (top.Focused); + Assert.Null (top.MostFocused); + Assert.Equal (tf1W2, win2.MostFocused); + Assert.Equal (2, Application.OverlappedChildren.Count); + + Application.MoveToOverlappedChild (win1); + Assert.Equal (win1, Application.Current); + Assert.Equal (win1, Application.OverlappedChildren [0]); + win1.Running = true; + Assert.True (Application.OnKeyDown (Application.QuitKey)); + Assert.False (isRunning); + Assert.False (win1.Running); + Assert.Equal (win1, Application.OverlappedChildren [0]); + + Assert.True ( + Application.OnKeyDown (Key.Z.WithCtrl) + ); + + Assert.True (Application.OnKeyDown (Key.F5)); // refresh + + Assert.True (Application.OnKeyDown (Key.Tab)); + Assert.True (win1.IsCurrentTop); + Assert.Equal (tvW1, win1.MostFocused); + Assert.True (Application.OnKeyDown (Key.Tab)); + Assert.Equal ($"\tFirst line Win1{Environment.NewLine}Second line Win1", tvW1.Text); + + Assert.True ( + Application.OnKeyDown (Key.Tab.WithShift) + ); + Assert.Equal ($"First line Win1{Environment.NewLine}Second line Win1", tvW1.Text); + + Assert.True ( + Application.OnKeyDown (Key.Tab.WithCtrl) + ); + Assert.Equal (win1, Application.OverlappedChildren [0]); + Assert.Equal (tf2W1, win1.MostFocused); + Assert.True (Application.OnKeyDown (Key.Tab)); + Assert.Equal (win1, Application.OverlappedChildren [0]); + Assert.Equal (tf1W1, win1.MostFocused); + Assert.True (Application.OnKeyDown (Key.CursorRight)); + Assert.Equal (win1, Application.OverlappedChildren [0]); + Assert.Equal (tf1W1, win1.MostFocused); + Assert.True (Application.OnKeyDown (Key.CursorDown)); + Assert.Equal (win1, Application.OverlappedChildren [0]); + Assert.Equal (tvW1, win1.MostFocused); +#if UNIX_KEY_BINDINGS + Assert.True (Application.OverlappedChildren [0].ProcessKeyDown (new (Key.I.WithCtrl))); + Assert.Equal (win1, Application.OverlappedChildren [0]); + Assert.Equal (tf2W1, win1.MostFocused); +#endif + Assert.True ( + Application.OverlappedChildren [0] + .NewKeyDownEvent (Key.Tab.WithShift) + ); + Assert.Equal (win1, Application.OverlappedChildren [0]); + Assert.Equal (tvW1, win1.MostFocused); + Assert.True (Application.OnKeyDown (Key.CursorLeft)); + Assert.Equal (win1, Application.OverlappedChildren [0]); + Assert.Equal (tf1W1, win1.MostFocused); + Assert.True (Application.OnKeyDown (Key.CursorUp)); + Assert.Equal (win1, Application.OverlappedChildren [0]); + Assert.Equal (tf2W1, win1.MostFocused); + Assert.True (Application.OnKeyDown (Key.Tab)); + Assert.Equal (win1, Application.OverlappedChildren [0]); + Assert.Equal (tf1W1, win1.MostFocused); + + Assert.True ( + Application.OverlappedChildren [0] + .NewKeyDownEvent (Key.Tab.WithCtrl) + ); + Assert.Equal (win2, Application.OverlappedChildren [0]); + Assert.Equal (tf1W2, win2.MostFocused); + tf2W2.SetFocus (); + Assert.True (tf2W2.HasFocus); + + Assert.True ( + Application.OverlappedChildren [0] + .NewKeyDownEvent (Key.Tab.WithCtrl.WithShift) + ); + Assert.Equal (win1, Application.OverlappedChildren [0]); + Assert.Equal (tf1W1, win1.MostFocused); + Assert.True (Application.OnKeyDown (Application.AlternateForwardKey)); + Assert.Equal (win2, Application.OverlappedChildren [0]); + Assert.Equal (tf2W2, win2.MostFocused); + Assert.True (Application.OnKeyDown (Application.AlternateBackwardKey)); + Assert.Equal (win1, Application.OverlappedChildren [0]); + Assert.Equal (tf1W1, win1.MostFocused); + Assert.True (Application.OnKeyDown (Key.CursorDown)); + Assert.Equal (win1, Application.OverlappedChildren [0]); + Assert.Equal (tvW1, win1.MostFocused); +#if UNIX_KEY_BINDINGS + Assert.True (Application.OverlappedChildren [0].ProcessKeyDown (new (Key.B.WithCtrl))); +#else + Assert.True (Application.OnKeyDown (Key.CursorLeft)); +#endif + Assert.Equal (win1, Application.OverlappedChildren [0]); + Assert.Equal (tf1W1, win1.MostFocused); + Assert.True (Application.OnKeyDown (Key.CursorDown)); + Assert.Equal (win1, Application.OverlappedChildren [0]); + Assert.Equal (tvW1, win1.MostFocused); + Assert.Equal (Point.Empty, tvW1.CursorPosition); + + Assert.True ( + Application.OverlappedChildren [0] + .NewKeyDownEvent (Key.End.WithCtrl) + ); + Assert.Equal (win1, Application.OverlappedChildren [0]); + Assert.Equal (tvW1, win1.MostFocused); + Assert.Equal (new (16, 1), tvW1.CursorPosition); +#if UNIX_KEY_BINDINGS + Assert.True (Application.OverlappedChildren [0].ProcessKeyDown (new (Key.F.WithCtrl))); +#else + Assert.True (Application.OnKeyDown (Key.CursorRight)); +#endif + Assert.Equal (win1, Application.OverlappedChildren [0]); + Assert.Equal (tf2W1, win1.MostFocused); + +#if UNIX_KEY_BINDINGS + Assert.True (Application.OverlappedChildren [0].ProcessKeyDown (new (Key.L.WithCtrl))); +#endif + win2.Dispose (); + win1.Dispose (); + top.Dispose (); + } } diff --git a/UnitTests/Views/ToplevelTests.cs b/UnitTests/Views/ToplevelTests.cs index ed3482a50..abbfdeb59 100644 --- a/UnitTests/Views/ToplevelTests.cs +++ b/UnitTests/Views/ToplevelTests.cs @@ -2,7 +2,7 @@ namespace Terminal.Gui.ViewsTests; -public class ToplevelTests (ITestOutputHelper output) +public partial class ToplevelTests (ITestOutputHelper output) { [Fact] public void Constructor_Default () @@ -556,193 +556,6 @@ public class ToplevelTests (ITestOutputHelper output) top.Dispose (); } - [Fact] - [AutoInitShutdown] - public void KeyBindings_Command_With_OverlappedTop () - { - Toplevel top = new (); - Assert.Null (Application.OverlappedTop); - top.IsOverlappedContainer = true; - Application.Begin (top); - Assert.Equal (Application.Top, Application.OverlappedTop); - - var isRunning = true; - - var win1 = new Window { Id = "win1", Width = Dim.Percent (50), Height = Dim.Fill () }; - var lblTf1W1 = new Label { Text = "Enter text in TextField on Win1:" }; - var tf1W1 = new TextField { X = Pos.Right (lblTf1W1) + 1, Width = Dim.Fill (), Text = "Text1 on Win1" }; - var lblTvW1 = new Label { Y = Pos.Bottom (lblTf1W1) + 1, Text = "Enter text in TextView on Win1:" }; - - var tvW1 = new TextView - { - X = Pos.Left (tf1W1), Width = Dim.Fill (), Height = 2, Text = "First line Win1\nSecond line Win1" - }; - var lblTf2W1 = new Label { Y = Pos.Bottom (lblTvW1) + 1, Text = "Enter text in TextField on Win1:" }; - var tf2W1 = new TextField { X = Pos.Left (tf1W1), Width = Dim.Fill (), Text = "Text2 on Win1" }; - win1.Add (lblTf1W1, tf1W1, lblTvW1, tvW1, lblTf2W1, tf2W1); - - var win2 = new Window { Id = "win2", Width = Dim.Percent (50), Height = Dim.Fill () }; - var lblTf1W2 = new Label { Text = "Enter text in TextField on Win2:" }; - var tf1W2 = new TextField { X = Pos.Right (lblTf1W2) + 1, Width = Dim.Fill (), Text = "Text1 on Win2" }; - var lblTvW2 = new Label { Y = Pos.Bottom (lblTf1W2) + 1, Text = "Enter text in TextView on Win2:" }; - - var tvW2 = new TextView - { - X = Pos.Left (tf1W2), Width = Dim.Fill (), Height = 2, Text = "First line Win1\nSecond line Win2" - }; - var lblTf2W2 = new Label { Y = Pos.Bottom (lblTvW2) + 1, Text = "Enter text in TextField on Win2:" }; - var tf2W2 = new TextField { X = Pos.Left (tf1W2), Width = Dim.Fill (), Text = "Text2 on Win2" }; - win2.Add (lblTf1W2, tf1W2, lblTvW2, tvW2, lblTf2W2, tf2W2); - - win1.Closing += (s, e) => isRunning = false; - Assert.Null (top.Focused); - Assert.Equal (top, Application.Current); - Assert.True (top.IsCurrentTop); - Assert.Equal (top, Application.OverlappedTop); - Application.Begin (win1); - Assert.Equal (new (0, 0, 40, 25), win1.Frame); - Assert.NotEqual (top, Application.Current); - Assert.False (top.IsCurrentTop); - Assert.Equal (win1, Application.Current); - Assert.True (win1.IsCurrentTop); - Assert.True (win1.IsOverlapped); - Assert.Null (top.Focused); - Assert.Null (top.MostFocused); - Assert.Equal (tf1W1, win1.MostFocused); - Assert.True (win1.IsOverlapped); - Assert.Single (Application.OverlappedChildren); - Application.Begin (win2); - Assert.Equal (new (0, 0, 40, 25), win2.Frame); - Assert.NotEqual (top, Application.Current); - Assert.False (top.IsCurrentTop); - Assert.Equal (win2, Application.Current); - Assert.True (win2.IsCurrentTop); - Assert.True (win2.IsOverlapped); - Assert.Null (top.Focused); - Assert.Null (top.MostFocused); - Assert.Equal (tf1W2, win2.MostFocused); - Assert.Equal (2, Application.OverlappedChildren.Count); - - Application.MoveToOverlappedChild (win1); - Assert.Equal (win1, Application.Current); - Assert.Equal (win1, Application.OverlappedChildren [0]); - win1.Running = true; - Assert.True (Application.OnKeyDown (Application.QuitKey)); - Assert.False (isRunning); - Assert.False (win1.Running); - Assert.Equal (win1, Application.OverlappedChildren [0]); - - Assert.True ( - Application.OnKeyDown (Key.Z.WithCtrl) - ); - - Assert.True (Application.OnKeyDown (Key.F5)); // refresh - - Assert.True (Application.OnKeyDown (Key.Tab)); - Assert.True (win1.IsCurrentTop); - Assert.Equal (tvW1, win1.MostFocused); - Assert.True (Application.OnKeyDown (Key.Tab)); - Assert.Equal ($"\tFirst line Win1{Environment.NewLine}Second line Win1", tvW1.Text); - - Assert.True ( - Application.OnKeyDown (Key.Tab.WithShift) - ); - Assert.Equal ($"First line Win1{Environment.NewLine}Second line Win1", tvW1.Text); - - Assert.True ( - Application.OnKeyDown (Key.Tab.WithCtrl) - ); - Assert.Equal (win1, Application.OverlappedChildren [0]); - Assert.Equal (tf2W1, win1.MostFocused); - Assert.True (Application.OnKeyDown (Key.Tab)); - Assert.Equal (win1, Application.OverlappedChildren [0]); - Assert.Equal (tf1W1, win1.MostFocused); - Assert.True (Application.OnKeyDown (Key.CursorRight)); - Assert.Equal (win1, Application.OverlappedChildren [0]); - Assert.Equal (tf1W1, win1.MostFocused); - Assert.True (Application.OnKeyDown (Key.CursorDown)); - Assert.Equal (win1, Application.OverlappedChildren [0]); - Assert.Equal (tvW1, win1.MostFocused); -#if UNIX_KEY_BINDINGS - Assert.True (Application.OverlappedChildren [0].ProcessKeyDown (new (Key.I.WithCtrl))); - Assert.Equal (win1, Application.OverlappedChildren [0]); - Assert.Equal (tf2W1, win1.MostFocused); -#endif - Assert.True ( - Application.OverlappedChildren [0] - .NewKeyDownEvent (Key.Tab.WithShift) - ); - Assert.Equal (win1, Application.OverlappedChildren [0]); - Assert.Equal (tvW1, win1.MostFocused); - Assert.True (Application.OnKeyDown (Key.CursorLeft)); - Assert.Equal (win1, Application.OverlappedChildren [0]); - Assert.Equal (tf1W1, win1.MostFocused); - Assert.True (Application.OnKeyDown (Key.CursorUp)); - Assert.Equal (win1, Application.OverlappedChildren [0]); - Assert.Equal (tf2W1, win1.MostFocused); - Assert.True (Application.OnKeyDown (Key.Tab)); - Assert.Equal (win1, Application.OverlappedChildren [0]); - Assert.Equal (tf1W1, win1.MostFocused); - - Assert.True ( - Application.OverlappedChildren [0] - .NewKeyDownEvent (Key.Tab.WithCtrl) - ); - Assert.Equal (win2, Application.OverlappedChildren [0]); - Assert.Equal (tf1W2, win2.MostFocused); - tf2W2.SetFocus (); - Assert.True (tf2W2.HasFocus); - - Assert.True ( - Application.OverlappedChildren [0] - .NewKeyDownEvent (Key.Tab.WithCtrl.WithShift) - ); - Assert.Equal (win1, Application.OverlappedChildren [0]); - Assert.Equal (tf1W1, win1.MostFocused); - Assert.True (Application.OnKeyDown (Application.AlternateForwardKey)); - Assert.Equal (win2, Application.OverlappedChildren [0]); - Assert.Equal (tf2W2, win2.MostFocused); - Assert.True (Application.OnKeyDown (Application.AlternateBackwardKey)); - Assert.Equal (win1, Application.OverlappedChildren [0]); - Assert.Equal (tf1W1, win1.MostFocused); - Assert.True (Application.OnKeyDown (Key.CursorDown)); - Assert.Equal (win1, Application.OverlappedChildren [0]); - Assert.Equal (tvW1, win1.MostFocused); -#if UNIX_KEY_BINDINGS - Assert.True (Application.OverlappedChildren [0].ProcessKeyDown (new (Key.B.WithCtrl))); -#else - Assert.True (Application.OnKeyDown (Key.CursorLeft)); -#endif - Assert.Equal (win1, Application.OverlappedChildren [0]); - Assert.Equal (tf1W1, win1.MostFocused); - Assert.True (Application.OnKeyDown (Key.CursorDown)); - Assert.Equal (win1, Application.OverlappedChildren [0]); - Assert.Equal (tvW1, win1.MostFocused); - Assert.Equal (Point.Empty, tvW1.CursorPosition); - - Assert.True ( - Application.OverlappedChildren [0] - .NewKeyDownEvent (Key.End.WithCtrl) - ); - Assert.Equal (win1, Application.OverlappedChildren [0]); - Assert.Equal (tvW1, win1.MostFocused); - Assert.Equal (new (16, 1), tvW1.CursorPosition); -#if UNIX_KEY_BINDINGS - Assert.True (Application.OverlappedChildren [0].ProcessKeyDown (new (Key.F.WithCtrl))); -#else - Assert.True (Application.OnKeyDown (Key.CursorRight)); -#endif - Assert.Equal (win1, Application.OverlappedChildren [0]); - Assert.Equal (tf2W1, win1.MostFocused); - -#if UNIX_KEY_BINDINGS - Assert.True (Application.OverlappedChildren [0].ProcessKeyDown (new (Key.L.WithCtrl))); -#endif - win2.Dispose (); - win1.Dispose (); - top.Dispose (); - } - [Fact] public void Added_Event_Should_Not_Be_Used_To_Initialize_Toplevel_Events () { From 44ce74a5c0e2debadb3771e24ca6452018850771 Mon Sep 17 00:00:00 2001 From: Tig Date: Mon, 22 Jul 2024 16:52:02 -0600 Subject: [PATCH 02/33] Refactored Application into smaller files. Made Application #nullable enable --- .../Application/Application.Driver.cs | 29 + .../Application/Application.Initialization.cs | 209 +++ ...ionKeyboard.cs => Application.Keyboard.cs} | 2 +- ...plicationMouse.cs => Application.Mouse.cs} | 4 +- Terminal.Gui/Application/Application.Run.cs | 863 +++++++++++++ Terminal.Gui/Application/Application.cs | 1117 +---------------- Terminal.Gui/Clipboard/Clipboard.cs | 33 +- Terminal.Gui/Drawing/LineCanvas.cs | 6 +- Terminal.Gui/Drawing/Ruler.cs | 8 +- Terminal.Gui/Drawing/Thickness.cs | 14 +- .../Text/Autocomplete/AppendAutocomplete.cs | 4 +- .../Text/Autocomplete/PopupAutocomplete.cs | 6 +- Terminal.Gui/View/Layout/Dim.cs | 4 +- Terminal.Gui/View/Layout/DimView.cs | 4 +- Terminal.Gui/View/Layout/Pos.cs | 12 +- Terminal.Gui/View/Layout/PosView.cs | 4 +- Terminal.Gui/View/Layout/ViewLayout.cs | 4 +- Terminal.Gui/View/ViewDrawing.cs | 8 +- Terminal.Gui/Views/GraphView/Annotations.cs | 6 +- Terminal.Gui/Views/GraphView/Axis.cs | 14 +- Terminal.Gui/Views/GraphView/Series.cs | 4 +- Terminal.Gui/Views/Menu/ContextMenu.cs | 2 +- Terminal.Gui/Views/RadioGroup.cs | 12 +- UICatalog/Scenarios/CombiningMarks.cs | 28 +- UICatalog/Scenarios/Images.cs | 4 +- UICatalog/Scenarios/SendKeys.cs | 2 +- UICatalog/Scenarios/TextEffectsScenario.cs | 2 +- UICatalog/Scenarios/TrueColors.cs | 4 +- UICatalog/Scenarios/VkeyPacketSimulator.cs | 2 +- UICatalog/UICatalog.cs | 4 +- UnitTests/Application/ApplicationTests.cs | 14 +- UnitTests/Application/CursorTests.cs | 7 +- UnitTests/Clipboard/ClipboardTests.cs | 4 +- UnitTests/ConsoleDrivers/ClipRegionTests.cs | 8 +- .../ConsoleDrivers/ConsoleDriverTests.cs | 2 +- .../ConsoleDrivers/ConsoleKeyMappingTests.cs | 2 +- UnitTests/Dialogs/MessageBoxTests.cs | 20 +- UnitTests/Drawing/RulerTests.cs | 10 +- UnitTests/Drawing/ThicknessTests.cs | 20 +- UnitTests/FileServices/FileDialogTests.cs | 4 +- UnitTests/Text/TextFormatterTests.cs | 4 +- UnitTests/View/Adornment/BorderTests.cs | 16 +- UnitTests/View/Adornment/MarginTests.cs | 2 +- UnitTests/View/Adornment/PaddingTests.cs | 2 +- UnitTests/View/DrawTests.cs | 46 +- UnitTests/View/Layout/Dim.FillTests.cs | 2 +- UnitTests/View/Layout/Pos.AnchorEndTests.cs | 2 +- UnitTests/View/Layout/Pos.CenterTests.cs | 4 +- UnitTests/View/Layout/ViewportTests.cs | 2 +- UnitTests/View/NavigationTests.cs | 14 +- UnitTests/View/TextTests.cs | 20 +- UnitTests/View/ViewTests.cs | 30 +- UnitTests/Views/AppendAutocompleteTests.cs | 36 +- UnitTests/Views/ButtonTests.cs | 6 +- UnitTests/Views/CheckBoxTests.cs | 8 +- UnitTests/Views/ContextMenuTests.cs | 18 +- UnitTests/Views/FrameViewTests.cs | 2 +- UnitTests/Views/LabelTests.cs | 26 +- UnitTests/Views/ListViewTests.cs | 4 +- UnitTests/Views/MenuBarTests.cs | 28 +- UnitTests/Views/OverlappedTests.cs | 2 +- UnitTests/Views/RadioGroupTests.cs | 2 +- UnitTests/Views/ScrollBarViewTests.cs | 12 +- UnitTests/Views/ScrollViewTests.cs | 2 +- UnitTests/Views/TableViewTests.cs | 2 +- UnitTests/Views/TextFieldTests.cs | 14 +- UnitTests/Views/TextViewTests.cs | 18 +- UnitTests/Views/ToplevelTests.cs | 48 +- UnitTests/Views/TreeTableSourceTests.cs | 4 +- UnitTests/Views/TreeViewTests.cs | 2 +- UnitTests/Views/WindowTests.cs | 6 +- 71 files changed, 1449 insertions(+), 1441 deletions(-) create mode 100644 Terminal.Gui/Application/Application.Driver.cs create mode 100644 Terminal.Gui/Application/Application.Initialization.cs rename Terminal.Gui/Application/{ApplicationKeyboard.cs => Application.Keyboard.cs} (99%) rename Terminal.Gui/Application/{ApplicationMouse.cs => Application.Mouse.cs} (98%) create mode 100644 Terminal.Gui/Application/Application.Run.cs diff --git a/Terminal.Gui/Application/Application.Driver.cs b/Terminal.Gui/Application/Application.Driver.cs new file mode 100644 index 000000000..f15bd8053 --- /dev/null +++ b/Terminal.Gui/Application/Application.Driver.cs @@ -0,0 +1,29 @@ +#nullable enable +namespace Terminal.Gui; + +public static partial class Application // Driver abstractions +{ + internal static bool _forceFakeConsole; + + /// Gets the that has been selected. See also . + public static ConsoleDriver? Driver { get; internal set; } + + /// + /// Gets or sets whether will be forced to output only the 16 colors defined in + /// . The default is , meaning 24-bit (TrueColor) colors will be output + /// as long as the selected supports TrueColor. + /// + [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] + public static bool Force16Colors { get; set; } + + /// + /// Forces the use of the specified driver (one of "fake", "ansi", "curses", "net", or "windows"). If not + /// specified, the driver is selected based on the platform. + /// + /// + /// Note, will override this configuration setting if called + /// with either `driver` or `driverName` specified. + /// + [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] + public static string ForceDriver { get; set; } = string.Empty; +} diff --git a/Terminal.Gui/Application/Application.Initialization.cs b/Terminal.Gui/Application/Application.Initialization.cs new file mode 100644 index 000000000..a971850e3 --- /dev/null +++ b/Terminal.Gui/Application/Application.Initialization.cs @@ -0,0 +1,209 @@ +using System.Diagnostics.CodeAnalysis; +using System.Reflection; + +namespace Terminal.Gui; + +public static partial class Application // Initialization (Init/Shutdown) +{ + /// Initializes a new instance of Application. + /// Call this method once per instance (or after has been called). + /// + /// This function loads the right for the platform, Creates a . and + /// assigns it to + /// + /// + /// must be called when the application is closing (typically after + /// has returned) to ensure resources are cleaned up and + /// terminal settings + /// restored. + /// + /// + /// The function combines + /// and + /// into a single + /// call. An application cam use without explicitly calling + /// . + /// + /// + /// The to use. If neither or + /// are specified the default driver for the platform will be used. + /// + /// + /// The short name (e.g. "net", "windows", "ansi", "fake", or "curses") of the + /// to use. If neither or are + /// specified the default driver for the platform will be used. + /// + [RequiresUnreferencedCode ("AOT")] + [RequiresDynamicCode ("AOT")] + public static void Init (ConsoleDriver driver = null, string driverName = null) { InternalInit (driver, driverName); } + + internal static bool _initialized; + internal static int _mainThreadId = -1; + + + // INTERNAL function for initializing an app with a Toplevel factory object, driver, and mainloop. + // + // Called from: + // + // Init() - When the user wants to use the default Toplevel. calledViaRunT will be false, causing all state to be reset. + // Run() - When the user wants to use a custom Toplevel. calledViaRunT will be true, enabling Run() to be called without calling Init first. + // Unit Tests - To initialize the app with a custom Toplevel, using the FakeDriver. calledViaRunT will be false, causing all state to be reset. + // + // calledViaRunT: If false (default) all state will be reset. If true the state will not be reset. + [RequiresUnreferencedCode ("AOT")] + [RequiresDynamicCode ("AOT")] + internal static void InternalInit ( + ConsoleDriver driver = null, + string driverName = null, + bool calledViaRunT = false + ) + { + if (_initialized && driver is null) + { + return; + } + + if (_initialized) + { + throw new InvalidOperationException ("Init has already been called and must be bracketed by Shutdown."); + } + + if (!calledViaRunT) + { + // Reset all class variables (Application is a singleton). + ResetState (); + } + + // For UnitTests + if (driver is { }) + { + Driver = driver; + } + + // Start the process of configuration management. + // Note that we end up calling LoadConfigurationFromAllSources + // multiple times. We need to do this because some settings are only + // valid after a Driver is loaded. In this case we need just + // `Settings` so we can determine which driver to use. + // Don't reset, so we can inherit the theme from the previous run. + Load (); + Apply (); + + // Ignore Configuration for ForceDriver if driverName is specified + if (!string.IsNullOrEmpty (driverName)) + { + ForceDriver = driverName; + } + + if (Driver is null) + { + PlatformID p = Environment.OSVersion.Platform; + + if (string.IsNullOrEmpty (ForceDriver)) + { + if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows) + { + Driver = new WindowsDriver (); + } + else + { + Driver = new CursesDriver (); + } + } + else + { + List drivers = GetDriverTypes (); + Type driverType = drivers.FirstOrDefault (t => t.Name.Equals (ForceDriver, StringComparison.InvariantCultureIgnoreCase)); + + if (driverType is { }) + { + Driver = (ConsoleDriver)Activator.CreateInstance (driverType); + } + else + { + throw new ArgumentException ( + $"Invalid driver name: {ForceDriver}. Valid names are {string.Join (", ", drivers.Select (t => t.Name))}" + ); + } + } + } + + try + { + MainLoop = Driver.Init (); + } + catch (InvalidOperationException ex) + { + // This is a case where the driver is unable to initialize the console. + // This can happen if the console is already in use by another process or + // if running in unit tests. + // In this case, we want to throw a more specific exception. + throw new InvalidOperationException ( + "Unable to initialize the console. This can happen if the console is already in use by another process or in unit tests.", + ex + ); + } + + Driver.SizeChanged += (s, args) => OnSizeChanging (args); + Driver.KeyDown += (s, args) => OnKeyDown (args); + Driver.KeyUp += (s, args) => OnKeyUp (args); + Driver.MouseEvent += (s, args) => OnMouseEvent (args); + + SynchronizationContext.SetSynchronizationContext (new MainLoopSyncContext ()); + + SupportedCultures = GetSupportedCultures (); + _mainThreadId = Thread.CurrentThread.ManagedThreadId; + _initialized = true; + InitializedChanged?.Invoke (null, new (in _initialized)); + } + + private static void Driver_SizeChanged (object sender, SizeChangedEventArgs e) { OnSizeChanging (e); } + private static void Driver_KeyDown (object sender, Key e) { OnKeyDown (e); } + private static void Driver_KeyUp (object sender, Key e) { OnKeyUp (e); } + private static void Driver_MouseEvent (object sender, MouseEvent e) { OnMouseEvent (e); } + + /// Gets of list of types that are available. + /// + [RequiresUnreferencedCode ("AOT")] + public static List GetDriverTypes () + { + // use reflection to get the list of drivers + List driverTypes = new (); + + foreach (Assembly asm in AppDomain.CurrentDomain.GetAssemblies ()) + { + foreach (Type type in asm.GetTypes ()) + { + if (type.IsSubclassOf (typeof (ConsoleDriver)) && !type.IsAbstract) + { + driverTypes.Add (type); + } + } + } + + return driverTypes; + } + + /// Shutdown an application initialized with . + /// + /// Shutdown must be called for every call to or + /// to ensure all resources are cleaned + /// up (Disposed) + /// and terminal settings are restored. + /// + public static void Shutdown () + { + // TODO: Throw an exception if Init hasn't been called. + ResetState (); + PrintJsonErrors (); + InitializedChanged?.Invoke (null, new (in _initialized)); + } + + /// + /// This event is raised after the and methods have been called. + /// + /// + /// Intended to support unit tests that need to know when the application has been initialized. + /// + public static event EventHandler> InitializedChanged; +} diff --git a/Terminal.Gui/Application/ApplicationKeyboard.cs b/Terminal.Gui/Application/Application.Keyboard.cs similarity index 99% rename from Terminal.Gui/Application/ApplicationKeyboard.cs rename to Terminal.Gui/Application/Application.Keyboard.cs index be737968f..e8d698286 100644 --- a/Terminal.Gui/Application/ApplicationKeyboard.cs +++ b/Terminal.Gui/Application/Application.Keyboard.cs @@ -2,7 +2,7 @@ namespace Terminal.Gui; -partial class Application +public static partial class Application // Keyboard handling { private static Key _alternateForwardKey = Key.Empty; // Defined in config.json diff --git a/Terminal.Gui/Application/ApplicationMouse.cs b/Terminal.Gui/Application/Application.Mouse.cs similarity index 98% rename from Terminal.Gui/Application/ApplicationMouse.cs rename to Terminal.Gui/Application/Application.Mouse.cs index 9f2a95339..4d3fb6129 100644 --- a/Terminal.Gui/Application/ApplicationMouse.cs +++ b/Terminal.Gui/Application/Application.Mouse.cs @@ -1,6 +1,6 @@ namespace Terminal.Gui; -partial class Application +public static partial class Application // Mouse handling { #region Mouse handling @@ -272,7 +272,7 @@ partial class Application if (view is Adornment adornmentView) { - view = adornmentView.Parent.SuperView; + view = adornmentView.Parent!.SuperView; } else { diff --git a/Terminal.Gui/Application/Application.Run.cs b/Terminal.Gui/Application/Application.Run.cs new file mode 100644 index 000000000..34189d9fc --- /dev/null +++ b/Terminal.Gui/Application/Application.Run.cs @@ -0,0 +1,863 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace Terminal.Gui; + +public static partial class Application // Run (Begin, Run, End, Stop) +{ + private static Toplevel _cachedRunStateToplevel; + + /// + /// Notify that a new was created ( was called). The token is + /// created in and this event will be fired before that function exits. + /// + /// + /// If is callers to + /// must also subscribe to and manually dispose of the token + /// when the application is done. + /// + public static event EventHandler NotifyNewRunState; + + /// Notify that an existent is stopping ( was called). + /// + /// If is callers to + /// must also subscribe to and manually dispose of the token + /// when the application is done. + /// + public static event EventHandler NotifyStopRunState; + + /// Building block API: Prepares the provided for execution. + /// + /// The handle that needs to be passed to the method upon + /// completion. + /// + /// The to prepare execution for. + /// + /// This method prepares the provided for running with the focus, it adds this to the list + /// of s, lays out the Subviews, focuses the first element, and draws the + /// in the screen. This is usually followed by executing the method, and then the + /// method upon termination which will undo these changes. + /// + public static RunState Begin (Toplevel toplevel) + { + ArgumentNullException.ThrowIfNull (toplevel); + +#if DEBUG_IDISPOSABLE + Debug.Assert (!toplevel.WasDisposed); + + if (_cachedRunStateToplevel is { } && _cachedRunStateToplevel != toplevel) + { + Debug.Assert (_cachedRunStateToplevel.WasDisposed); + } +#endif + + if (toplevel.IsOverlappedContainer && OverlappedTop != toplevel && OverlappedTop is { }) + { + throw new InvalidOperationException ("Only one Overlapped Container is allowed."); + } + + // Ensure the mouse is ungrabbed. + MouseGrabView = null; + + var rs = new RunState (toplevel); + + // View implements ISupportInitializeNotification which is derived from ISupportInitialize + if (!toplevel.IsInitialized) + { + toplevel.BeginInit (); + toplevel.EndInit (); + } + +#if DEBUG_IDISPOSABLE + if (Top is { } && toplevel != Top && !_topLevels.Contains (Top)) + { + // This assertion confirm if the Top was already disposed + Debug.Assert (Top.WasDisposed); + Debug.Assert (Top == _cachedRunStateToplevel); + } +#endif + + lock (_topLevels) + { + if (Top is { } && toplevel != Top && !_topLevels.Contains (Top)) + { + // If Top was already disposed and isn't on the Toplevels Stack, + // clean it up here if is the same as _cachedRunStateToplevel + if (Top == _cachedRunStateToplevel) + { + Top = null; + } + else + { + // Probably this will never hit + throw new ObjectDisposedException (Top.GetType ().FullName); + } + } + else if (OverlappedTop is { } && toplevel != Top && _topLevels.Contains (Top)) + { + Top.OnLeave (toplevel); + } + + // BUGBUG: We should not depend on `Id` internally. + // BUGBUG: It is super unclear what this code does anyway. + if (string.IsNullOrEmpty (toplevel.Id)) + { + var count = 1; + var id = (_topLevels.Count + count).ToString (); + + while (_topLevels.Count > 0 && _topLevels.FirstOrDefault (x => x.Id == id) is { }) + { + count++; + id = (_topLevels.Count + count).ToString (); + } + + toplevel.Id = (_topLevels.Count + count).ToString (); + + _topLevels.Push (toplevel); + } + else + { + Toplevel dup = _topLevels.FirstOrDefault (x => x.Id == toplevel.Id); + + if (dup is null) + { + _topLevels.Push (toplevel); + } + } + + if (_topLevels.FindDuplicates (new ToplevelEqualityComparer ()).Count > 0) + { + throw new ArgumentException ("There are duplicates Toplevel IDs"); + } + } + + if (Top is null || toplevel.IsOverlappedContainer) + { + Top = toplevel; + } + + var refreshDriver = true; + + if (OverlappedTop is null + || toplevel.IsOverlappedContainer + || (Current?.Modal == false && toplevel.Modal) + || (Current?.Modal == false && !toplevel.Modal) + || (Current?.Modal == true && toplevel.Modal)) + { + if (toplevel.Visible) + { + Current?.OnDeactivate (toplevel); + Toplevel previousCurrent = Current; + Current = toplevel; + Current.OnActivate (previousCurrent); + + SetCurrentOverlappedAsTop (); + } + else + { + refreshDriver = false; + } + } + else if ((OverlappedTop != null + && toplevel != OverlappedTop + && Current?.Modal == true + && !_topLevels.Peek ().Modal) + || (OverlappedTop is { } && toplevel != OverlappedTop && Current?.Running == false)) + { + refreshDriver = false; + MoveCurrent (toplevel); + } + else + { + refreshDriver = false; + MoveCurrent (Current); + } + + toplevel.SetRelativeLayout (Driver.Screen.Size); + + toplevel.LayoutSubviews (); + toplevel.PositionToplevels (); + toplevel.FocusFirst (); + BringOverlappedTopToFront (); + + if (refreshDriver) + { + OverlappedTop?.OnChildLoaded (toplevel); + toplevel.OnLoaded (); + toplevel.SetNeedsDisplay (); + toplevel.Draw (); + Driver.UpdateScreen (); + + if (PositionCursor (toplevel)) + { + Driver.UpdateCursor (); + } + } + + NotifyNewRunState?.Invoke (toplevel, new (rs)); + + return rs; + } + + /// + /// Calls on the most focused view in the view starting with . + /// + /// + /// Does nothing if is or if the most focused view is not visible or + /// enabled. + /// + /// If the most focused view is not visible within it's superview, the cursor will be hidden. + /// + /// + /// if a view positioned the cursor and the position is visible. + internal static bool PositionCursor (View view) + { + // Find the most focused view and position the cursor there. + View mostFocused = view?.MostFocused; + + if (mostFocused is null) + { + if (view is { HasFocus: true }) + { + mostFocused = view; + } + else + { + return false; + } + } + + // If the view is not visible or enabled, don't position the cursor + if (!mostFocused.Visible || !mostFocused.Enabled) + { + Driver.GetCursorVisibility (out CursorVisibility current); + + if (current != CursorVisibility.Invisible) + { + Driver.SetCursorVisibility (CursorVisibility.Invisible); + } + + return false; + } + + // If the view is not visible within it's superview, don't position the cursor + Rectangle mostFocusedViewport = mostFocused.ViewportToScreen (mostFocused.Viewport with { Location = Point.Empty }); + Rectangle superViewViewport = mostFocused.SuperView?.ViewportToScreen (mostFocused.SuperView.Viewport with { Location = Point.Empty }) ?? Driver.Screen; + + if (!superViewViewport.IntersectsWith (mostFocusedViewport)) + { + return false; + } + + Point? cursor = mostFocused.PositionCursor (); + + Driver.GetCursorVisibility (out CursorVisibility currentCursorVisibility); + + if (cursor is { }) + { + // Convert cursor to screen coords + cursor = mostFocused.ViewportToScreen (mostFocused.Viewport with { Location = cursor.Value }).Location; + + // If the cursor is not in a visible location in the SuperView, hide it + if (!superViewViewport.Contains (cursor.Value)) + { + if (currentCursorVisibility != CursorVisibility.Invisible) + { + Driver.SetCursorVisibility (CursorVisibility.Invisible); + } + + return false; + } + + // Show it + if (currentCursorVisibility == CursorVisibility.Invisible) + { + Driver.SetCursorVisibility (mostFocused.CursorVisibility); + } + + return true; + } + + if (currentCursorVisibility != CursorVisibility.Invisible) + { + Driver.SetCursorVisibility (CursorVisibility.Invisible); + } + + return false; + } + + /// + /// Runs the application by creating a object and calling + /// . + /// + /// + /// Calling first is not needed as this function will initialize the application. + /// + /// must be called when the application is closing (typically after Run> has returned) to + /// ensure resources are cleaned up and terminal settings restored. + /// + /// + /// The caller is responsible for disposing the object returned by this method. + /// + /// + /// The created object. The caller is responsible for disposing this object. + [RequiresUnreferencedCode ("AOT")] + [RequiresDynamicCode ("AOT")] + public static Toplevel Run (Func errorHandler = null, ConsoleDriver driver = null) { return Run (errorHandler, driver); } + + /// + /// Runs the application by creating a -derived object of type T and calling + /// . + /// + /// + /// Calling first is not needed as this function will initialize the application. + /// + /// must be called when the application is closing (typically after Run> has returned) to + /// ensure resources are cleaned up and terminal settings restored. + /// + /// + /// The caller is responsible for disposing the object returned by this method. + /// + /// + /// + /// + /// The to use. If not specified the default driver for the platform will + /// be used ( , , or ). Must be + /// if has already been called. + /// + /// The created T object. The caller is responsible for disposing this object. + [RequiresUnreferencedCode ("AOT")] + [RequiresDynamicCode ("AOT")] + public static T Run (Func errorHandler = null, ConsoleDriver driver = null) + where T : Toplevel, new () + { + if (!_initialized) + { + // Init() has NOT been called. + InternalInit (driver, null, true); + } + + var top = new T (); + + Run (top, errorHandler); + + return top; + } + + /// Runs the Application using the provided view. + /// + /// + /// This method is used to start processing events for the main application, but it is also used to run other + /// modal s such as boxes. + /// + /// + /// To make a stop execution, call + /// . + /// + /// + /// Calling is equivalent to calling + /// , followed by , and then calling + /// . + /// + /// + /// Alternatively, to have a program control the main loop and process events manually, call + /// to set things up manually and then repeatedly call + /// with the wait parameter set to false. By doing this the + /// method will only process any pending events, timers, idle handlers and then + /// return control immediately. + /// + /// When using or + /// + /// will be called automatically. + /// + /// + /// RELEASE builds only: When is any exceptions will be + /// rethrown. Otherwise, if will be called. If + /// returns the will resume; otherwise this method will + /// exit. + /// + /// + /// The to run as a modal. + /// + /// RELEASE builds only: Handler for any unhandled exceptions (resumes when returns true, + /// rethrows when null). + /// + public static void Run (Toplevel view, Func errorHandler = null) + { + ArgumentNullException.ThrowIfNull (view); + + if (_initialized) + { + if (Driver is null) + { + // Disposing before throwing + view.Dispose (); + + // This code path should be impossible because Init(null, null) will select the platform default driver + throw new InvalidOperationException ( + "Init() completed without a driver being set (this should be impossible); Run() cannot be called." + ); + } + } + else + { + // Init() has NOT been called. + throw new InvalidOperationException ( + "Init() has not been called. Only Run() or Run() can be used without calling Init()." + ); + } + + var resume = true; + + while (resume) + { +#if !DEBUG + try + { +#endif + resume = false; + RunState runState = Begin (view); + + // If EndAfterFirstIteration is true then the user must dispose of the runToken + // by using NotifyStopRunState event. + RunLoop (runState); + + if (runState.Toplevel is null) + { +#if DEBUG_IDISPOSABLE + Debug.Assert (_topLevels.Count == 0); +#endif + runState.Dispose (); + + return; + } + + if (!EndAfterFirstIteration) + { + End (runState); + } +#if !DEBUG + } + catch (Exception error) + { + if (errorHandler is null) + { + throw; + } + + resume = errorHandler (error); + } +#endif + } + } + + /// Adds a timeout to the application. + /// + /// When time specified passes, the callback will be invoked. If the callback returns true, the timeout will be + /// reset, repeating the invocation. If it returns false, the timeout will stop and be removed. The returned value is a + /// token that can be used to stop the timeout by calling . + /// + public static object AddTimeout (TimeSpan time, Func callback) { return MainLoop?.AddTimeout (time, callback); } + + /// Removes a previously scheduled timeout + /// The token parameter is the value returned by . + /// Returns + /// true + /// if the timeout is successfully removed; otherwise, + /// false + /// . + /// This method also returns + /// false + /// if the timeout is not found. + public static bool RemoveTimeout (object token) { return MainLoop?.RemoveTimeout (token) ?? false; } + + /// Runs on the thread that is processing events + /// the action to be invoked on the main processing thread. + public static void Invoke (Action action) + { + MainLoop?.AddIdle ( + () => + { + action (); + + return false; + } + ); + } + + /// Wakes up the running application that might be waiting on input. + public static void Wakeup () { MainLoop?.Wakeup (); } + + /// Triggers a refresh of the entire display. + public static void Refresh () + { + // TODO: Figure out how to remove this call to ClearContents. Refresh should just repaint damaged areas, not clear + Driver.ClearContents (); + View last = null; + + foreach (Toplevel v in _topLevels.Reverse ()) + { + if (v.Visible) + { + v.SetNeedsDisplay (); + v.SetSubViewNeedsDisplay (); + v.Draw (); + } + + last = v; + } + + Driver.Refresh (); + } + + /// This event is raised on each iteration of the main loop. + /// See also + public static event EventHandler Iteration; + + /// The driver for the application + /// The main loop. + internal static MainLoop MainLoop { get; private set; } + + /// + /// Set to true to cause to be called after the first iteration. Set to false (the default) to + /// cause the application to continue running until Application.RequestStop () is called. + /// + public static bool EndAfterFirstIteration { get; set; } + + /// Building block API: Runs the main loop for the created . + /// The state returned by the method. + public static void RunLoop (RunState state) + { + ArgumentNullException.ThrowIfNull (state); + ObjectDisposedException.ThrowIf (state.Toplevel is null, "state"); + + var firstIteration = true; + + for (state.Toplevel.Running = true; state.Toplevel?.Running == true;) + { + MainLoop.Running = true; + + if (EndAfterFirstIteration && !firstIteration) + { + return; + } + + RunIteration (ref state, ref firstIteration); + } + + MainLoop.Running = false; + + // Run one last iteration to consume any outstanding input events from Driver + // This is important for remaining OnKeyUp events. + RunIteration (ref state, ref firstIteration); + } + + /// Run one application iteration. + /// The state returned by . + /// + /// Set to if this is the first run loop iteration. Upon return, it + /// will be set to if at least one iteration happened. + /// + public static void RunIteration (ref RunState state, ref bool firstIteration) + { + if (MainLoop.Running && MainLoop.EventsPending ()) + { + // Notify Toplevel it's ready + if (firstIteration) + { + state.Toplevel.OnReady (); + } + + MainLoop.RunIteration (); + Iteration?.Invoke (null, new ()); + EnsureModalOrVisibleAlwaysOnTop (state.Toplevel); + + if (state.Toplevel != Current) + { + OverlappedTop?.OnDeactivate (state.Toplevel); + state.Toplevel = Current; + OverlappedTop?.OnActivate (state.Toplevel); + Top.SetSubViewNeedsDisplay (); + Refresh (); + } + } + + firstIteration = false; + + if (Current == null) + { + return; + } + + if (state.Toplevel != Top && (Top.NeedsDisplay || Top.SubViewNeedsDisplay || Top.LayoutNeeded)) + { + state.Toplevel.SetNeedsDisplay (state.Toplevel.Frame); + Top.Draw (); + + foreach (Toplevel top in _topLevels.Reverse ()) + { + if (top != Top && top != state.Toplevel) + { + top.SetNeedsDisplay (); + top.SetSubViewNeedsDisplay (); + top.Draw (); + } + } + } + + if (_topLevels.Count == 1 + && state.Toplevel == Top + && (Driver.Cols != state.Toplevel.Frame.Width + || Driver.Rows != state.Toplevel.Frame.Height) + && (state.Toplevel.NeedsDisplay + || state.Toplevel.SubViewNeedsDisplay + || state.Toplevel.LayoutNeeded)) + { + Driver.ClearContents (); + } + + if (state.Toplevel.NeedsDisplay || state.Toplevel.SubViewNeedsDisplay || state.Toplevel.LayoutNeeded || OverlappedChildNeedsDisplay ()) + { + state.Toplevel.SetNeedsDisplay (); + state.Toplevel.Draw (); + Driver.UpdateScreen (); + + //Driver.UpdateCursor (); + } + + if (PositionCursor (state.Toplevel)) + { + Driver.UpdateCursor (); + } + + // else + { + //if (PositionCursor (state.Toplevel)) + //{ + // Driver.Refresh (); + //} + //Driver.UpdateCursor (); + } + + if (state.Toplevel != Top && !state.Toplevel.Modal && (Top.NeedsDisplay || Top.SubViewNeedsDisplay || Top.LayoutNeeded)) + { + Top.Draw (); + } + } + + /// Stops the provided , causing or the if provided. + /// The to stop. + /// + /// This will cause to return. + /// + /// Calling is equivalent to setting the + /// property on the currently running to false. + /// + /// + public static void RequestStop (Toplevel top = null) + { + if (OverlappedTop is null || top is null || (OverlappedTop is null && top is { })) + { + top = Current; + } + + if (OverlappedTop != null + && top.IsOverlappedContainer + && top?.Running == true + && (Current?.Modal == false || (Current?.Modal == true && Current?.Running == false))) + { + OverlappedTop.RequestStop (); + } + else if (OverlappedTop != null + && top != Current + && Current?.Running == true + && Current?.Modal == true + && top.Modal + && top.Running) + { + var ev = new ToplevelClosingEventArgs (Current); + Current.OnClosing (ev); + + if (ev.Cancel) + { + return; + } + + ev = new (top); + top.OnClosing (ev); + + if (ev.Cancel) + { + return; + } + + Current.Running = false; + OnNotifyStopRunState (Current); + top.Running = false; + OnNotifyStopRunState (top); + } + else if ((OverlappedTop != null + && top != OverlappedTop + && top != Current + && Current?.Modal == false + && Current?.Running == true + && !top.Running) + || (OverlappedTop != null + && top != OverlappedTop + && top != Current + && Current?.Modal == false + && Current?.Running == false + && !top.Running + && _topLevels.ToArray () [1].Running)) + { + MoveCurrent (top); + } + else if (OverlappedTop != null + && Current != top + && Current?.Running == true + && !top.Running + && Current?.Modal == true + && top.Modal) + { + // The Current and the top are both modal so needed to set the Current.Running to false too. + Current.Running = false; + OnNotifyStopRunState (Current); + } + else if (OverlappedTop != null + && Current == top + && OverlappedTop?.Running == true + && Current?.Running == true + && top.Running + && Current?.Modal == true + && top.Modal) + { + // The OverlappedTop was requested to stop inside a modal Toplevel which is the Current and top, + // both are the same, so needed to set the Current.Running to false too. + Current.Running = false; + OnNotifyStopRunState (Current); + } + else + { + Toplevel currentTop; + + if (top == Current || (Current?.Modal == true && !top.Modal)) + { + currentTop = Current; + } + else + { + currentTop = top; + } + + if (!currentTop.Running) + { + return; + } + + var ev = new ToplevelClosingEventArgs (currentTop); + currentTop.OnClosing (ev); + + if (ev.Cancel) + { + return; + } + + currentTop.Running = false; + OnNotifyStopRunState (currentTop); + } + } + + private static void OnNotifyStopRunState (Toplevel top) + { + if (EndAfterFirstIteration) + { + NotifyStopRunState?.Invoke (top, new (top)); + } + } + + /// + /// Building block API: completes the execution of a that was started with + /// . + /// + /// The returned by the method. + public static void End (RunState runState) + { + ArgumentNullException.ThrowIfNull (runState); + + if (OverlappedTop is { }) + { + OverlappedTop.OnChildUnloaded (runState.Toplevel); + } + else + { + runState.Toplevel.OnUnloaded (); + } + + // End the RunState.Toplevel + // First, take it off the Toplevel Stack + if (_topLevels.Count > 0) + { + if (_topLevels.Peek () != runState.Toplevel) + { + // If the top of the stack is not the RunState.Toplevel then + // this call to End is not balanced with the call to Begin that started the RunState + throw new ArgumentException ("End must be balanced with calls to Begin"); + } + + _topLevels.Pop (); + } + + // Notify that it is closing + runState.Toplevel?.OnClosed (runState.Toplevel); + + // If there is a OverlappedTop that is not the RunState.Toplevel then RunState.Toplevel + // is a child of MidTop, and we should notify the OverlappedTop that it is closing + if (OverlappedTop is { } && !runState.Toplevel.Modal && runState.Toplevel != OverlappedTop) + { + OverlappedTop.OnChildClosed (runState.Toplevel); + } + + // Set Current and Top to the next TopLevel on the stack + if (_topLevels.Count == 0) + { + Current = null; + } + else + { + if (_topLevels.Count > 1 && _topLevels.Peek () == OverlappedTop && OverlappedChildren.Any (t => t.Visible) is { }) + { + OverlappedMoveNext (); + } + + Current = _topLevels.Peek (); + + if (_topLevels.Count == 1 && Current == OverlappedTop) + { + OverlappedTop.OnAllChildClosed (); + } + else + { + SetCurrentOverlappedAsTop (); + runState.Toplevel.OnLeave (Current); + Current.OnEnter (runState.Toplevel); + } + + Refresh (); + } + + // Don't dispose runState.Toplevel. It's up to caller dispose it + // If it's not the same as the current in the RunIteration, + // it will be fixed later in the next RunIteration. + if (OverlappedTop is { } && !_topLevels.Contains (OverlappedTop)) + { + _cachedRunStateToplevel = OverlappedTop; + } + else + { + _cachedRunStateToplevel = runState.Toplevel; + } + + runState.Toplevel = null; + runState.Dispose (); + } +} diff --git a/Terminal.Gui/Application/Application.cs b/Terminal.Gui/Application/Application.cs index 9c981b743..1561413e9 100644 --- a/Terminal.Gui/Application/Application.cs +++ b/Terminal.Gui/Application/Application.cs @@ -1,5 +1,5 @@ +#nullable enable using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Reflection; @@ -18,31 +18,6 @@ namespace Terminal.Gui; /// TODO: Flush this out. public static partial class Application { - // For Unit testing - ignores UseSystemConsole - internal static bool _forceFakeConsole; - - /// Gets the that has been selected. See also . - public static ConsoleDriver Driver { get; internal set; } - - /// - /// Gets or sets whether will be forced to output only the 16 colors defined in - /// . The default is , meaning 24-bit (TrueColor) colors will be output - /// as long as the selected supports TrueColor. - /// - [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] - public static bool Force16Colors { get; set; } - - /// - /// Forces the use of the specified driver (one of "fake", "ansi", "curses", "net", or "windows"). If not - /// specified, the driver is selected based on the platform. - /// - /// - /// Note, will override this configuration setting if called - /// with either `driver` or `driverName` specified. - /// - [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] - public static string ForceDriver { get; set; } = string.Empty; - /// Gets all cultures supported by the application without the invariant language. public static List SupportedCultures { get; private set; } @@ -68,10 +43,6 @@ public static partial class Application .ToList (); } - // When `End ()` is called, it is possible `RunState.Toplevel` is a different object than `Top`. - // This variable is set in `End` in this case so that `Begin` correctly sets `Top`. - private static Toplevel _cachedRunStateToplevel; - // 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 @@ -82,9 +53,9 @@ public static partial class Application // 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 (Toplevel t in _topLevels) + foreach (Toplevel? t in _topLevels) { - t.Running = false; + t!.Running = false; } _topLevels.Clear (); @@ -163,1072 +134,11 @@ public static partial class Application SynchronizationContext.SetSynchronizationContext (null); } - #region Initialization (Init/Shutdown) - - /// Initializes a new instance of Application. - /// Call this method once per instance (or after has been called). - /// - /// This function loads the right for the platform, Creates a . and - /// assigns it to - /// - /// - /// must be called when the application is closing (typically after - /// has returned) to ensure resources are cleaned up and - /// terminal settings - /// restored. - /// - /// - /// The function combines - /// and - /// into a single - /// call. An application cam use without explicitly calling - /// . - /// - /// - /// The to use. If neither or - /// are specified the default driver for the platform will be used. - /// - /// - /// The short name (e.g. "net", "windows", "ansi", "fake", or "curses") of the - /// to use. If neither or are - /// specified the default driver for the platform will be used. - /// - [RequiresUnreferencedCode ("AOT")] - [RequiresDynamicCode ("AOT")] - public static void Init (ConsoleDriver driver = null, string driverName = null) { InternalInit (driver, driverName); } - - internal static bool _initialized; - internal static int _mainThreadId = -1; - - // INTERNAL function for initializing an app with a Toplevel factory object, driver, and mainloop. - // - // Called from: - // - // Init() - When the user wants to use the default Toplevel. calledViaRunT will be false, causing all state to be reset. - // Run() - When the user wants to use a custom Toplevel. calledViaRunT will be true, enabling Run() to be called without calling Init first. - // Unit Tests - To initialize the app with a custom Toplevel, using the FakeDriver. calledViaRunT will be false, causing all state to be reset. - // - // calledViaRunT: If false (default) all state will be reset. If true the state will not be reset. - [RequiresUnreferencedCode ("AOT")] - [RequiresDynamicCode ("AOT")] - internal static void InternalInit ( - ConsoleDriver driver = null, - string driverName = null, - bool calledViaRunT = false - ) - { - if (_initialized && driver is null) - { - return; - } - - if (_initialized) - { - throw new InvalidOperationException ("Init has already been called and must be bracketed by Shutdown."); - } - - if (!calledViaRunT) - { - // Reset all class variables (Application is a singleton). - ResetState (); - } - - // For UnitTests - if (driver is { }) - { - Driver = driver; - } - - // Start the process of configuration management. - // Note that we end up calling LoadConfigurationFromAllSources - // multiple times. We need to do this because some settings are only - // valid after a Driver is loaded. In this case we need just - // `Settings` so we can determine which driver to use. - // Don't reset, so we can inherit the theme from the previous run. - Load (); - Apply (); - - // Ignore Configuration for ForceDriver if driverName is specified - if (!string.IsNullOrEmpty (driverName)) - { - ForceDriver = driverName; - } - - if (Driver is null) - { - PlatformID p = Environment.OSVersion.Platform; - - if (string.IsNullOrEmpty (ForceDriver)) - { - if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows) - { - Driver = new WindowsDriver (); - } - else - { - Driver = new CursesDriver (); - } - } - else - { - List drivers = GetDriverTypes (); - Type driverType = drivers.FirstOrDefault (t => t.Name.Equals (ForceDriver, StringComparison.InvariantCultureIgnoreCase)); - - if (driverType is { }) - { - Driver = (ConsoleDriver)Activator.CreateInstance (driverType); - } - else - { - throw new ArgumentException ( - $"Invalid driver name: {ForceDriver}. Valid names are {string.Join (", ", drivers.Select (t => t.Name))}" - ); - } - } - } - - try - { - MainLoop = Driver.Init (); - } - catch (InvalidOperationException ex) - { - // This is a case where the driver is unable to initialize the console. - // This can happen if the console is already in use by another process or - // if running in unit tests. - // In this case, we want to throw a more specific exception. - throw new InvalidOperationException ( - "Unable to initialize the console. This can happen if the console is already in use by another process or in unit tests.", - ex - ); - } - - Driver.SizeChanged += (s, args) => OnSizeChanging (args); - Driver.KeyDown += (s, args) => OnKeyDown (args); - Driver.KeyUp += (s, args) => OnKeyUp (args); - Driver.MouseEvent += (s, args) => OnMouseEvent (args); - - SynchronizationContext.SetSynchronizationContext (new MainLoopSyncContext ()); - - SupportedCultures = GetSupportedCultures (); - _mainThreadId = Thread.CurrentThread.ManagedThreadId; - _initialized = true; - InitializedChanged?.Invoke (null, new (in _initialized)); - } - - private static void Driver_SizeChanged (object sender, SizeChangedEventArgs e) { OnSizeChanging (e); } - private static void Driver_KeyDown (object sender, Key e) { OnKeyDown (e); } - private static void Driver_KeyUp (object sender, Key e) { OnKeyUp (e); } - private static void Driver_MouseEvent (object sender, MouseEvent e) { OnMouseEvent (e); } - - /// Gets of list of types that are available. - /// - [RequiresUnreferencedCode ("AOT")] - public static List GetDriverTypes () - { - // use reflection to get the list of drivers - List driverTypes = new (); - - foreach (Assembly asm in AppDomain.CurrentDomain.GetAssemblies ()) - { - foreach (Type type in asm.GetTypes ()) - { - if (type.IsSubclassOf (typeof (ConsoleDriver)) && !type.IsAbstract) - { - driverTypes.Add (type); - } - } - } - - return driverTypes; - } - - /// Shutdown an application initialized with . - /// - /// Shutdown must be called for every call to or - /// to ensure all resources are cleaned - /// up (Disposed) - /// and terminal settings are restored. - /// - public static void Shutdown () - { - // TODO: Throw an exception if Init hasn't been called. - ResetState (); - PrintJsonErrors (); - InitializedChanged?.Invoke (null, new (in _initialized)); - } - -#nullable enable - /// - /// This event is raised after the and methods have been called. - /// - /// - /// Intended to support unit tests that need to know when the application has been initialized. - /// - public static event EventHandler>? InitializedChanged; -#nullable restore - - #endregion Initialization (Init/Shutdown) - - #region Run (Begin, Run, End, Stop) - - /// - /// Notify that a new was created ( was called). The token is - /// created in and this event will be fired before that function exits. - /// - /// - /// If is callers to - /// must also subscribe to and manually dispose of the token - /// when the application is done. - /// - public static event EventHandler NotifyNewRunState; - - /// Notify that an existent is stopping ( was called). - /// - /// If is callers to - /// must also subscribe to and manually dispose of the token - /// when the application is done. - /// - public static event EventHandler NotifyStopRunState; - - /// Building block API: Prepares the provided for execution. - /// - /// The handle that needs to be passed to the method upon - /// completion. - /// - /// The to prepare execution for. - /// - /// This method prepares the provided for running with the focus, it adds this to the list - /// of s, lays out the Subviews, focuses the first element, and draws the - /// in the screen. This is usually followed by executing the method, and then the - /// method upon termination which will undo these changes. - /// - public static RunState Begin (Toplevel toplevel) - { - ArgumentNullException.ThrowIfNull (toplevel); - -#if DEBUG_IDISPOSABLE - Debug.Assert (!toplevel.WasDisposed); - - if (_cachedRunStateToplevel is { } && _cachedRunStateToplevel != toplevel) - { - Debug.Assert (_cachedRunStateToplevel.WasDisposed); - } -#endif - - if (toplevel.IsOverlappedContainer && OverlappedTop != toplevel && OverlappedTop is { }) - { - throw new InvalidOperationException ("Only one Overlapped Container is allowed."); - } - - // Ensure the mouse is ungrabbed. - MouseGrabView = null; - - var rs = new RunState (toplevel); - - // View implements ISupportInitializeNotification which is derived from ISupportInitialize - if (!toplevel.IsInitialized) - { - toplevel.BeginInit (); - toplevel.EndInit (); - } - -#if DEBUG_IDISPOSABLE - if (Top is { } && toplevel != Top && !_topLevels.Contains (Top)) - { - // This assertion confirm if the Top was already disposed - Debug.Assert (Top.WasDisposed); - Debug.Assert (Top == _cachedRunStateToplevel); - } -#endif - - lock (_topLevels) - { - if (Top is { } && toplevel != Top && !_topLevels.Contains (Top)) - { - // If Top was already disposed and isn't on the Toplevels Stack, - // clean it up here if is the same as _cachedRunStateToplevel - if (Top == _cachedRunStateToplevel) - { - Top = null; - } - else - { - // Probably this will never hit - throw new ObjectDisposedException (Top.GetType ().FullName); - } - } - else if (OverlappedTop is { } && toplevel != Top && _topLevels.Contains (Top)) - { - Top.OnLeave (toplevel); - } - - // BUGBUG: We should not depend on `Id` internally. - // BUGBUG: It is super unclear what this code does anyway. - if (string.IsNullOrEmpty (toplevel.Id)) - { - var count = 1; - var id = (_topLevels.Count + count).ToString (); - - while (_topLevels.Count > 0 && _topLevels.FirstOrDefault (x => x.Id == id) is { }) - { - count++; - id = (_topLevels.Count + count).ToString (); - } - - toplevel.Id = (_topLevels.Count + count).ToString (); - - _topLevels.Push (toplevel); - } - else - { - Toplevel dup = _topLevels.FirstOrDefault (x => x.Id == toplevel.Id); - - if (dup is null) - { - _topLevels.Push (toplevel); - } - } - - if (_topLevels.FindDuplicates (new ToplevelEqualityComparer ()).Count > 0) - { - throw new ArgumentException ("There are duplicates Toplevel IDs"); - } - } - - if (Top is null || toplevel.IsOverlappedContainer) - { - Top = toplevel; - } - - var refreshDriver = true; - - if (OverlappedTop is null - || toplevel.IsOverlappedContainer - || (Current?.Modal == false && toplevel.Modal) - || (Current?.Modal == false && !toplevel.Modal) - || (Current?.Modal == true && toplevel.Modal)) - { - if (toplevel.Visible) - { - Current?.OnDeactivate (toplevel); - Toplevel previousCurrent = Current; - Current = toplevel; - Current.OnActivate (previousCurrent); - - SetCurrentOverlappedAsTop (); - } - else - { - refreshDriver = false; - } - } - else if ((OverlappedTop != null - && toplevel != OverlappedTop - && Current?.Modal == true - && !_topLevels.Peek ().Modal) - || (OverlappedTop is { } && toplevel != OverlappedTop && Current?.Running == false)) - { - refreshDriver = false; - MoveCurrent (toplevel); - } - else - { - refreshDriver = false; - MoveCurrent (Current); - } - - toplevel.SetRelativeLayout (Driver.Screen.Size); - - toplevel.LayoutSubviews (); - toplevel.PositionToplevels (); - toplevel.FocusFirst (); - BringOverlappedTopToFront (); - - if (refreshDriver) - { - OverlappedTop?.OnChildLoaded (toplevel); - toplevel.OnLoaded (); - toplevel.SetNeedsDisplay (); - toplevel.Draw (); - Driver.UpdateScreen (); - - if (PositionCursor (toplevel)) - { - Driver.UpdateCursor (); - } - } - - NotifyNewRunState?.Invoke (toplevel, new (rs)); - - return rs; - } - - /// - /// Calls on the most focused view in the view starting with . - /// - /// - /// Does nothing if is or if the most focused view is not visible or - /// enabled. - /// - /// If the most focused view is not visible within it's superview, the cursor will be hidden. - /// - /// - /// if a view positioned the cursor and the position is visible. - internal static bool PositionCursor (View view) - { - // Find the most focused view and position the cursor there. - View mostFocused = view?.MostFocused; - - if (mostFocused is null) - { - if (view is { HasFocus: true }) - { - mostFocused = view; - } - else - { - return false; - } - } - - // If the view is not visible or enabled, don't position the cursor - if (!mostFocused.Visible || !mostFocused.Enabled) - { - Driver.GetCursorVisibility (out CursorVisibility current); - - if (current != CursorVisibility.Invisible) - { - Driver.SetCursorVisibility (CursorVisibility.Invisible); - } - - return false; - } - - // If the view is not visible within it's superview, don't position the cursor - Rectangle mostFocusedViewport = mostFocused.ViewportToScreen (mostFocused.Viewport with { Location = Point.Empty }); - Rectangle superViewViewport = mostFocused.SuperView?.ViewportToScreen (mostFocused.SuperView.Viewport with { Location = Point.Empty }) ?? Driver.Screen; - - if (!superViewViewport.IntersectsWith (mostFocusedViewport)) - { - return false; - } - - Point? cursor = mostFocused.PositionCursor (); - - Driver.GetCursorVisibility (out CursorVisibility currentCursorVisibility); - - if (cursor is { }) - { - // Convert cursor to screen coords - cursor = mostFocused.ViewportToScreen (mostFocused.Viewport with { Location = cursor.Value }).Location; - - // If the cursor is not in a visible location in the SuperView, hide it - if (!superViewViewport.Contains (cursor.Value)) - { - if (currentCursorVisibility != CursorVisibility.Invisible) - { - Driver.SetCursorVisibility (CursorVisibility.Invisible); - } - - return false; - } - - // Show it - if (currentCursorVisibility == CursorVisibility.Invisible) - { - Driver.SetCursorVisibility (mostFocused.CursorVisibility); - } - - return true; - } - - if (currentCursorVisibility != CursorVisibility.Invisible) - { - Driver.SetCursorVisibility (CursorVisibility.Invisible); - } - - return false; - } - - /// - /// Runs the application by creating a object and calling - /// . - /// - /// - /// Calling first is not needed as this function will initialize the application. - /// - /// must be called when the application is closing (typically after Run> has returned) to - /// ensure resources are cleaned up and terminal settings restored. - /// - /// - /// The caller is responsible for disposing the object returned by this method. - /// - /// - /// The created object. The caller is responsible for disposing this object. - [RequiresUnreferencedCode ("AOT")] - [RequiresDynamicCode ("AOT")] - public static Toplevel Run (Func errorHandler = null, ConsoleDriver driver = null) { return Run (errorHandler, driver); } - - /// - /// Runs the application by creating a -derived object of type T and calling - /// . - /// - /// - /// Calling first is not needed as this function will initialize the application. - /// - /// must be called when the application is closing (typically after Run> has returned) to - /// ensure resources are cleaned up and terminal settings restored. - /// - /// - /// The caller is responsible for disposing the object returned by this method. - /// - /// - /// - /// - /// The to use. If not specified the default driver for the platform will - /// be used ( , , or ). Must be - /// if has already been called. - /// - /// The created T object. The caller is responsible for disposing this object. - [RequiresUnreferencedCode ("AOT")] - [RequiresDynamicCode ("AOT")] - public static T Run (Func errorHandler = null, ConsoleDriver driver = null) - where T : Toplevel, new() - { - if (!_initialized) - { - // Init() has NOT been called. - InternalInit (driver, null, true); - } - - var top = new T (); - - Run (top, errorHandler); - - return top; - } - - /// Runs the Application using the provided view. - /// - /// - /// This method is used to start processing events for the main application, but it is also used to run other - /// modal s such as boxes. - /// - /// - /// To make a stop execution, call - /// . - /// - /// - /// Calling is equivalent to calling - /// , followed by , and then calling - /// . - /// - /// - /// Alternatively, to have a program control the main loop and process events manually, call - /// to set things up manually and then repeatedly call - /// with the wait parameter set to false. By doing this the - /// method will only process any pending events, timers, idle handlers and then - /// return control immediately. - /// - /// When using or - /// - /// will be called automatically. - /// - /// - /// RELEASE builds only: When is any exceptions will be - /// rethrown. Otherwise, if will be called. If - /// returns the will resume; otherwise this method will - /// exit. - /// - /// - /// The to run as a modal. - /// - /// RELEASE builds only: Handler for any unhandled exceptions (resumes when returns true, - /// rethrows when null). - /// - public static void Run (Toplevel view, Func errorHandler = null) - { - ArgumentNullException.ThrowIfNull (view); - - if (_initialized) - { - if (Driver is null) - { - // Disposing before throwing - view.Dispose (); - - // This code path should be impossible because Init(null, null) will select the platform default driver - throw new InvalidOperationException ( - "Init() completed without a driver being set (this should be impossible); Run() cannot be called." - ); - } - } - else - { - // Init() has NOT been called. - throw new InvalidOperationException ( - "Init() has not been called. Only Run() or Run() can be used without calling Init()." - ); - } - - var resume = true; - - while (resume) - { -#if !DEBUG - try - { -#endif - resume = false; - RunState runState = Begin (view); - - // If EndAfterFirstIteration is true then the user must dispose of the runToken - // by using NotifyStopRunState event. - RunLoop (runState); - - if (runState.Toplevel is null) - { -#if DEBUG_IDISPOSABLE - Debug.Assert (_topLevels.Count == 0); -#endif - runState.Dispose (); - - return; - } - - if (!EndAfterFirstIteration) - { - End (runState); - } -#if !DEBUG - } - catch (Exception error) - { - if (errorHandler is null) - { - throw; - } - - resume = errorHandler (error); - } -#endif - } - } - - /// Adds a timeout to the application. - /// - /// When time specified passes, the callback will be invoked. If the callback returns true, the timeout will be - /// reset, repeating the invocation. If it returns false, the timeout will stop and be removed. The returned value is a - /// token that can be used to stop the timeout by calling . - /// - public static object AddTimeout (TimeSpan time, Func callback) { return MainLoop?.AddTimeout (time, callback); } - - /// Removes a previously scheduled timeout - /// The token parameter is the value returned by . - /// Returns - /// true - /// if the timeout is successfully removed; otherwise, - /// false - /// . - /// This method also returns - /// false - /// if the timeout is not found. - public static bool RemoveTimeout (object token) { return MainLoop?.RemoveTimeout (token) ?? false; } - - /// Runs on the thread that is processing events - /// the action to be invoked on the main processing thread. - public static void Invoke (Action action) - { - MainLoop?.AddIdle ( - () => - { - action (); - - return false; - } - ); - } + // When `End ()` is called, it is possible `RunState.Toplevel` is a different object than `Top`. + // This field is set in `End` in this case so that `Begin` correctly sets `Top`. // TODO: Determine if this is really needed. The only code that calls WakeUp I can find // is ProgressBarStyles, and it's not clear it needs to. - /// Wakes up the running application that might be waiting on input. - public static void Wakeup () { MainLoop?.Wakeup (); } - - /// Triggers a refresh of the entire display. - public static void Refresh () - { - // TODO: Figure out how to remove this call to ClearContents. Refresh should just repaint damaged areas, not clear - Driver.ClearContents (); - View last = null; - - foreach (Toplevel v in _topLevels.Reverse ()) - { - if (v.Visible) - { - v.SetNeedsDisplay (); - v.SetSubViewNeedsDisplay (); - v.Draw (); - } - - last = v; - } - - Driver.Refresh (); - } - - /// This event is raised on each iteration of the main loop. - /// See also - public static event EventHandler Iteration; - - /// The driver for the application - /// The main loop. - internal static MainLoop MainLoop { get; private set; } - - /// - /// Set to true to cause to be called after the first iteration. Set to false (the default) to - /// cause the application to continue running until Application.RequestStop () is called. - /// - public static bool EndAfterFirstIteration { get; set; } - - /// Building block API: Runs the main loop for the created . - /// The state returned by the method. - public static void RunLoop (RunState state) - { - ArgumentNullException.ThrowIfNull (state); - ObjectDisposedException.ThrowIf (state.Toplevel is null, "state"); - - var firstIteration = true; - - for (state.Toplevel.Running = true; state.Toplevel?.Running == true;) - { - MainLoop.Running = true; - - if (EndAfterFirstIteration && !firstIteration) - { - return; - } - - RunIteration (ref state, ref firstIteration); - } - - MainLoop.Running = false; - - // Run one last iteration to consume any outstanding input events from Driver - // This is important for remaining OnKeyUp events. - RunIteration (ref state, ref firstIteration); - } - - /// Run one application iteration. - /// The state returned by . - /// - /// Set to if this is the first run loop iteration. Upon return, it - /// will be set to if at least one iteration happened. - /// - public static void RunIteration (ref RunState state, ref bool firstIteration) - { - if (MainLoop.Running && MainLoop.EventsPending ()) - { - // Notify Toplevel it's ready - if (firstIteration) - { - state.Toplevel.OnReady (); - } - - MainLoop.RunIteration (); - Iteration?.Invoke (null, new ()); - EnsureModalOrVisibleAlwaysOnTop (state.Toplevel); - - if (state.Toplevel != Current) - { - OverlappedTop?.OnDeactivate (state.Toplevel); - state.Toplevel = Current; - OverlappedTop?.OnActivate (state.Toplevel); - Top.SetSubViewNeedsDisplay (); - Refresh (); - } - } - - firstIteration = false; - - if (Current == null) - { - return; - } - - if (state.Toplevel != Top && (Top.NeedsDisplay || Top.SubViewNeedsDisplay || Top.LayoutNeeded)) - { - state.Toplevel.SetNeedsDisplay (state.Toplevel.Frame); - Top.Draw (); - - foreach (Toplevel top in _topLevels.Reverse ()) - { - if (top != Top && top != state.Toplevel) - { - top.SetNeedsDisplay (); - top.SetSubViewNeedsDisplay (); - top.Draw (); - } - } - } - - if (_topLevels.Count == 1 - && state.Toplevel == Top - && (Driver.Cols != state.Toplevel.Frame.Width - || Driver.Rows != state.Toplevel.Frame.Height) - && (state.Toplevel.NeedsDisplay - || state.Toplevel.SubViewNeedsDisplay - || state.Toplevel.LayoutNeeded)) - { - Driver.ClearContents (); - } - - if (state.Toplevel.NeedsDisplay || state.Toplevel.SubViewNeedsDisplay || state.Toplevel.LayoutNeeded || OverlappedChildNeedsDisplay ()) - { - state.Toplevel.SetNeedsDisplay (); - state.Toplevel.Draw (); - Driver.UpdateScreen (); - - //Driver.UpdateCursor (); - } - - if (PositionCursor (state.Toplevel)) - { - Driver.UpdateCursor (); - } - - // else - { - //if (PositionCursor (state.Toplevel)) - //{ - // Driver.Refresh (); - //} - //Driver.UpdateCursor (); - } - - if (state.Toplevel != Top && !state.Toplevel.Modal && (Top.NeedsDisplay || Top.SubViewNeedsDisplay || Top.LayoutNeeded)) - { - Top.Draw (); - } - } - - /// Stops the provided , causing or the if provided. - /// The to stop. - /// - /// This will cause to return. - /// - /// Calling is equivalent to setting the - /// property on the currently running to false. - /// - /// - public static void RequestStop (Toplevel top = null) - { - if (OverlappedTop is null || top is null || (OverlappedTop is null && top is { })) - { - top = Current; - } - - if (OverlappedTop != null - && top.IsOverlappedContainer - && top?.Running == true - && (Current?.Modal == false || (Current?.Modal == true && Current?.Running == false))) - { - OverlappedTop.RequestStop (); - } - else if (OverlappedTop != null - && top != Current - && Current?.Running == true - && Current?.Modal == true - && top.Modal - && top.Running) - { - var ev = new ToplevelClosingEventArgs (Current); - Current.OnClosing (ev); - - if (ev.Cancel) - { - return; - } - - ev = new (top); - top.OnClosing (ev); - - if (ev.Cancel) - { - return; - } - - Current.Running = false; - OnNotifyStopRunState (Current); - top.Running = false; - OnNotifyStopRunState (top); - } - else if ((OverlappedTop != null - && top != OverlappedTop - && top != Current - && Current?.Modal == false - && Current?.Running == true - && !top.Running) - || (OverlappedTop != null - && top != OverlappedTop - && top != Current - && Current?.Modal == false - && Current?.Running == false - && !top.Running - && _topLevels.ToArray () [1].Running)) - { - MoveCurrent (top); - } - else if (OverlappedTop != null - && Current != top - && Current?.Running == true - && !top.Running - && Current?.Modal == true - && top.Modal) - { - // The Current and the top are both modal so needed to set the Current.Running to false too. - Current.Running = false; - OnNotifyStopRunState (Current); - } - else if (OverlappedTop != null - && Current == top - && OverlappedTop?.Running == true - && Current?.Running == true - && top.Running - && Current?.Modal == true - && top.Modal) - { - // The OverlappedTop was requested to stop inside a modal Toplevel which is the Current and top, - // both are the same, so needed to set the Current.Running to false too. - Current.Running = false; - OnNotifyStopRunState (Current); - } - else - { - Toplevel currentTop; - - if (top == Current || (Current?.Modal == true && !top.Modal)) - { - currentTop = Current; - } - else - { - currentTop = top; - } - - if (!currentTop.Running) - { - return; - } - - var ev = new ToplevelClosingEventArgs (currentTop); - currentTop.OnClosing (ev); - - if (ev.Cancel) - { - return; - } - - currentTop.Running = false; - OnNotifyStopRunState (currentTop); - } - } - - private static void OnNotifyStopRunState (Toplevel top) - { - if (EndAfterFirstIteration) - { - NotifyStopRunState?.Invoke (top, new (top)); - } - } - - /// - /// Building block API: completes the execution of a that was started with - /// . - /// - /// The returned by the method. - public static void End (RunState runState) - { - ArgumentNullException.ThrowIfNull (runState); - - if (OverlappedTop is { }) - { - OverlappedTop.OnChildUnloaded (runState.Toplevel); - } - else - { - runState.Toplevel.OnUnloaded (); - } - - // End the RunState.Toplevel - // First, take it off the Toplevel Stack - if (_topLevels.Count > 0) - { - if (_topLevels.Peek () != runState.Toplevel) - { - // If the top of the stack is not the RunState.Toplevel then - // this call to End is not balanced with the call to Begin that started the RunState - throw new ArgumentException ("End must be balanced with calls to Begin"); - } - - _topLevels.Pop (); - } - - // Notify that it is closing - runState.Toplevel?.OnClosed (runState.Toplevel); - - // If there is a OverlappedTop that is not the RunState.Toplevel then RunState.Toplevel - // is a child of MidTop, and we should notify the OverlappedTop that it is closing - if (OverlappedTop is { } && !runState.Toplevel.Modal && runState.Toplevel != OverlappedTop) - { - OverlappedTop.OnChildClosed (runState.Toplevel); - } - - // Set Current and Top to the next TopLevel on the stack - if (_topLevels.Count == 0) - { - Current = null; - } - else - { - if (_topLevels.Count > 1 && _topLevels.Peek () == OverlappedTop && OverlappedChildren.Any (t => t.Visible) is { }) - { - OverlappedMoveNext (); - } - - Current = _topLevels.Peek (); - - if (_topLevels.Count == 1 && Current == OverlappedTop) - { - OverlappedTop.OnAllChildClosed (); - } - else - { - SetCurrentOverlappedAsTop (); - runState.Toplevel.OnLeave (Current); - Current.OnEnter (runState.Toplevel); - } - - Refresh (); - } - - // Don't dispose runState.Toplevel. It's up to caller dispose it - // If it's not the same as the current in the RunIteration, - // it will be fixed later in the next RunIteration. - if (OverlappedTop is { } && !_topLevels.Contains (OverlappedTop)) - { - _cachedRunStateToplevel = OverlappedTop; - } - else - { - _cachedRunStateToplevel = runState.Toplevel; - } - - runState.Toplevel = null; - runState.Dispose (); - } - - #endregion Run (Begin, Run, End) #region Toplevel handling @@ -1240,7 +150,7 @@ public static partial class Application /// The object used for the application on startup () /// The top. - public static Toplevel Top { get; private set; } + public static Toplevel? Top { get; private set; } /// /// The current object. This is updated in enters and leaves to @@ -1251,7 +161,7 @@ public static partial class Application /// Only relevant in scenarios where is . /// /// The current. - public static Toplevel Current { get; private set; } + public static Toplevel? Current { get; private set; } private static void EnsureModalOrVisibleAlwaysOnTop (Toplevel topLevel) { @@ -1263,7 +173,7 @@ public static partial class Application return; } - foreach (Toplevel top in _topLevels.Reverse ()) + foreach (Toplevel? top in _topLevels.Reverse ()) { if (top.Modal && top != Current) { @@ -1292,7 +202,7 @@ public static partial class Application int rx = location.X - start.Frame.X; int ry = location.Y - start.Frame.Y; - foreach (Toplevel t in _topLevels) + foreach (Toplevel? t in _topLevels) { if (t != Current) { @@ -1327,7 +237,7 @@ public static partial class Application #nullable enable // Only return true if the Current has changed. - private static bool MoveCurrent (Toplevel? top) + private static bool MoveCurrent (Toplevel top) { // The Current is modal and the top is not modal Toplevel then // the Current must be moved above the first not modal Toplevel. @@ -1343,9 +253,9 @@ public static partial class Application } var index = 0; - Toplevel [] savedToplevels = _topLevels.ToArray (); + Toplevel? [] savedToplevels = _topLevels.ToArray (); - foreach (Toplevel t in savedToplevels) + foreach (Toplevel? t in savedToplevels) { if (!t.Modal && t != Current && t != top && t != savedToplevels [index]) { @@ -1376,7 +286,7 @@ public static partial class Application var index = 0; - foreach (Toplevel t in _topLevels.ToArray ()) + foreach (Toplevel? t in _topLevels.ToArray ()) { if (!t.Running && t != Current && index > 0) { @@ -1505,6 +415,7 @@ public static partial class Application sb.AppendLine (); } + return sb.ToString (); } } diff --git a/Terminal.Gui/Clipboard/Clipboard.cs b/Terminal.Gui/Clipboard/Clipboard.cs index 63c1cc40a..5dccea0a4 100644 --- a/Terminal.Gui/Clipboard/Clipboard.cs +++ b/Terminal.Gui/Clipboard/Clipboard.cs @@ -31,11 +31,11 @@ public static class Clipboard { if (IsSupported) { - string clipData = Application.Driver.Clipboard.GetClipboardData (); + string clipData = Application.Driver?.Clipboard.GetClipboardData (); if (clipData is null) { - // throw new InvalidOperationException ($"{Application.Driver.GetType ().Name}.GetClipboardData returned null instead of string.Empty"); + // throw new InvalidOperationException ($"{Application.Driver?.GetType ().Name}.GetClipboardData returned null instead of string.Empty"); clipData = string.Empty; } @@ -60,7 +60,7 @@ public static class Clipboard value = string.Empty; } - Application.Driver.Clipboard.SetClipboardData (value); + Application.Driver?.Clipboard.SetClipboardData (value); } _contents = value; @@ -74,19 +74,16 @@ public static class Clipboard /// Returns true if the environmental dependencies are in place to interact with the OS clipboard. /// - public static bool IsSupported => Application.Driver.Clipboard.IsSupported; + public static bool IsSupported => Application.Driver?.Clipboard.IsSupported ?? false; /// Copies the _contents of the OS clipboard to if possible. /// The _contents of the OS clipboard if successful, if not. /// the OS clipboard was retrieved, otherwise. public static bool TryGetClipboardData (out string result) { - if (IsSupported && Application.Driver.Clipboard.TryGetClipboardData (out result)) + if (IsSupported && Application.Driver!.Clipboard.TryGetClipboardData (out result)) { - if (_contents != result) - { - _contents = result; - } + _contents = result; return true; } @@ -101,7 +98,7 @@ public static class Clipboard /// the OS clipboard was set, otherwise. public static bool TrySetClipboardData (string text) { - if (IsSupported && Application.Driver.Clipboard.TrySetClipboardData (text)) + if (IsSupported && Application.Driver!.Clipboard.TrySetClipboardData (text)) { _contents = text; @@ -155,7 +152,7 @@ internal static class ClipboardProcessRunner using (var process = new Process { - StartInfo = new ProcessStartInfo + StartInfo = new() { FileName = cmd, Arguments = arguments, @@ -191,17 +188,9 @@ internal static class ClipboardProcessRunner if (process.ExitCode > 0) { - output = $@"Process failed to run. Command line: { - cmd - } { - arguments - }. - Output: { - output - } - Error: { - process.StandardError.ReadToEnd () - }"; + output = $@"Process failed to run. Command line: {cmd} {arguments}. + Output: {output} + Error: {process.StandardError.ReadToEnd ()}"; } return (process.ExitCode, output); diff --git a/Terminal.Gui/Drawing/LineCanvas.cs b/Terminal.Gui/Drawing/LineCanvas.cs index 4b5119a82..9a7365f26 100644 --- a/Terminal.Gui/Drawing/LineCanvas.cs +++ b/Terminal.Gui/Drawing/LineCanvas.cs @@ -336,7 +336,7 @@ public class LineCanvas : IDisposable return Fill != null ? Fill.GetAttribute (intersects [0]!.Point) : intersects [0]!.Line.Attribute; } - private Cell? GetCellForIntersects (ConsoleDriver driver, IntersectionDefinition? [] intersects) + private Cell? GetCellForIntersects (ConsoleDriver? driver, IntersectionDefinition? [] intersects) { if (!intersects.Any ()) { @@ -356,7 +356,7 @@ public class LineCanvas : IDisposable return cell; } - private Rune? GetRuneForIntersects (ConsoleDriver driver, IntersectionDefinition? [] intersects) + private Rune? GetRuneForIntersects (ConsoleDriver? driver, IntersectionDefinition? [] intersects) { if (!intersects.Any ()) { @@ -679,7 +679,7 @@ public class LineCanvas : IDisposable internal Rune _thickV; public IntersectionRuneResolver () { SetGlyphs (); } - public Rune? GetRuneForIntersects (ConsoleDriver driver, IntersectionDefinition? [] intersects) + public Rune? GetRuneForIntersects (ConsoleDriver? driver, IntersectionDefinition? [] intersects) { bool useRounded = intersects.Any ( i => i?.Line.Length != 0 diff --git a/Terminal.Gui/Drawing/Ruler.cs b/Terminal.Gui/Drawing/Ruler.cs index 348036c84..d2551101d 100644 --- a/Terminal.Gui/Drawing/Ruler.cs +++ b/Terminal.Gui/Drawing/Ruler.cs @@ -39,8 +39,8 @@ public class Ruler _hTemplate.Repeat ((int)Math.Ceiling (Length + 2 / (double)_hTemplate.Length)) [start..(Length + start)]; // Top - Application.Driver.Move (location.X, location.Y); - Application.Driver.AddStr (hrule); + Application.Driver?.Move (location.X, location.Y); + Application.Driver?.AddStr (hrule); } else { @@ -50,8 +50,8 @@ public class Ruler for (int r = location.Y; r < location.Y + Length; r++) { - Application.Driver.Move (location.X, r); - Application.Driver.AddRune ((Rune)vrule [r - location.Y]); + Application.Driver?.Move (location.X, r); + Application.Driver?.AddRune ((Rune)vrule [r - location.Y]); } } } diff --git a/Terminal.Gui/Drawing/Thickness.cs b/Terminal.Gui/Drawing/Thickness.cs index 532c0af8a..ac6cc6cd6 100644 --- a/Terminal.Gui/Drawing/Thickness.cs +++ b/Terminal.Gui/Drawing/Thickness.cs @@ -119,20 +119,20 @@ public record struct Thickness // Draw the Top side if (Top > 0) { - Application.Driver.FillRect (rect with { Height = Math.Min (rect.Height, Top) }, topChar); + Application.Driver?.FillRect (rect with { Height = Math.Min (rect.Height, Top) }, topChar); } // Draw the Left side // Draw the Left side if (Left > 0) { - Application.Driver.FillRect (rect with { Width = Math.Min (rect.Width, Left) }, leftChar); + Application.Driver?.FillRect (rect with { Width = Math.Min (rect.Width, Left) }, leftChar); } // Draw the Right side if (Right > 0) { - Application.Driver.FillRect ( + Application.Driver?.FillRect ( rect with { X = Math.Max (0, rect.X + rect.Width - Right), @@ -145,7 +145,7 @@ public record struct Thickness // Draw the Bottom side if (Bottom > 0) { - Application.Driver.FillRect ( + Application.Driver?.FillRect ( rect with { Y = rect.Y + Math.Max (0, rect.Height - Bottom), @@ -197,7 +197,11 @@ public record struct Thickness VerticalAlignment = Alignment.End, AutoSize = true }; - tf.Draw (rect, Application.Driver.CurrentAttribute, Application.Driver.CurrentAttribute, rect); + + if (Application.Driver?.CurrentAttribute is { }) + { + tf.Draw (rect, Application.Driver!.CurrentAttribute, Application.Driver!.CurrentAttribute, rect); + } } return GetInside (rect); diff --git a/Terminal.Gui/Text/Autocomplete/AppendAutocomplete.cs b/Terminal.Gui/Text/Autocomplete/AppendAutocomplete.cs index f5f7190e6..2fa920e70 100644 --- a/Terminal.Gui/Text/Autocomplete/AppendAutocomplete.cs +++ b/Terminal.Gui/Text/Autocomplete/AppendAutocomplete.cs @@ -106,7 +106,7 @@ public class AppendAutocomplete : AutocompleteBase } // draw it like it's selected, even though it's not - Application.Driver.SetAttribute ( + Application.Driver?.SetAttribute ( new Attribute ( ColorScheme.Normal.Foreground, textField.ColorScheme.Focus.Background @@ -128,7 +128,7 @@ public class AppendAutocomplete : AutocompleteBase ); } - Application.Driver.AddStr (fragment); + Application.Driver?.AddStr (fragment); } /// diff --git a/Terminal.Gui/Text/Autocomplete/PopupAutocomplete.cs b/Terminal.Gui/Text/Autocomplete/PopupAutocomplete.cs index 93e32b12e..4dfeb8a95 100644 --- a/Terminal.Gui/Text/Autocomplete/PopupAutocomplete.cs +++ b/Terminal.Gui/Text/Autocomplete/PopupAutocomplete.cs @@ -376,18 +376,18 @@ public abstract partial class PopupAutocomplete : AutocompleteBase { if (i == SelectedIdx - ScrollOffset) { - Application.Driver.SetAttribute (ColorScheme.Focus); + Application.Driver?.SetAttribute (ColorScheme.Focus); } else { - Application.Driver.SetAttribute (ColorScheme.Normal); + Application.Driver?.SetAttribute (ColorScheme.Normal); } popup.Move (0, i); string text = TextFormatter.ClipOrPad (toRender [i].Title, width); - Application.Driver.AddStr (text); + Application.Driver?.AddStr (text); } } diff --git a/Terminal.Gui/View/Layout/Dim.cs b/Terminal.Gui/View/Layout/Dim.cs index e765414cb..7dfc6eb2e 100644 --- a/Terminal.Gui/View/Layout/Dim.cs +++ b/Terminal.Gui/View/Layout/Dim.cs @@ -139,7 +139,7 @@ public abstract class Dim /// Creates a object that tracks the Height of the specified . /// The height of the other . /// The view that will be tracked. - public static Dim Height (View view) { return new DimView (view, Dimension.Height); } + public static Dim Height (View? view) { return new DimView (view, Dimension.Height); } /// Creates a percentage object that is a percentage of the width or height of the SuperView. /// The percent object. @@ -171,7 +171,7 @@ public abstract class Dim /// Creates a object that tracks the Width of the specified . /// The width of the other . /// The view that will be tracked. - public static Dim Width (View view) { return new DimView (view, Dimension.Width); } + public static Dim Width (View? view) { return new DimView (view, Dimension.Width); } #endregion static Dim creation methods diff --git a/Terminal.Gui/View/Layout/DimView.cs b/Terminal.Gui/View/Layout/DimView.cs index 22c0d1f70..09ea96800 100644 --- a/Terminal.Gui/View/Layout/DimView.cs +++ b/Terminal.Gui/View/Layout/DimView.cs @@ -15,7 +15,7 @@ public class DimView : Dim /// /// The view the dimension is anchored to. /// Indicates which dimension is tracked. - public DimView (View view, Dimension dimension) + public DimView (View? view, Dimension dimension) { Target = view; Dimension = dimension; @@ -35,7 +35,7 @@ public class DimView : Dim /// /// Gets the View the dimension is anchored to. /// - public View Target { get; init; } + public View? Target { get; init; } /// public override string ToString () diff --git a/Terminal.Gui/View/Layout/Pos.cs b/Terminal.Gui/View/Layout/Pos.cs index 2213524ae..853bfa0ab 100644 --- a/Terminal.Gui/View/Layout/Pos.cs +++ b/Terminal.Gui/View/Layout/Pos.cs @@ -257,22 +257,22 @@ public abstract class Pos /// Creates a object that tracks the Top (Y) position of the specified . /// The that depends on the other view. /// The that will be tracked. - public static Pos Top (View view) { return new PosView (view, Side.Top); } + public static Pos Top (View? view) { return new PosView (view, Side.Top); } /// Creates a object that tracks the Top (Y) position of the specified . /// The that depends on the other view. /// The that will be tracked. - public static Pos Y (View view) { return new PosView (view, Side.Top); } + public static Pos Y (View? view) { return new PosView (view, Side.Top); } /// Creates a object that tracks the Left (X) position of the specified . /// The that depends on the other view. /// The that will be tracked. - public static Pos Left (View view) { return new PosView (view, Side.Left); } + public static Pos Left (View? view) { return new PosView (view, Side.Left); } /// Creates a object that tracks the Left (X) position of the specified . /// The that depends on the other view. /// The that will be tracked. - public static Pos X (View view) { return new PosView (view, Side.Left); } + public static Pos X (View? view) { return new PosView (view, Side.Left); } /// /// Creates a object that tracks the Bottom (Y+Height) coordinate of the specified @@ -280,7 +280,7 @@ public abstract class Pos /// /// The that depends on the other view. /// The that will be tracked. - public static Pos Bottom (View view) { return new PosView (view, Side.Bottom); } + public static Pos Bottom (View? view) { return new PosView (view, Side.Bottom); } /// /// Creates a object that tracks the Right (X+Width) coordinate of the specified @@ -288,7 +288,7 @@ public abstract class Pos /// /// The that depends on the other view. /// The that will be tracked. - public static Pos Right (View view) { return new PosView (view, Side.Right); } + public static Pos Right (View? view) { return new PosView (view, Side.Right); } #endregion static Pos creation methods diff --git a/Terminal.Gui/View/Layout/PosView.cs b/Terminal.Gui/View/Layout/PosView.cs index b48613307..fdf5bf784 100644 --- a/Terminal.Gui/View/Layout/PosView.cs +++ b/Terminal.Gui/View/Layout/PosView.cs @@ -12,12 +12,12 @@ namespace Terminal.Gui; /// /// The View the position is anchored to. /// The side of the View the position is anchored to. -public class PosView (View view, Side side) : Pos +public class PosView (View? view, Side side) : Pos { /// /// Gets the View the position is anchored to. /// - public View Target { get; } = view; + public View? Target { get; } = view; /// /// Gets the side of the View the position is anchored to. diff --git a/Terminal.Gui/View/Layout/ViewLayout.cs b/Terminal.Gui/View/Layout/ViewLayout.cs index fa0983993..637d1f0ca 100644 --- a/Terminal.Gui/View/Layout/ViewLayout.cs +++ b/Terminal.Gui/View/Layout/ViewLayout.cs @@ -398,7 +398,7 @@ public partial class View /// Either (if does not have a Super View) or /// 's SuperView. This can be used to ensure LayoutSubviews is called on the correct View. /// - internal static View GetLocationEnsuringFullVisibility ( + internal static View? GetLocationEnsuringFullVisibility ( View viewToMove, int targetX, int targetY, @@ -408,7 +408,7 @@ public partial class View ) { int maxDimension; - View superView; + View? superView; statusBar = null!; if (viewToMove?.SuperView is null || viewToMove == Application.Top || viewToMove?.SuperView == Application.Top) diff --git a/Terminal.Gui/View/ViewDrawing.cs b/Terminal.Gui/View/ViewDrawing.cs index 67778536f..73fa5d550 100644 --- a/Terminal.Gui/View/ViewDrawing.cs +++ b/Terminal.Gui/View/ViewDrawing.cs @@ -288,19 +288,19 @@ public partial class View public void DrawHotString (string text, Attribute hotColor, Attribute normalColor) { Rune hotkeySpec = HotKeySpecifier == (Rune)0xffff ? (Rune)'_' : HotKeySpecifier; - Application.Driver.SetAttribute (normalColor); + Application.Driver?.SetAttribute (normalColor); foreach (Rune rune in text.EnumerateRunes ()) { if (rune == new Rune (hotkeySpec.Value)) { - Application.Driver.SetAttribute (hotColor); + Application.Driver?.SetAttribute (hotColor); continue; } - Application.Driver.AddRune (rune); - Application.Driver.SetAttribute (normalColor); + Application.Driver?.AddRune (rune); + Application.Driver?.SetAttribute (normalColor); } } diff --git a/Terminal.Gui/Views/GraphView/Annotations.cs b/Terminal.Gui/Views/GraphView/Annotations.cs index 30247d4a6..7dbc8836e 100644 --- a/Terminal.Gui/Views/GraphView/Annotations.cs +++ b/Terminal.Gui/Views/GraphView/Annotations.cs @@ -133,7 +133,7 @@ public class LegendAnnotation : View, IAnnotation { if (!IsInitialized) { - ColorScheme = new ColorScheme { Normal = Application.Driver.GetAttribute () }; + ColorScheme = new ColorScheme { Normal = Application.Driver?.GetAttribute () ?? Attribute.Default}; graph.Add (this); } @@ -149,7 +149,7 @@ public class LegendAnnotation : View, IAnnotation { if (entry.Item1.Color.HasValue) { - Application.Driver.SetAttribute (entry.Item1.Color.Value); + Application.Driver?.SetAttribute (entry.Item1.Color.Value); } else { @@ -166,7 +166,7 @@ public class LegendAnnotation : View, IAnnotation Move (1, linesDrawn); string str = TextFormatter.ClipOrPad (entry.Item2, Viewport.Width - 1); - Application.Driver.AddStr (str); + Application.Driver?.AddStr (str); linesDrawn++; diff --git a/Terminal.Gui/Views/GraphView/Axis.cs b/Terminal.Gui/Views/GraphView/Axis.cs index efff79ce9..c46938890 100644 --- a/Terminal.Gui/Views/GraphView/Axis.cs +++ b/Terminal.Gui/Views/GraphView/Axis.cs @@ -103,7 +103,7 @@ public class HorizontalAxis : Axis graph.Move (screenPosition, y); // draw the tick on the axis - Application.Driver.AddRune (Glyphs.TopTee); + Application.Driver?.AddRune (Glyphs.TopTee); // and the label text if (!string.IsNullOrWhiteSpace (text)) @@ -161,7 +161,7 @@ public class HorizontalAxis : Axis } graph.Move (graph.Viewport.Width / 2 - toRender.Length / 2, graph.Viewport.Height - 1); - Application.Driver.AddStr (toRender); + Application.Driver?.AddStr (toRender); } } @@ -222,7 +222,7 @@ public class HorizontalAxis : Axis protected override void DrawAxisLine (GraphView graph, int x, int y) { graph.Move (x, y); - Application.Driver.AddRune (Glyphs.HLine); + Application.Driver?.AddRune (Glyphs.HLine); } private IEnumerable GetLabels (GraphView graph, Rectangle viewport) @@ -298,13 +298,13 @@ public class VerticalAxis : Axis graph.Move (x, screenPosition); // draw the tick on the axis - Application.Driver.AddRune (Glyphs.RightTee); + Application.Driver?.AddRune (Glyphs.RightTee); // and the label text if (!string.IsNullOrWhiteSpace (text)) { graph.Move (Math.Max (0, x - labelThickness), screenPosition); - Application.Driver.AddStr (text); + Application.Driver?.AddStr (text); } } @@ -342,7 +342,7 @@ public class VerticalAxis : Axis for (var i = 0; i < toRender.Length; i++) { graph.Move (0, startDrawingAtY + i); - Application.Driver.AddRune ((Rune)toRender [i]); + Application.Driver?.AddRune ((Rune)toRender [i]); } } } @@ -395,7 +395,7 @@ public class VerticalAxis : Axis protected override void DrawAxisLine (GraphView graph, int x, int y) { graph.Move (x, y); - Application.Driver.AddRune (Glyphs.VLine); + Application.Driver?.AddRune (Glyphs.VLine); } private int GetAxisYEnd (GraphView graph) diff --git a/Terminal.Gui/Views/GraphView/Series.cs b/Terminal.Gui/Views/GraphView/Series.cs index f0974556c..f7c02e174 100644 --- a/Terminal.Gui/Views/GraphView/Series.cs +++ b/Terminal.Gui/Views/GraphView/Series.cs @@ -33,7 +33,7 @@ public class ScatterSeries : ISeries { if (Fill.Color.HasValue) { - Application.Driver.SetAttribute (Fill.Color.Value); + Application.Driver?.SetAttribute (Fill.Color.Value); } foreach (PointF p in Points.Where (p => graphBounds.Contains (p))) @@ -261,7 +261,7 @@ public class BarSeries : ISeries if (adjusted.Color.HasValue) { - Application.Driver.SetAttribute (adjusted.Color.Value); + Application.Driver?.SetAttribute (adjusted.Color.Value); } graph.DrawLine (start, end, adjusted.Rune); diff --git a/Terminal.Gui/Views/Menu/ContextMenu.cs b/Terminal.Gui/Views/Menu/ContextMenu.cs index 8bcbc076a..f35cbdf34 100644 --- a/Terminal.Gui/Views/Menu/ContextMenu.cs +++ b/Terminal.Gui/Views/Menu/ContextMenu.cs @@ -144,7 +144,7 @@ public sealed class ContextMenu : IDisposable _container = Application.Current; _container.Closing += Container_Closing; _container.Deactivate += Container_Deactivate; - Rectangle frame = Application.Driver.Screen; + Rectangle frame = Application.Driver?.Screen ?? Rectangle.Empty; Point position = Position; if (Host is { }) diff --git a/Terminal.Gui/Views/RadioGroup.cs b/Terminal.Gui/Views/RadioGroup.cs index f0dc5174b..bb2996ba4 100644 --- a/Terminal.Gui/Views/RadioGroup.cs +++ b/Terminal.Gui/Views/RadioGroup.cs @@ -278,7 +278,7 @@ public class RadioGroup : View, IDesignable if (j == hotPos && i == _cursor) { - Application.Driver.SetAttribute ( + Application.Driver?.SetAttribute ( HasFocus ? ColorScheme.HotFocus : GetHotNormalColor () @@ -286,11 +286,11 @@ public class RadioGroup : View, IDesignable } else if (j == hotPos && i != _cursor) { - Application.Driver.SetAttribute (GetHotNormalColor ()); + Application.Driver?.SetAttribute (GetHotNormalColor ()); } else if (HasFocus && i == _cursor) { - Application.Driver.SetAttribute (ColorScheme.Focus); + Application.Driver?.SetAttribute (ColorScheme.Focus); } if (rune == HotKeySpecifier && j + 1 < rlRunes.Length) @@ -300,7 +300,7 @@ public class RadioGroup : View, IDesignable if (i == _cursor) { - Application.Driver.SetAttribute ( + Application.Driver?.SetAttribute ( HasFocus ? ColorScheme.HotFocus : GetHotNormalColor () @@ -308,11 +308,11 @@ public class RadioGroup : View, IDesignable } else if (i != _cursor) { - Application.Driver.SetAttribute (GetHotNormalColor ()); + Application.Driver?.SetAttribute (GetHotNormalColor ()); } } - Application.Driver.AddRune (rune); + Application.Driver?.AddRune (rune); Driver.SetAttribute (GetNormalColor ()); } } diff --git a/UICatalog/Scenarios/CombiningMarks.cs b/UICatalog/Scenarios/CombiningMarks.cs index b61ebb09f..78455eeca 100644 --- a/UICatalog/Scenarios/CombiningMarks.cs +++ b/UICatalog/Scenarios/CombiningMarks.cs @@ -15,20 +15,20 @@ public class CombiningMarks : Scenario top.DrawContentComplete += (s, e) => { - Application.Driver.Move (0, 0); - Application.Driver.AddStr ("Terminal.Gui only supports combining marks that normalize. See Issue #2616."); - Application.Driver.Move (0, 2); - Application.Driver.AddStr ("\u0301\u0301\u0328<- \"\\u301\\u301\\u328]\" using AddStr."); - Application.Driver.Move (0, 3); - Application.Driver.AddStr ("[a\u0301\u0301\u0328]<- \"[a\\u301\\u301\\u328]\" using AddStr."); - Application.Driver.Move (0, 4); - Application.Driver.AddRune ('['); - Application.Driver.AddRune ('a'); - Application.Driver.AddRune ('\u0301'); - Application.Driver.AddRune ('\u0301'); - Application.Driver.AddRune ('\u0328'); - Application.Driver.AddRune (']'); - Application.Driver.AddStr ("<- \"[a\\u301\\u301\\u328]\" using AddRune for each."); + Application.Driver?.Move (0, 0); + Application.Driver?.AddStr ("Terminal.Gui only supports combining marks that normalize. See Issue #2616."); + Application.Driver?.Move (0, 2); + Application.Driver?.AddStr ("\u0301\u0301\u0328<- \"\\u301\\u301\\u328]\" using AddStr."); + Application.Driver?.Move (0, 3); + Application.Driver?.AddStr ("[a\u0301\u0301\u0328]<- \"[a\\u301\\u301\\u328]\" using AddStr."); + Application.Driver?.Move (0, 4); + Application.Driver?.AddRune ('['); + Application.Driver?.AddRune ('a'); + Application.Driver?.AddRune ('\u0301'); + Application.Driver?.AddRune ('\u0301'); + Application.Driver?.AddRune ('\u0328'); + Application.Driver?.AddRune (']'); + Application.Driver?.AddStr ("<- \"[a\\u301\\u301\\u328]\" using AddRune for each."); }; Application.Run (top); diff --git a/UICatalog/Scenarios/Images.cs b/UICatalog/Scenarios/Images.cs index d31eae9cc..f17246742 100644 --- a/UICatalog/Scenarios/Images.cs +++ b/UICatalog/Scenarios/Images.cs @@ -20,9 +20,9 @@ public class Images : Scenario Application.Init (); var win = new Window { Title = $"{Application.QuitKey} to Quit - Scenario: {GetName()}" }; - bool canTrueColor = Application.Driver.SupportsTrueColor; + bool canTrueColor = Application.Driver?.SupportsTrueColor ?? false; - var lblDriverName = new Label { X = 0, Y = 0, Text = $"Driver is {Application.Driver.GetType ().Name}" }; + var lblDriverName = new Label { X = 0, Y = 0, Text = $"Driver is {Application.Driver?.GetType ().Name}" }; win.Add (lblDriverName); var cbSupportsTrueColor = new CheckBox diff --git a/UICatalog/Scenarios/SendKeys.cs b/UICatalog/Scenarios/SendKeys.cs index a27a80232..6dc4a3bdf 100644 --- a/UICatalog/Scenarios/SendKeys.cs +++ b/UICatalog/Scenarios/SendKeys.cs @@ -86,7 +86,7 @@ public class SendKeys : Scenario ? (ConsoleKey)char.ToUpper (r) : (ConsoleKey)r; - Application.Driver.SendKeys ( + Application.Driver?.SendKeys ( r, ck, ckbShift.State == CheckState.Checked, diff --git a/UICatalog/Scenarios/TextEffectsScenario.cs b/UICatalog/Scenarios/TextEffectsScenario.cs index 17f6a6e5c..7d5d0e156 100644 --- a/UICatalog/Scenarios/TextEffectsScenario.cs +++ b/UICatalog/Scenarios/TextEffectsScenario.cs @@ -260,5 +260,5 @@ internal class GradientsView : View } } - private static void SetColor (Color color) { Application.Driver.SetAttribute (new (color, color)); } + private static void SetColor (Color color) { Application.Driver?.SetAttribute (new (color, color)); } } diff --git a/UICatalog/Scenarios/TrueColors.cs b/UICatalog/Scenarios/TrueColors.cs index 19e00187d..d08d9685a 100644 --- a/UICatalog/Scenarios/TrueColors.cs +++ b/UICatalog/Scenarios/TrueColors.cs @@ -19,11 +19,11 @@ public class TrueColors : Scenario var x = 2; var y = 1; - bool canTrueColor = Application.Driver.SupportsTrueColor; + bool canTrueColor = Application.Driver?.SupportsTrueColor ?? false; var lblDriverName = new Label { - X = x, Y = y++, Text = $"Current driver is {Application.Driver.GetType ().Name}" + X = x, Y = y++, Text = $"Current driver is {Application.Driver?.GetType ().Name}" }; app.Add (lblDriverName); y++; diff --git a/UICatalog/Scenarios/VkeyPacketSimulator.cs b/UICatalog/Scenarios/VkeyPacketSimulator.cs index 50ce09b71..975775f45 100644 --- a/UICatalog/Scenarios/VkeyPacketSimulator.cs +++ b/UICatalog/Scenarios/VkeyPacketSimulator.cs @@ -198,7 +198,7 @@ public class VkeyPacketSimulator : Scenario char keyChar = ConsoleKeyMapping.EncodeKeyCharForVKPacket (consoleKeyInfo); - Application.Driver.SendKeys ( + Application.Driver?.SendKeys ( keyChar, ConsoleKey.Packet, consoleKeyInfo.Modifiers.HasFlag (ConsoleModifiers.Shift), diff --git a/UICatalog/UICatalog.cs b/UICatalog/UICatalog.cs index ddb4c656f..dd4184cb4 100644 --- a/UICatalog/UICatalog.cs +++ b/UICatalog/UICatalog.cs @@ -369,7 +369,7 @@ internal class UICatalogApp /// public class UICatalogTopLevel : Toplevel { - public ListView CategoryList; + public ListView? CategoryList; public MenuItem? MiForce16Colors; public MenuItem? MiIsMenuBorderDisabled; public MenuItem? MiIsMouseDisabled; @@ -999,7 +999,7 @@ internal class UICatalogApp Title = "Force _16 Colors", Shortcut = (KeyCode)Key.F6, Checked = Application.Force16Colors, - CanExecute = () => Application.Driver.SupportsTrueColor + CanExecute = () => Application.Driver?.SupportsTrueColor ?? false }; MiForce16Colors.CheckType |= MenuItemCheckStyle.Checked; diff --git a/UnitTests/Application/ApplicationTests.cs b/UnitTests/Application/ApplicationTests.cs index a7c1fc64a..6363233a9 100644 --- a/UnitTests/Application/ApplicationTests.cs +++ b/UnitTests/Application/ApplicationTests.cs @@ -44,7 +44,7 @@ public class ApplicationTests Toplevel top = new (); Application.Begin (top); Assert.Equal (new (0, 0, 80, 25), Application.Top.Frame); - ((FakeDriver)Application.Driver).SetBufferSize (5, 5); + ((FakeDriver)Application.Driver!).SetBufferSize (5, 5); Assert.Equal (new (0, 0, 5, 5), Application.Top.Frame); top.Dispose (); } @@ -134,7 +134,7 @@ public class ApplicationTests Application.Init (driverName: driverType.Name); Assert.NotNull (Application.Driver); Assert.NotEqual (driver, Application.Driver); - Assert.Equal (driverType, Application.Driver.GetType ()); + Assert.Equal (driverType, Application.Driver?.GetType ()); Shutdown (); } @@ -565,8 +565,8 @@ public class ApplicationTests Assert.NotNull (Application.MainLoop); // FakeDriver is always 80x25 - Assert.Equal (80, Application.Driver.Cols); - Assert.Equal (25, Application.Driver.Rows); + Assert.Equal (80, Application.Driver!.Cols); + Assert.Equal (25, Application.Driver!.Rows); } private void Pre_Init_State () @@ -695,7 +695,7 @@ public class ApplicationTests Application.ForceDriver = "FakeDriver"; Application.Init (); - Assert.Equal (typeof (FakeDriver), Application.Driver.GetType ()); + Assert.Equal (typeof (FakeDriver), Application.Driver?.GetType ()); Application.Iteration += (s, a) => { Application.RequestStop (); }; @@ -737,7 +737,7 @@ public class ApplicationTests Application.Iteration += (s, a) => { Application.RequestStop (); }; Application.Run (); - Assert.Equal (typeof (FakeDriver), Application.Driver.GetType ()); + Assert.Equal (typeof (FakeDriver), Application.Driver?.GetType ()); Application.Top.Dispose (); Shutdown (); @@ -888,7 +888,7 @@ public class ApplicationTests Width = 5, Height = 5, Arrangement = ViewArrangement.Movable }; - ((FakeDriver)Application.Driver).SetBufferSize (10, 10); + ((FakeDriver)Application.Driver!).SetBufferSize (10, 10); RunState rs = Application.Begin (w); // Don't use visuals to test as style of border can change over time. diff --git a/UnitTests/Application/CursorTests.cs b/UnitTests/Application/CursorTests.cs index 337003b0f..87999a9d2 100644 --- a/UnitTests/Application/CursorTests.cs +++ b/UnitTests/Application/CursorTests.cs @@ -141,7 +141,10 @@ public class CursorTests Assert.True (view.HasFocus); Assert.False (Application.PositionCursor (view)); - Application.Driver.GetCursorVisibility (out CursorVisibility cursor); - Assert.Equal (CursorVisibility.Invisible, cursor); + + if (Application.Driver?.GetCursorVisibility (out CursorVisibility cursor) ?? false) + { + Assert.Equal (CursorVisibility.Invisible, cursor); + } } } diff --git a/UnitTests/Clipboard/ClipboardTests.cs b/UnitTests/Clipboard/ClipboardTests.cs index 65c2e7707..e2c0ac11f 100644 --- a/UnitTests/Clipboard/ClipboardTests.cs +++ b/UnitTests/Clipboard/ClipboardTests.cs @@ -9,14 +9,14 @@ public class ClipboardTests [Fact, AutoInitShutdown (useFakeClipboard: true, fakeClipboardAlwaysThrowsNotSupportedException: true)] public void IClipboard_GetClipBoardData_Throws_NotSupportedException () { - var iclip = Application.Driver.Clipboard; + var iclip = Application.Driver?.Clipboard; Assert.Throws (() => iclip.GetClipboardData ()); } [Fact, AutoInitShutdown (useFakeClipboard: true, fakeClipboardAlwaysThrowsNotSupportedException: true)] public void IClipboard_SetClipBoardData_Throws_NotSupportedException () { - var iclip = Application.Driver.Clipboard; + var iclip = Application.Driver?.Clipboard; Assert.Throws (() => iclip.SetClipboardData ("foo")); } diff --git a/UnitTests/ConsoleDrivers/ClipRegionTests.cs b/UnitTests/ConsoleDrivers/ClipRegionTests.cs index 0d27f91c1..8a90f2e4d 100644 --- a/UnitTests/ConsoleDrivers/ClipRegionTests.cs +++ b/UnitTests/ConsoleDrivers/ClipRegionTests.cs @@ -26,8 +26,8 @@ public class ClipRegionTests { var driver = (ConsoleDriver)Activator.CreateInstance (driverType); Application.Init (driver); - Application.Driver.Rows = 25; - Application.Driver.Cols = 80; + Application.Driver!.Rows = 25; + Application.Driver!.Cols = 80; driver.Move (0, 0); driver.AddRune ('x'); @@ -94,8 +94,8 @@ public class ClipRegionTests { var driver = (ConsoleDriver)Activator.CreateInstance (driverType); Application.Init (driver); - Application.Driver.Rows = 10; - Application.Driver.Cols = 10; + Application.Driver!.Rows = 10; + Application.Driver!.Cols = 10; // positive Assert.True (driver.IsValidLocation (0, 0)); diff --git a/UnitTests/ConsoleDrivers/ConsoleDriverTests.cs b/UnitTests/ConsoleDrivers/ConsoleDriverTests.cs index afbf20d96..8ecc97807 100644 --- a/UnitTests/ConsoleDrivers/ConsoleDriverTests.cs +++ b/UnitTests/ConsoleDrivers/ConsoleDriverTests.cs @@ -234,7 +234,7 @@ public class ConsoleDriverTests // { // var win = new Window (); // Application.Begin (win); - // ((FakeDriver)Application.Driver).SetBufferSize (20, 8); + // ((FakeDriver)Application.Driver!).SetBufferSize (20, 8); // System.Threading.Tasks.Task.Run (() => { // System.Threading.Tasks.Task.Delay (500).Wait (); diff --git a/UnitTests/ConsoleDrivers/ConsoleKeyMappingTests.cs b/UnitTests/ConsoleDrivers/ConsoleKeyMappingTests.cs index de05b868f..1ea984b3d 100644 --- a/UnitTests/ConsoleDrivers/ConsoleKeyMappingTests.cs +++ b/UnitTests/ConsoleDrivers/ConsoleKeyMappingTests.cs @@ -123,7 +123,7 @@ public class ConsoleKeyMappingTests if (iterations == 0) { var keyChar = ConsoleKeyMapping.EncodeKeyCharForVKPacket (consoleKeyInfo); - Application.Driver.SendKeys (keyChar, ConsoleKey.Packet, shift, alt, control); + Application.Driver?.SendKeys (keyChar, ConsoleKey.Packet, shift, alt, control); } }; Application.Run (); diff --git a/UnitTests/Dialogs/MessageBoxTests.cs b/UnitTests/Dialogs/MessageBoxTests.cs index f32f7074a..8715eea2c 100644 --- a/UnitTests/Dialogs/MessageBoxTests.cs +++ b/UnitTests/Dialogs/MessageBoxTests.cs @@ -125,7 +125,7 @@ public class MessageBoxTests public void Location_Default () { int iterations = -1; - ((FakeDriver)Application.Driver).SetBufferSize (100, 100); + ((FakeDriver)Application.Driver!).SetBufferSize (100, 100); Application.Iteration += (s, a) => { @@ -243,7 +243,7 @@ public class MessageBoxTests int iterations = -1; var top = new Toplevel (); top.BorderStyle = LineStyle.None; - ((FakeDriver)Application.Driver).SetBufferSize (20, 10); + ((FakeDriver)Application.Driver!).SetBufferSize (20, 10); var btn = $"{ @@ -319,7 +319,7 @@ public class MessageBoxTests int iterations = -1; var top = new Toplevel (); top.BorderStyle = LineStyle.None; - ((FakeDriver)Application.Driver).SetBufferSize (20, 10); + ((FakeDriver)Application.Driver!).SetBufferSize (20, 10); var btn = $"{ @@ -396,7 +396,7 @@ ffffffffffffffffffff int iterations = -1; var top = new Toplevel(); top.BorderStyle = LineStyle.None; - ((FakeDriver)Application.Driver).SetBufferSize (20, 10); + ((FakeDriver)Application.Driver!).SetBufferSize (20, 10); var btn = $"{ @@ -477,7 +477,7 @@ ffffffffffffffffffff int iterations = -1; var top = new Toplevel(); top.BorderStyle = LineStyle.None; - ((FakeDriver)Application.Driver).SetBufferSize (20, 10); + ((FakeDriver)Application.Driver!).SetBufferSize (20, 10); var btn = $"{ @@ -547,7 +547,7 @@ ffffffffffffffffffff public void Size_Default () { int iterations = -1; - ((FakeDriver)Application.Driver).SetBufferSize (100, 100); + ((FakeDriver)Application.Driver!).SetBufferSize (100, 100); Application.Iteration += (s, a) => { @@ -650,7 +650,7 @@ ffffffffffffffffffff CM.Glyphs.RightBracket }"; - ((FakeDriver)Application.Driver).SetBufferSize (40 + 4, 8); + ((FakeDriver)Application.Driver!).SetBufferSize (40 + 4, 8); Application.Iteration += (s, a) => { @@ -737,7 +737,7 @@ ffffffffffffffffffff public void Size_Not_Default_Message (int height, int width, string message) { int iterations = -1; - ((FakeDriver)Application.Driver).SetBufferSize (100, 100); + ((FakeDriver)Application.Driver!).SetBufferSize (100, 100); Application.Iteration += (s, a) => { @@ -774,7 +774,7 @@ ffffffffffffffffffff public void Size_Not_Default_Message_Button (int height, int width, string message) { int iterations = -1; - ((FakeDriver)Application.Driver).SetBufferSize (100, 100); + ((FakeDriver)Application.Driver!).SetBufferSize (100, 100); Application.Iteration += (s, a) => { @@ -807,7 +807,7 @@ ffffffffffffffffffff public void Size_Not_Default_No_Message (int height, int width) { int iterations = -1; - ((FakeDriver)Application.Driver).SetBufferSize (100, 100); + ((FakeDriver)Application.Driver!).SetBufferSize (100, 100); Application.Iteration += (s, a) => { diff --git a/UnitTests/Drawing/RulerTests.cs b/UnitTests/Drawing/RulerTests.cs index 0fdbfd7e2..d43ea327f 100644 --- a/UnitTests/Drawing/RulerTests.cs +++ b/UnitTests/Drawing/RulerTests.cs @@ -29,7 +29,7 @@ public class RulerTests [AutoInitShutdown] public void Draw_Default () { - ((FakeDriver)Application.Driver).SetBufferSize (25, 25); + ((FakeDriver)Application.Driver!).SetBufferSize (25, 25); var r = new Ruler (); r.Draw (Point.Empty); @@ -47,7 +47,7 @@ public class RulerTests var top = new Toplevel (); top.Add (f); Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (len + 5, 5); + ((FakeDriver)Application.Driver!).SetBufferSize (len + 5, 5); Assert.Equal (new (0, 0, len + 5, 5), f.Frame); var r = new Ruler (); @@ -121,7 +121,7 @@ public class RulerTests var top = new Toplevel (); top.Add (f); Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (len + 5, 5); + ((FakeDriver)Application.Driver!).SetBufferSize (len + 5, 5); Assert.Equal (new (0, 0, len + 5, 5), f.Frame); var r = new Ruler (); @@ -168,7 +168,7 @@ public class RulerTests var top = new Toplevel (); top.Add (f); Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (5, len + 5); + ((FakeDriver)Application.Driver!).SetBufferSize (5, len + 5); Assert.Equal (new (0, 0, 5, len + 5), f.Frame); var r = new Ruler (); @@ -302,7 +302,7 @@ public class RulerTests var top = new Toplevel (); top.Add (f); Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (5, len + 5); + ((FakeDriver)Application.Driver!).SetBufferSize (5, len + 5); Assert.Equal (new (0, 0, 5, len + 5), f.Frame); var r = new Ruler (); diff --git a/UnitTests/Drawing/ThicknessTests.cs b/UnitTests/Drawing/ThicknessTests.cs index 8215f8148..c71135722 100644 --- a/UnitTests/Drawing/ThicknessTests.cs +++ b/UnitTests/Drawing/ThicknessTests.cs @@ -51,13 +51,13 @@ public class ThicknessTests (ITestOutputHelper output) [AutoInitShutdown] public void DrawTests () { - ((FakeDriver)Application.Driver).SetBufferSize (60, 60); + ((FakeDriver)Application.Driver!).SetBufferSize (60, 60); var t = new Thickness (0, 0, 0, 0); var r = new Rectangle (5, 5, 40, 15); View.Diagnostics |= ViewDiagnosticFlags.Padding; - Application.Driver.FillRect ( - new Rectangle (0, 0, Application.Driver.Cols, Application.Driver.Rows), + Application.Driver?.FillRect ( + new Rectangle (0, 0, Application.Driver!.Cols, Application.Driver!.Rows), (Rune)' ' ); t.Draw (r, "Test"); @@ -73,8 +73,8 @@ public class ThicknessTests (ITestOutputHelper output) r = new Rectangle (5, 5, 40, 15); View.Diagnostics |= ViewDiagnosticFlags.Padding; - Application.Driver.FillRect ( - new Rectangle (0, 0, Application.Driver.Cols, Application.Driver.Rows), + Application.Driver?.FillRect ( + new Rectangle (0, 0, Application.Driver!.Cols, Application.Driver!.Rows), (Rune)' ' ); t.Draw (r, "Test"); @@ -104,8 +104,8 @@ public class ThicknessTests (ITestOutputHelper output) r = new Rectangle (5, 5, 40, 15); View.Diagnostics |= ViewDiagnosticFlags.Padding; - Application.Driver.FillRect ( - new Rectangle (0, 0, Application.Driver.Cols, Application.Driver.Rows), + Application.Driver?.FillRect ( + new Rectangle (0, 0, Application.Driver!.Cols, Application.Driver!.Rows), (Rune)' ' ); t.Draw (r, "Test"); @@ -135,8 +135,8 @@ public class ThicknessTests (ITestOutputHelper output) r = new Rectangle (5, 5, 40, 15); View.Diagnostics |= ViewDiagnosticFlags.Padding; - Application.Driver.FillRect ( - new Rectangle (0, 0, Application.Driver.Cols, Application.Driver.Rows), + Application.Driver?.FillRect ( + new Rectangle (0, 0, Application.Driver!.Cols, Application.Driver!.Rows), (Rune)' ' ); t.Draw (r, "Test"); @@ -174,7 +174,7 @@ public class ThicknessTests (ITestOutputHelper output) top.Add (f); Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (45, 20); + ((FakeDriver)Application.Driver!).SetBufferSize (45, 20); var t = new Thickness (0, 0, 0, 0); var r = new Rectangle (2, 2, 40, 15); Application.Refresh (); diff --git a/UnitTests/FileServices/FileDialogTests.cs b/UnitTests/FileServices/FileDialogTests.cs index 1395543ee..1488a75f0 100644 --- a/UnitTests/FileServices/FileDialogTests.cs +++ b/UnitTests/FileServices/FileDialogTests.cs @@ -701,14 +701,14 @@ public class FileDialogTests (ITestOutputHelper output) private void Send (char ch, ConsoleKey ck, bool shift = false, bool alt = false, bool control = false) { - Application.Driver.SendKeys (ch, ck, shift, alt, control); + Application.Driver?.SendKeys (ch, ck, shift, alt, control); } private void Send (string chars) { foreach (char ch in chars) { - Application.Driver.SendKeys (ch, ConsoleKey.NoName, false, false, false); + Application.Driver?.SendKeys (ch, ConsoleKey.NoName, false, false, false); } } diff --git a/UnitTests/Text/TextFormatterTests.cs b/UnitTests/Text/TextFormatterTests.cs index 6746f8ad7..f821d504b 100644 --- a/UnitTests/Text/TextFormatterTests.cs +++ b/UnitTests/Text/TextFormatterTests.cs @@ -451,7 +451,7 @@ ssb [SetupFakeDriver] public void FillRemaining_True_False () { - ((FakeDriver)Application.Driver).SetBufferSize (22, 5); + ((FakeDriver)Application.Driver!).SetBufferSize (22, 5); Attribute [] attrs = { @@ -6041,7 +6041,7 @@ B")] Text = text }; - Application.Driver.FillRect (new Rectangle (0, 0, 7, 7), (Rune)'*'); + Application.Driver?.FillRect (new Rectangle (0, 0, 7, 7), (Rune)'*'); tf.Draw (new Rectangle (0, 0, 7, 7), Attribute.Default, Attribute.Default); TestHelpers.AssertDriverContentsWithFrameAre (expectedText, _output); } diff --git a/UnitTests/View/Adornment/BorderTests.cs b/UnitTests/View/Adornment/BorderTests.cs index 387844dbe..cae90f708 100644 --- a/UnitTests/View/Adornment/BorderTests.cs +++ b/UnitTests/View/Adornment/BorderTests.cs @@ -95,7 +95,7 @@ public class BorderTests (ITestOutputHelper output) RunState rs = Application.Begin (win); var firstIteration = false; - ((FakeDriver)Application.Driver).SetBufferSize (width, 5); + ((FakeDriver)Application.Driver!).SetBufferSize (width, 5); Application.RunIteration (ref rs, ref firstIteration); var expected = string.Empty; @@ -229,7 +229,7 @@ public class BorderTests (ITestOutputHelper output) RunState rs = Application.Begin (win); var firstIteration = false; - ((FakeDriver)Application.Driver).SetBufferSize (width, 4); + ((FakeDriver)Application.Driver!).SetBufferSize (width, 4); Application.RunIteration (ref rs, ref firstIteration); var expected = string.Empty; @@ -363,7 +363,7 @@ public class BorderTests (ITestOutputHelper output) RunState rs = Application.Begin (win); var firstIteration = false; - ((FakeDriver)Application.Driver).SetBufferSize (width, 4); + ((FakeDriver)Application.Driver!).SetBufferSize (width, 4); Application.RunIteration (ref rs, ref firstIteration); var expected = string.Empty; @@ -486,7 +486,7 @@ public class BorderTests (ITestOutputHelper output) RunState rs = Application.Begin (win); var firstIteration = false; - ((FakeDriver)Application.Driver).SetBufferSize (20, height); + ((FakeDriver)Application.Driver!).SetBufferSize (20, height); Application.RunIteration (ref rs, ref firstIteration); var expected = string.Empty; @@ -548,7 +548,7 @@ public class BorderTests (ITestOutputHelper output) RunState rs = Application.Begin (win); var firstIteration = false; - ((FakeDriver)Application.Driver).SetBufferSize (width, 3); + ((FakeDriver)Application.Driver!).SetBufferSize (width, 3); Application.RunIteration (ref rs, ref firstIteration); var expected = string.Empty; @@ -728,7 +728,7 @@ public class BorderTests (ITestOutputHelper output) RunState rs = Application.Begin (top); var firstIteration = false; - ((FakeDriver)Application.Driver).SetBufferSize (5, 5); + ((FakeDriver)Application.Driver!).SetBufferSize (5, 5); Application.RunIteration (ref rs, ref firstIteration); var expected = @" @@ -756,7 +756,7 @@ public class BorderTests (ITestOutputHelper output) RunState rs = Application.Begin (top); var firstIteration = false; - ((FakeDriver)Application.Driver).SetBufferSize (10, 4); + ((FakeDriver)Application.Driver!).SetBufferSize (10, 4); Application.RunIteration (ref rs, ref firstIteration); var expected = @" @@ -779,7 +779,7 @@ public class BorderTests (ITestOutputHelper output) RunState rs = Application.Begin (win); var firstIteration = false; - ((FakeDriver)Application.Driver).SetBufferSize (3, 3); + ((FakeDriver)Application.Driver!).SetBufferSize (3, 3); Application.RunIteration (ref rs, ref firstIteration); var expected = @" diff --git a/UnitTests/View/Adornment/MarginTests.cs b/UnitTests/View/Adornment/MarginTests.cs index 1cfe6f0d1..736a720b1 100644 --- a/UnitTests/View/Adornment/MarginTests.cs +++ b/UnitTests/View/Adornment/MarginTests.cs @@ -8,7 +8,7 @@ public class MarginTests (ITestOutputHelper output) [SetupFakeDriver] public void Margin_Uses_SuperView_ColorScheme () { - ((FakeDriver)Application.Driver).SetBufferSize (5, 5); + ((FakeDriver)Application.Driver!).SetBufferSize (5, 5); var view = new View { Height = 3, Width = 3 }; view.Margin.Thickness = new (1); diff --git a/UnitTests/View/Adornment/PaddingTests.cs b/UnitTests/View/Adornment/PaddingTests.cs index 4f7bffb20..2c917572f 100644 --- a/UnitTests/View/Adornment/PaddingTests.cs +++ b/UnitTests/View/Adornment/PaddingTests.cs @@ -8,7 +8,7 @@ public class PaddingTests (ITestOutputHelper output) [SetupFakeDriver] public void Padding_Uses_Parent_ColorScheme () { - ((FakeDriver)Application.Driver).SetBufferSize (5, 5); + ((FakeDriver)Application.Driver!).SetBufferSize (5, 5); var view = new View { Height = 3, Width = 3 }; view.Padding.Thickness = new (1); diff --git a/UnitTests/View/DrawTests.cs b/UnitTests/View/DrawTests.cs index 46c791ab4..52b4659f8 100644 --- a/UnitTests/View/DrawTests.cs +++ b/UnitTests/View/DrawTests.cs @@ -22,13 +22,13 @@ public class DrawTests (ITestOutputHelper _output) // Only valid location w/in Viewport is 0, 0 (view) - 2, 2 (screen) view.Move (0, 0); - Assert.Equal (new Point (2, 2), new Point (Application.Driver.Col, Application.Driver.Row)); + Assert.Equal (new Point (2, 2), new Point (Application.Driver!.Col, Application.Driver!.Row)); view.Move (-1, -1); - Assert.Equal (new Point (2, 2), new Point (Application.Driver.Col, Application.Driver.Row)); + Assert.Equal (new Point (2, 2), new Point (Application.Driver!.Col, Application.Driver!.Row)); view.Move (1, 1); - Assert.Equal (new Point (2, 2), new Point (Application.Driver.Col, Application.Driver.Row)); + Assert.Equal (new Point (2, 2), new Point (Application.Driver!.Col, Application.Driver!.Row)); } [Fact] @@ -48,16 +48,16 @@ public class DrawTests (ITestOutputHelper _output) view.Draw (); // Only valid location w/in Viewport is 0, 0 (view) - 2, 2 (screen) - Assert.Equal ((Rune)' ', Application.Driver.Contents [2, 2].Rune); + Assert.Equal ((Rune)' ', Application.Driver?.Contents [2, 2].Rune); view.AddRune (0, 0, Rune.ReplacementChar); - Assert.Equal (Rune.ReplacementChar, Application.Driver.Contents [2, 2].Rune); + Assert.Equal (Rune.ReplacementChar, Application.Driver?.Contents [2, 2].Rune); view.AddRune (-1, -1, Rune.ReplacementChar); - Assert.Equal ((Rune)'M', Application.Driver.Contents [1, 1].Rune); + Assert.Equal ((Rune)'M', Application.Driver?.Contents [1, 1].Rune); view.AddRune (1, 1, Rune.ReplacementChar); - Assert.Equal ((Rune)'M', Application.Driver.Contents [3, 3].Rune); + Assert.Equal ((Rune)'M', Application.Driver?.Contents [3, 3].Rune); View.Diagnostics = ViewDiagnosticFlags.Off; } @@ -250,7 +250,7 @@ public class DrawTests (ITestOutputHelper _output) top.Add (win); Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (10, 4); + ((FakeDriver)Application.Driver!).SetBufferSize (10, 4); const string expectedOutput = """ @@ -301,7 +301,7 @@ public class DrawTests (ITestOutputHelper _output) dg.Add (view); RunState rsTop = Application.Begin (top); RunState rsDiag = Application.Begin (dg); - ((FakeDriver)Application.Driver).SetBufferSize (30, 10); + ((FakeDriver)Application.Driver!).SetBufferSize (30, 10); const string expectedOutput = """ @@ -354,7 +354,7 @@ public class DrawTests (ITestOutputHelper _output) top.Add (viewRight, viewBottom); Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (7, 7); + ((FakeDriver)Application.Driver!).SetBufferSize (7, 7); TestHelpers.AssertDriverContentsWithFrameAre ( """ @@ -394,7 +394,7 @@ public class DrawTests (ITestOutputHelper _output) var view = new View { Width = 2, Height = 2, BorderStyle = LineStyle.Single }; view.BeginInit (); view.EndInit (); - view.SetRelativeLayout (Application.Driver.Screen.Size); + view.SetRelativeLayout (Application.Driver!.Screen.Size); Assert.Equal (new (0, 0, 2, 2), view.Frame); Assert.Equal (Rectangle.Empty, view.Viewport); @@ -419,7 +419,7 @@ public class DrawTests (ITestOutputHelper _output) view.Border.Thickness = new Thickness (1, 1, 1, 0); view.BeginInit (); view.EndInit (); - view.SetRelativeLayout (Application.Driver.Screen.Size); + view.SetRelativeLayout (Application.Driver!.Screen.Size); Assert.Equal (new (0, 0, 2, 1), view.Frame); Assert.Equal (Rectangle.Empty, view.Viewport); @@ -437,7 +437,7 @@ public class DrawTests (ITestOutputHelper _output) view.Border.Thickness = new Thickness (0, 1, 1, 1); view.BeginInit (); view.EndInit (); - view.SetRelativeLayout (Application.Driver.Screen.Size); + view.SetRelativeLayout (Application.Driver!.Screen.Size); Assert.Equal (new (0, 0, 1, 2), view.Frame); Assert.Equal (Rectangle.Empty, view.Viewport); @@ -462,7 +462,7 @@ public class DrawTests (ITestOutputHelper _output) view.Border.Thickness = new Thickness (1, 1, 0, 1); view.BeginInit (); view.EndInit (); - view.SetRelativeLayout (Application.Driver.Screen.Size); + view.SetRelativeLayout (Application.Driver!.Screen.Size); Assert.Equal (new (0, 0, 1, 2), view.Frame); Assert.Equal (Rectangle.Empty, view.Viewport); @@ -488,7 +488,7 @@ public class DrawTests (ITestOutputHelper _output) view.BeginInit (); view.EndInit (); - view.SetRelativeLayout (Application.Driver.Screen.Size); + view.SetRelativeLayout (Application.Driver!.Screen.Size); Assert.Equal (new (0, 0, 2, 1), view.Frame); Assert.Equal (Rectangle.Empty, view.Viewport); @@ -561,7 +561,7 @@ public class DrawTests (ITestOutputHelper _output) container.Add (content); Toplevel top = new (); top.Add (container); - Application.Driver.Clip = container.Frame; + Application.Driver!.Clip = container.Frame; Application.Begin (top); TestHelpers.AssertDriverContentsWithFrameAre ( @@ -727,7 +727,7 @@ public class DrawTests (ITestOutputHelper _output) return; - void Top_LayoutComplete (object? sender, LayoutEventArgs e) { Application.Driver.Clip = container.Frame; } + void Top_LayoutComplete (object? sender, LayoutEventArgs e) { Application.Driver!.Clip = container.Frame; } } [Fact] @@ -767,7 +767,7 @@ public class DrawTests (ITestOutputHelper _output) container.Add (content); Toplevel top = new (); top.Add (container); - Application.Driver.Clip = container.Frame; + Application.Driver!.Clip = container.Frame; Application.Begin (top); TestHelpers.AssertDriverContentsWithFrameAre ( @@ -889,7 +889,7 @@ public class DrawTests (ITestOutputHelper _output) top.Add (win); Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (10, 4); + ((FakeDriver)Application.Driver!).SetBufferSize (10, 4); var expected = """ @@ -928,13 +928,13 @@ public class DrawTests (ITestOutputHelper _output) view.Border.Thickness = new Thickness (1); view.BeginInit (); view.EndInit (); - Assert.Equal (view.Frame, Application.Driver.Clip); + Assert.Equal (view.Frame, Application.Driver?.Clip); // Act view.SetClip (); // Assert - Assert.Equal (expectedClip, Application.Driver.Clip); + Assert.Equal (expectedClip, Application.Driver?.Clip); view.Dispose (); } @@ -960,14 +960,14 @@ public class DrawTests (ITestOutputHelper _output) view.Border.Thickness = new Thickness (1); view.BeginInit (); view.EndInit (); - Assert.Equal (view.Frame, Application.Driver.Clip); + Assert.Equal (view.Frame, Application.Driver?.Clip); view.Viewport = view.Viewport with { X = 1, Y = 1 }; // Act view.SetClip (); // Assert - Assert.Equal (expectedClip, Application.Driver.Clip); + Assert.Equal (expectedClip, Application.Driver?.Clip); view.Dispose (); } diff --git a/UnitTests/View/Layout/Dim.FillTests.cs b/UnitTests/View/Layout/Dim.FillTests.cs index c4b4ebacf..cdda3088d 100644 --- a/UnitTests/View/Layout/Dim.FillTests.cs +++ b/UnitTests/View/Layout/Dim.FillTests.cs @@ -14,7 +14,7 @@ public class DimFillTests (ITestOutputHelper output) var top = new Toplevel (); top.Add (view); RunState rs = Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (32, 5); + ((FakeDriver)Application.Driver!).SetBufferSize (32, 5); //view.SetNeedsLayout (); top.LayoutSubviews (); diff --git a/UnitTests/View/Layout/Pos.AnchorEndTests.cs b/UnitTests/View/Layout/Pos.AnchorEndTests.cs index 4309ee858..ddd3c62af 100644 --- a/UnitTests/View/Layout/Pos.AnchorEndTests.cs +++ b/UnitTests/View/Layout/Pos.AnchorEndTests.cs @@ -184,7 +184,7 @@ public class PosAnchorEndTests (ITestOutputHelper output) [SetupFakeDriver] public void PosAnchorEnd_View_And_Button () { - ((FakeDriver)Application.Driver).SetBufferSize (20, 5); + ((FakeDriver)Application.Driver!).SetBufferSize (20, 5); var b = $"{CM.Glyphs.LeftBracket} Ok {CM.Glyphs.RightBracket}"; diff --git a/UnitTests/View/Layout/Pos.CenterTests.cs b/UnitTests/View/Layout/Pos.CenterTests.cs index a17b1132a..e713c07b6 100644 --- a/UnitTests/View/Layout/Pos.CenterTests.cs +++ b/UnitTests/View/Layout/Pos.CenterTests.cs @@ -85,7 +85,7 @@ public class PosCenterTests (ITestOutputHelper output) RunState rs = Application.Begin (win); var firstIteration = false; - ((FakeDriver)Application.Driver).SetBufferSize (20, height); + ((FakeDriver)Application.Driver!).SetBufferSize (20, height); Application.RunIteration (ref rs, ref firstIteration); var expected = string.Empty; @@ -232,7 +232,7 @@ public class PosCenterTests (ITestOutputHelper output) RunState rs = Application.Begin (win); var firstIteration = false; - ((FakeDriver)Application.Driver).SetBufferSize (width, 7); + ((FakeDriver)Application.Driver!).SetBufferSize (width, 7); Application.RunIteration (ref rs, ref firstIteration); var expected = string.Empty; diff --git a/UnitTests/View/Layout/ViewportTests.cs b/UnitTests/View/Layout/ViewportTests.cs index f5bc9212d..f0d30f118 100644 --- a/UnitTests/View/Layout/ViewportTests.cs +++ b/UnitTests/View/Layout/ViewportTests.cs @@ -472,7 +472,7 @@ public class ViewportTests (ITestOutputHelper output) //[InlineData (5, 5, false)] //public void IsVisibleInSuperView_With_Driver (int x, int y, bool expected) //{ - // ((FakeDriver)Application.Driver).SetBufferSize (10, 10); + // ((FakeDriver)Application.Driver!).SetBufferSize (10, 10); // var view = new View { X = 1, Y = 1, Width = 5, Height = 5 }; // var top = new Toplevel (); diff --git a/UnitTests/View/NavigationTests.cs b/UnitTests/View/NavigationTests.cs index 05dc30a1f..5a7019c19 100644 --- a/UnitTests/View/NavigationTests.cs +++ b/UnitTests/View/NavigationTests.cs @@ -669,7 +669,7 @@ public class NavigationTests (ITestOutputHelper output) // Assert.False (tfQuiting); // Assert.False (topQuiting); -// Application.Driver.SendKeys ('Q', ConsoleKey.Q, false, false, true); +// Application.Driver?.SendKeys ('Q', ConsoleKey.Q, false, false, true); // Assert.False (sbQuiting); // Assert.True (tfQuiting); // Assert.False (topQuiting); @@ -677,7 +677,7 @@ public class NavigationTests (ITestOutputHelper output) //#if BROKE_WITH_2927 // tf.KeyPressed -= Tf_KeyPress; // tfQuiting = false; -// Application.Driver.SendKeys ('q', ConsoleKey.Q, false, false, true); +// Application.Driver?.SendKeys ('q', ConsoleKey.Q, false, false, true); // Application.MainLoop.RunIteration (); // Assert.True (sbQuiting); // Assert.False (tfQuiting); @@ -685,7 +685,7 @@ public class NavigationTests (ITestOutputHelper output) // sb.RemoveItem (0); // sbQuiting = false; -// Application.Driver.SendKeys ('q', ConsoleKey.Q, false, false, true); +// Application.Driver?.SendKeys ('q', ConsoleKey.Q, false, false, true); // Application.MainLoop.RunIteration (); // Assert.False (sbQuiting); // Assert.False (tfQuiting); @@ -733,13 +733,13 @@ public class NavigationTests (ITestOutputHelper output) // Assert.False (sbQuiting); // Assert.False (tfQuiting); -// Application.Driver.SendKeys ('Q', ConsoleKey.Q, false, false, true); +// Application.Driver?.SendKeys ('Q', ConsoleKey.Q, false, false, true); // Assert.False (sbQuiting); // Assert.True (tfQuiting); // tf.KeyDown -= Tf_KeyPressed; // tfQuiting = false; -// Application.Driver.SendKeys ('Q', ConsoleKey.Q, false, false, true); +// Application.Driver?.SendKeys ('Q', ConsoleKey.Q, false, false, true); // Application.MainLoop.RunIteration (); //#if BROKE_WITH_2927 // Assert.True (sbQuiting); @@ -834,7 +834,7 @@ public class NavigationTests (ITestOutputHelper output) Assert.Equal (new Rectangle (0, 0, View.Driver.Cols, View.Driver.Rows), top.Frame); Assert.Equal (new Rectangle (0, 0, 80, 25), top.Frame); - ((FakeDriver)Application.Driver).SetBufferSize (20, 10); + ((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); @@ -984,7 +984,7 @@ public class NavigationTests (ITestOutputHelper output) Assert.NotEqual (new Rectangle (0, 0, View.Driver.Cols, View.Driver.Rows), top.Frame); Assert.Equal (new Rectangle (3, 2, 20, 10), top.Frame); - ((FakeDriver)Application.Driver).SetBufferSize (30, 20); + ((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); diff --git a/UnitTests/View/TextTests.cs b/UnitTests/View/TextTests.cs index 3bd6cfb23..f0cb43091 100644 --- a/UnitTests/View/TextTests.cs +++ b/UnitTests/View/TextTests.cs @@ -148,7 +148,7 @@ Y top.Add (win); RunState rs = Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (15, 15); + ((FakeDriver)Application.Driver!).SetBufferSize (15, 15); Assert.Equal (new (0, 0, 15, 15), win.Frame); Assert.Equal (new (0, 0, 15, 15), win.Margin.Frame); @@ -416,7 +416,7 @@ Y var top = new Toplevel (); top.Add (win); Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (4, 10); + ((FakeDriver)Application.Driver!).SetBufferSize (4, 10); Assert.Equal (5, text.Length); @@ -489,7 +489,7 @@ Y var top = new Toplevel (); top.Add (win); Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (4, 10); + ((FakeDriver)Application.Driver!).SetBufferSize (4, 10); Assert.Equal (5, text.Length); Assert.Equal (new (0, 0, 2, 5), view.Frame); @@ -584,7 +584,7 @@ Y var top = new Toplevel (); top.Add (win); RunState rs = Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (20, 20); + ((FakeDriver)Application.Driver!).SetBufferSize (20, 20); Assert.Equal (new (0, 0, 11, 2), horizontalView.Frame); Assert.Equal (new (0, 3, 2, 11), verticalView.Frame); @@ -672,7 +672,7 @@ Y var top = new Toplevel (); top.Add (win); RunState rs = Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (22, 22); + ((FakeDriver)Application.Driver!).SetBufferSize (22, 22); Assert.Equal (new (text.GetColumns (), 1), horizontalView.TextFormatter.Size); Assert.Equal (new (2, 8), verticalView.TextFormatter.Size); @@ -769,7 +769,7 @@ Y for (var i = 0; i < 4; i++) { - text += Application.Driver.Contents [0, i].Rune; + text += Application.Driver?.Contents [0, i].Rune; } return text; @@ -804,7 +804,7 @@ Y var top = new Toplevel (); top.Add (horizontalView, verticalView); Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (50, 50); + ((FakeDriver)Application.Driver!).SetBufferSize (50, 50); Assert.Equal (new (0, 0, 12, 1), horizontalView.Frame); Assert.Equal (new (12, 1), horizontalView.GetSizeNeededForTextWithoutHotKey ()); @@ -900,7 +900,7 @@ Y var top = new Toplevel (); top.Add (frame); Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (width + 2, 6); + ((FakeDriver)Application.Driver!).SetBufferSize (width + 2, 6); if (autoSize) { @@ -1028,7 +1028,7 @@ Y var top = new Toplevel (); top.Add (frame); Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (9, height + 2); + ((FakeDriver)Application.Driver!).SetBufferSize (9, height + 2); if (autoSize) { @@ -1272,7 +1272,7 @@ Y [SetupFakeDriver] public void Narrow_Wide_Runes () { - ((FakeDriver)Application.Driver).SetBufferSize (32, 32); + ((FakeDriver)Application.Driver!).SetBufferSize (32, 32); var top = new View { Width = 32, Height = 32 }; var text = $"First line{Environment.NewLine}Second line"; diff --git a/UnitTests/View/ViewTests.cs b/UnitTests/View/ViewTests.cs index 4044507f1..d545cdc1d 100644 --- a/UnitTests/View/ViewTests.cs +++ b/UnitTests/View/ViewTests.cs @@ -14,26 +14,26 @@ public class ViewTests (ITestOutputHelper output) view.DrawContent += (s, e) => { - Rectangle savedClip = Application.Driver.Clip; - Application.Driver.Clip = new (1, 1, view.Viewport.Width, view.Viewport.Height); + Rectangle savedClip = Application.Driver!.Clip; + Application.Driver!.Clip = new (1, 1, view.Viewport.Width, view.Viewport.Height); for (var row = 0; row < view.Viewport.Height; row++) { - Application.Driver.Move (1, row + 1); + Application.Driver?.Move (1, row + 1); for (var col = 0; col < view.Viewport.Width; col++) { - Application.Driver.AddStr ($"{col}"); + Application.Driver?.AddStr ($"{col}"); } } - Application.Driver.Clip = savedClip; + Application.Driver!.Clip = savedClip; e.Cancel = true; }; var top = new Toplevel (); top.Add (view); Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (20, 10); + ((FakeDriver)Application.Driver!).SetBufferSize (20, 10); var expected = @" ┌──────────────────┐ @@ -78,26 +78,26 @@ public class ViewTests (ITestOutputHelper output) view.DrawContent += (s, e) => { - Rectangle savedClip = Application.Driver.Clip; - Application.Driver.Clip = new (1, 1, view.Viewport.Width, view.Viewport.Height); + Rectangle savedClip = Application.Driver!.Clip; + Application.Driver!.Clip = new (1, 1, view.Viewport.Width, view.Viewport.Height); for (var row = 0; row < view.Viewport.Height; row++) { - Application.Driver.Move (1, row + 1); + Application.Driver?.Move (1, row + 1); for (var col = 0; col < view.Viewport.Width; col++) { - Application.Driver.AddStr ($"{col}"); + Application.Driver?.AddStr ($"{col}"); } } - Application.Driver.Clip = savedClip; + Application.Driver!.Clip = savedClip; e.Cancel = true; }; var top = new Toplevel (); top.Add (view); Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (20, 10); + ((FakeDriver)Application.Driver!).SetBufferSize (20, 10); var expected = @" ┌──────────────────┐ @@ -1016,7 +1016,7 @@ At 0,0 view.Height = Dim.Auto (); Assert.Equal ("Testing visibility.".Length, view.Frame.Width); Assert.True (view.Visible); - ((FakeDriver)Application.Driver).SetBufferSize (30, 5); + ((FakeDriver)Application.Driver!).SetBufferSize (30, 5); TestHelpers.AssertDriverContentsWithFrameAre ( @" @@ -1107,9 +1107,9 @@ At 0,0 Cell [,] contents = ((FakeDriver)Application.Driver).Contents; var runesCount = 0; - for (var i = 0; i < Application.Driver.Rows; i++) + for (var i = 0; i < Application.Driver!.Rows; i++) { - for (var j = 0; j < Application.Driver.Cols; j++) + for (var j = 0; j < Application.Driver!.Cols; j++) { if (contents [i, j].Rune != (Rune)' ') { diff --git a/UnitTests/Views/AppendAutocompleteTests.cs b/UnitTests/Views/AppendAutocompleteTests.cs index fab9ca750..eaabc43a6 100644 --- a/UnitTests/Views/AppendAutocompleteTests.cs +++ b/UnitTests/Views/AppendAutocompleteTests.cs @@ -11,14 +11,14 @@ public class AppendAutocompleteTests (ITestOutputHelper output) TextField tf = GetTextFieldsInViewSuggesting ("fish"); // f is typed and suggestion is "fish" - Application.Driver.SendKeys ('f', ConsoleKey.F, false, false, false); + Application.Driver?.SendKeys ('f', ConsoleKey.F, false, false, false); tf.Draw (); tf.PositionCursor (); TestHelpers.AssertDriverContentsAre ("fish", output); Assert.Equal ("f", tf.Text); // When cancelling autocomplete - Application.Driver.SendKeys ('e', ConsoleKey.Escape, false, false, false); + Application.Driver?.SendKeys ('e', ConsoleKey.Escape, false, false, false); // Suggestion should disappear tf.Draw (); @@ -29,7 +29,7 @@ public class AppendAutocompleteTests (ITestOutputHelper output) Assert.Same (tf, Application.Top.Focused); // But can tab away - Application.Driver.SendKeys ('\t', ConsoleKey.Tab, false, false, false); + Application.Driver?.SendKeys ('\t', ConsoleKey.Tab, false, false, false); Assert.NotSame (tf, Application.Top.Focused); Application.Top.Dispose (); } @@ -41,14 +41,14 @@ public class AppendAutocompleteTests (ITestOutputHelper output) TextField tf = GetTextFieldsInViewSuggesting ("fish"); // f is typed and suggestion is "fish" - Application.Driver.SendKeys ('f', ConsoleKey.F, false, false, false); + Application.Driver?.SendKeys ('f', ConsoleKey.F, false, false, false); tf.Draw (); tf.PositionCursor (); TestHelpers.AssertDriverContentsAre ("fish", output); Assert.Equal ("f", tf.Text); // When cancelling autocomplete - Application.Driver.SendKeys ('\0', ConsoleKey.Escape, false, false, false); + Application.Driver?.SendKeys ('\0', ConsoleKey.Escape, false, false, false); // Suggestion should disappear tf.Draw (); @@ -56,7 +56,7 @@ public class AppendAutocompleteTests (ITestOutputHelper output) Assert.Equal ("f", tf.Text); // Should reappear when you press next letter - Application.Driver.SendKeys ('i', ConsoleKey.I, false, false, false); + Application.Driver?.SendKeys ('i', ConsoleKey.I, false, false, false); tf.Draw (); tf.PositionCursor (); TestHelpers.AssertDriverContentsAre ("fish", output); @@ -73,14 +73,14 @@ public class AppendAutocompleteTests (ITestOutputHelper output) TextField tf = GetTextFieldsInViewSuggesting ("fish", "friend"); // f is typed and suggestion is "fish" - Application.Driver.SendKeys ('f', ConsoleKey.F, false, false, false); + Application.Driver?.SendKeys ('f', ConsoleKey.F, false, false, false); tf.Draw (); tf.PositionCursor (); TestHelpers.AssertDriverContentsAre ("fish", output); Assert.Equal ("f", tf.Text); // When cycling autocomplete - Application.Driver.SendKeys (' ', cycleKey, false, false, false); + Application.Driver?.SendKeys (' ', cycleKey, false, false, false); tf.Draw (); tf.PositionCursor (); @@ -88,7 +88,7 @@ public class AppendAutocompleteTests (ITestOutputHelper output) Assert.Equal ("f", tf.Text); // Should be able to cycle in circles endlessly - Application.Driver.SendKeys (' ', cycleKey, false, false, false); + Application.Driver?.SendKeys (' ', cycleKey, false, false, false); tf.Draw (); tf.PositionCursor (); TestHelpers.AssertDriverContentsAre ("fish", output); @@ -103,15 +103,15 @@ public class AppendAutocompleteTests (ITestOutputHelper output) TextField tf = GetTextFieldsInViewSuggesting ("fish"); // f is typed and suggestion is "fish" - Application.Driver.SendKeys ('f', ConsoleKey.F, false, false, false); + Application.Driver?.SendKeys ('f', ConsoleKey.F, false, false, false); tf.Draw (); tf.PositionCursor (); TestHelpers.AssertDriverContentsAre ("fish", output); Assert.Equal ("f", tf.Text); // add a space then go back 1 - Application.Driver.SendKeys (' ', ConsoleKey.Spacebar, false, false, false); - Application.Driver.SendKeys ('<', ConsoleKey.LeftArrow, false, false, false); + Application.Driver?.SendKeys (' ', ConsoleKey.Spacebar, false, false, false); + Application.Driver?.SendKeys ('<', ConsoleKey.LeftArrow, false, false, false); tf.Draw (); TestHelpers.AssertDriverContentsAre ("f", output); @@ -126,14 +126,14 @@ public class AppendAutocompleteTests (ITestOutputHelper output) TextField tf = GetTextFieldsInViewSuggesting ("fish"); // f is typed and suggestion is "fish" - Application.Driver.SendKeys ('f', ConsoleKey.F, false, false, false); + Application.Driver?.SendKeys ('f', ConsoleKey.F, false, false, false); tf.Draw (); tf.PositionCursor (); TestHelpers.AssertDriverContentsAre ("fish", output); Assert.Equal ("f", tf.Text); // x is typed and suggestion should disappear - Application.Driver.SendKeys ('x', ConsoleKey.X, false, false, false); + Application.Driver?.SendKeys ('x', ConsoleKey.X, false, false, false); tf.Draw (); TestHelpers.AssertDriverContentsAre ("fx", output); Assert.Equal ("fx", tf.Text); @@ -166,7 +166,7 @@ public class AppendAutocompleteTests (ITestOutputHelper output) Assert.Equal ("my f", tf.Text); // When tab completing the case of the whole suggestion should be applied - Application.Driver.SendKeys ('\t', ConsoleKey.Tab, false, false, false); + Application.Driver?.SendKeys ('\t', ConsoleKey.Tab, false, false, false); tf.Draw (); TestHelpers.AssertDriverContentsAre ("my FISH", output); Assert.Equal ("my FISH", tf.Text); @@ -194,7 +194,7 @@ public class AppendAutocompleteTests (ITestOutputHelper output) TestHelpers.AssertDriverContentsAre ("fish", output); Assert.Equal ("f", tf.Text); - Application.Driver.SendKeys ('\t', ConsoleKey.Tab, false, false, false); + Application.Driver?.SendKeys ('\t', ConsoleKey.Tab, false, false, false); tf.Draw (); TestHelpers.AssertDriverContentsAre ("fish", output); @@ -204,7 +204,7 @@ public class AppendAutocompleteTests (ITestOutputHelper output) Assert.Same (tf, Application.Top.Focused); // Second tab should move focus (nothing to autocomplete) - Application.Driver.SendKeys ('\t', ConsoleKey.Tab, false, false, false); + Application.Driver?.SendKeys ('\t', ConsoleKey.Tab, false, false, false); Assert.NotSame (tf, Application.Top.Focused); Application.Top.Dispose (); } @@ -219,7 +219,7 @@ public class AppendAutocompleteTests (ITestOutputHelper output) TextField tf = GetTextFieldsInViewSuggesting (overspillUsing); // f is typed we should only see 'f' up to size of View (10) - Application.Driver.SendKeys ('f', ConsoleKey.F, false, false, false); + Application.Driver?.SendKeys ('f', ConsoleKey.F, false, false, false); tf.Draw (); tf.PositionCursor (); TestHelpers.AssertDriverContentsAre (expectRender, output); diff --git a/UnitTests/Views/ButtonTests.cs b/UnitTests/Views/ButtonTests.cs index 4ef86d387..d5bc04e07 100644 --- a/UnitTests/Views/ButtonTests.cs +++ b/UnitTests/Views/ButtonTests.cs @@ -224,7 +224,7 @@ public class ButtonTests (ITestOutputHelper output) Assert.Equal ('_', btn.HotKeySpecifier.Value); Assert.True (btn.CanFocus); - Application.Driver.ClearContents (); + Application.Driver?.ClearContents (); btn.Draw (); expected = @$" @@ -563,7 +563,7 @@ public class ButtonTests (ITestOutputHelper output) Assert.False (btn.IsInitialized); Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (30, 5); + ((FakeDriver)Application.Driver!).SetBufferSize (30, 5); Assert.True (btn.IsInitialized); Assert.Equal ("Say Hello 你", btn.Text); @@ -597,7 +597,7 @@ public class ButtonTests (ITestOutputHelper output) Assert.False (btn.IsInitialized); Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (30, 5); + ((FakeDriver)Application.Driver!).SetBufferSize (30, 5); Assert.True (btn.IsInitialized); Assert.Equal ("Say Hello 你", btn.Text); diff --git a/UnitTests/Views/CheckBoxTests.cs b/UnitTests/Views/CheckBoxTests.cs index c7739272d..da41ec55a 100644 --- a/UnitTests/Views/CheckBoxTests.cs +++ b/UnitTests/Views/CheckBoxTests.cs @@ -254,7 +254,7 @@ public class CheckBoxTests (ITestOutputHelper output) top.Add (win); Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (30, 5); + ((FakeDriver)Application.Driver!).SetBufferSize (30, 5); Assert.Equal (Alignment.Center, checkBox.TextAlignment); Assert.Equal (new (1, 1, 25, 1), checkBox.Frame); @@ -314,7 +314,7 @@ public class CheckBoxTests (ITestOutputHelper output) top.Add (win); Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (30, 6); + ((FakeDriver)Application.Driver!).SetBufferSize (30, 6); Assert.Equal (Alignment.Fill, checkBox1.TextAlignment); Assert.Equal (new (1, 1, 25, 1), checkBox1.Frame); @@ -372,7 +372,7 @@ public class CheckBoxTests (ITestOutputHelper output) top.Add (win); Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (30, 5); + ((FakeDriver)Application.Driver!).SetBufferSize (30, 5); Assert.Equal (Alignment.Start, checkBox.TextAlignment); Assert.Equal (new (1, 1, 25, 1), checkBox.Frame); @@ -423,7 +423,7 @@ public class CheckBoxTests (ITestOutputHelper output) top.Add (win); Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (30, 5); + ((FakeDriver)Application.Driver!).SetBufferSize (30, 5); Assert.Equal (Alignment.End, checkBox.TextAlignment); Assert.Equal (new (1, 1, 25, 1), checkBox.Frame); diff --git a/UnitTests/Views/ContextMenuTests.cs b/UnitTests/Views/ContextMenuTests.cs index 6d42add38..efeb709fb 100644 --- a/UnitTests/Views/ContextMenuTests.cs +++ b/UnitTests/Views/ContextMenuTests.cs @@ -117,9 +117,9 @@ public class ContextMenuTests (ITestOutputHelper output) [AutoInitShutdown] public void Draw_A_ContextMenu_Over_A_Borderless_Top () { - ((FakeDriver)Application.Driver).SetBufferSize (20, 15); + ((FakeDriver)Application.Driver!).SetBufferSize (20, 15); - Assert.Equal (new Rectangle (0, 0, 20, 15), Application.Driver.Clip); + Assert.Equal (new Rectangle (0, 0, 20, 15), Application.Driver?.Clip); TestHelpers.AssertDriverContentsWithFrameAre ("", output); var top = new Toplevel { X = 2, Y = 2, Width = 15, Height = 4 }; @@ -167,7 +167,7 @@ public class ContextMenuTests (ITestOutputHelper output) var win = new Window (); top.Add (win); RunState rsTop = Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (20, 15); + ((FakeDriver)Application.Driver!).SetBufferSize (20, 15); Assert.Equal (new Rectangle (0, 0, 20, 15), win.Frame); @@ -252,9 +252,9 @@ public class ContextMenuTests (ITestOutputHelper output) [AutoInitShutdown] public void Draw_A_ContextMenu_Over_A_Top_Dialog () { - ((FakeDriver)Application.Driver).SetBufferSize (20, 15); + ((FakeDriver)Application.Driver!).SetBufferSize (20, 15); - Assert.Equal (new Rectangle (0, 0, 20, 15), Application.Driver.Clip); + Assert.Equal (new Rectangle (0, 0, 20, 15), Application.Driver?.Clip); TestHelpers.AssertDriverContentsWithFrameAre ("", output); // Don't use Dialog here as it has more layout logic. Use Window instead. @@ -542,7 +542,7 @@ public class ContextMenuTests (ITestOutputHelper output) output ); - ((FakeDriver)Application.Driver).SetBufferSize (40, 20); + ((FakeDriver)Application.Driver!).SetBufferSize (40, 20); cm.Position = new Point (41, -2); cm.Show (); Application.Refresh (); @@ -677,7 +677,7 @@ public class ContextMenuTests (ITestOutputHelper output) output ); - ((FakeDriver)Application.Driver).SetBufferSize (18, 8); + ((FakeDriver)Application.Driver!).SetBufferSize (18, 8); cm.Position = new Point (19, 10); cm.Show (); Application.Refresh (); @@ -891,7 +891,7 @@ public class ContextMenuTests (ITestOutputHelper output) [AutoInitShutdown] public void Show_Display_At_Zero_If_The_Toplevel_Height_Is_Less_Than_The_Menu_Height () { - ((FakeDriver)Application.Driver).SetBufferSize (80, 3); + ((FakeDriver)Application.Driver!).SetBufferSize (80, 3); var cm = new ContextMenu { @@ -929,7 +929,7 @@ public class ContextMenuTests (ITestOutputHelper output) [AutoInitShutdown] public void Show_Display_At_Zero_If_The_Toplevel_Width_Is_Less_Than_The_Menu_Width () { - ((FakeDriver)Application.Driver).SetBufferSize (5, 25); + ((FakeDriver)Application.Driver!).SetBufferSize (5, 25); var cm = new ContextMenu { diff --git a/UnitTests/Views/FrameViewTests.cs b/UnitTests/Views/FrameViewTests.cs index 056b45747..88a5c786c 100644 --- a/UnitTests/Views/FrameViewTests.cs +++ b/UnitTests/Views/FrameViewTests.cs @@ -37,7 +37,7 @@ public class FrameViewTests (ITestOutputHelper output) [AutoInitShutdown] public void Draw_Defaults () { - ((FakeDriver)Application.Driver).SetBufferSize (10, 10); + ((FakeDriver)Application.Driver!).SetBufferSize (10, 10); var fv = new FrameView (); Assert.Equal (string.Empty, fv.Title); Assert.Equal (string.Empty, fv.Text); diff --git a/UnitTests/Views/LabelTests.cs b/UnitTests/Views/LabelTests.cs index 24a19c77d..78ed678a8 100644 --- a/UnitTests/Views/LabelTests.cs +++ b/UnitTests/Views/LabelTests.cs @@ -97,7 +97,7 @@ public class LabelTests (ITestOutputHelper output) top.Add (win); Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (30, 5); + ((FakeDriver)Application.Driver!).SetBufferSize (30, 5); var expected = @" ┌────────────────────────────┐ @@ -137,7 +137,7 @@ public class LabelTests (ITestOutputHelper output) top.Add (win); Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (30, 5); + ((FakeDriver)Application.Driver!).SetBufferSize (30, 5); var expected = @" ┌────────────────────────────┐ @@ -179,7 +179,7 @@ public class LabelTests (ITestOutputHelper output) label.Text = "Say Hello 你"; Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (30, 5); + ((FakeDriver)Application.Driver!).SetBufferSize (30, 5); var expected = @" ┌────────────────────────────┐ @@ -414,7 +414,7 @@ e Assert.False (label.IsInitialized); Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (30, 5); + ((FakeDriver)Application.Driver!).SetBufferSize (30, 5); Assert.True (label.IsInitialized); Assert.Equal ("Say Hello 你", label.Text); @@ -446,7 +446,7 @@ e Assert.False (label.IsInitialized); Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (30, 5); + ((FakeDriver)Application.Driver!).SetBufferSize (30, 5); Assert.True (label.IsInitialized); Assert.Equal ("Say Hello 你", label.Text); @@ -473,7 +473,7 @@ e var label = new Label { BorderStyle = LineStyle.Single, Text = "Test" }; label.BeginInit (); label.EndInit (); - label.SetRelativeLayout (Application.Driver.Screen.Size); + label.SetRelativeLayout (Application.Driver!.Screen.Size); Assert.Equal (new (0, 0, 4, 1), label.Viewport); Assert.Equal (new (0, 0, 6, 3), label.Frame); @@ -881,7 +881,7 @@ e Toplevel top = new (); top.Add (win); RunState rs = Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (40, 10); + ((FakeDriver)Application.Driver!).SetBufferSize (40, 10); Assert.Equal (29, label.Text.Length); Assert.Equal (new (0, 0, 40, 10), top.Frame); @@ -931,7 +931,7 @@ e Toplevel top = new (); top.Add (win); RunState rs = Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (40, 10); + ((FakeDriver)Application.Driver!).SetBufferSize (40, 10); Assert.Equal (new (0, 0, 40, 10), top.Frame); Assert.Equal (new (0, 0, 40, 10), win.Frame); @@ -1071,7 +1071,7 @@ e { if (k.KeyCode == KeyCode.Enter) { - ((FakeDriver)Application.Driver).SetBufferSize (22, count + 4); + ((FakeDriver)Application.Driver!).SetBufferSize (22, count + 4); Rectangle pos = TestHelpers.AssertDriverContentsWithFrameAre (expecteds [count], output); Assert.Equal (new (0, 0, 22, count + 4), pos); @@ -1135,7 +1135,7 @@ e [SetupFakeDriver] public void Label_Height_Zero_Stays_Zero () { - ((FakeDriver)Application.Driver).SetBufferSize (10, 4); + ((FakeDriver)Application.Driver!).SetBufferSize (10, 4); var text = "Label"; var label = new Label @@ -1223,7 +1223,7 @@ e { if (k.KeyCode == KeyCode.Enter) { - ((FakeDriver)Application.Driver).SetBufferSize (22, count + 4); + ((FakeDriver)Application.Driver!).SetBufferSize (22, count + 4); Rectangle pos = TestHelpers.AssertDriverContentsWithFrameAre (expecteds [count], output); Assert.Equal (new (0, 0, 22, count + 4), pos); @@ -1299,7 +1299,7 @@ e var top = new Toplevel (); top.Add (win); Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (10, 4); + ((FakeDriver)Application.Driver!).SetBufferSize (10, 4); Assert.Equal (5, text.Length); Assert.Equal (new (0, 0, 5, 1), label.Frame); @@ -1358,7 +1358,7 @@ e var top = new Toplevel (); top.Add (win); Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (10, 4); + ((FakeDriver)Application.Driver!).SetBufferSize (10, 4); Assert.Equal (5, text.Length); Assert.Equal (new (0, 0, 5, 1), label.Frame); diff --git a/UnitTests/Views/ListViewTests.cs b/UnitTests/Views/ListViewTests.cs index 2682fb677..049c5f7e1 100644 --- a/UnitTests/Views/ListViewTests.cs +++ b/UnitTests/Views/ListViewTests.cs @@ -55,7 +55,7 @@ public class ListViewTests (ITestOutputHelper output) var top = new Toplevel (); top.Add (win); Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (12, 12); + ((FakeDriver)Application.Driver!).SetBufferSize (12, 12); Application.Refresh (); Assert.Equal (-1, lv.SelectedItem); @@ -357,7 +357,7 @@ Item 6", for (var i = 0; i < 7; i++) { - item += Application.Driver.Contents [line, i].Rune; + item += Application.Driver?.Contents [line, i].Rune; } return item; diff --git a/UnitTests/Views/MenuBarTests.cs b/UnitTests/Views/MenuBarTests.cs index f7804dd42..35d08beff 100644 --- a/UnitTests/Views/MenuBarTests.cs +++ b/UnitTests/Views/MenuBarTests.cs @@ -366,7 +366,7 @@ public class MenuBarTests (ITestOutputHelper output) var win = new Window (); top.Add (win); RunState rsTop = Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (40, 15); + ((FakeDriver)Application.Driver!).SetBufferSize (40, 15); Assert.Equal (new (0, 0, 40, 15), win.Frame); @@ -556,7 +556,7 @@ public class MenuBarTests (ITestOutputHelper output) Assert.Equal (items [i], menu.Menus [0].Title); } - ((FakeDriver)Application.Driver).SetBufferSize (20, 15); + ((FakeDriver)Application.Driver!).SetBufferSize (20, 15); menu.OpenMenu (); firstIteration = false; Application.RunIteration (ref rsDialog, ref firstIteration); @@ -590,9 +590,9 @@ public class MenuBarTests (ITestOutputHelper output) [AutoInitShutdown] public void Draw_A_Menu_Over_A_Top_Dialog () { - ((FakeDriver)Application.Driver).SetBufferSize (40, 15); + ((FakeDriver)Application.Driver!).SetBufferSize (40, 15); - Assert.Equal (new (0, 0, 40, 15), Application.Driver.Clip); + Assert.Equal (new (0, 0, 40, 15), Application.Driver?.Clip); TestHelpers.AssertDriverContentsWithFrameAre (@"", output); List items = new () @@ -734,7 +734,7 @@ public class MenuBarTests (ITestOutputHelper output) Assert.Equal (items [i], menu.Menus [0].Title); } - ((FakeDriver)Application.Driver).SetBufferSize (20, 15); + ((FakeDriver)Application.Driver!).SetBufferSize (20, 15); menu.OpenMenu (); firstIteration = false; Application.RunIteration (ref rs, ref firstIteration); @@ -805,7 +805,7 @@ public class MenuBarTests (ITestOutputHelper output) menu.CloseAllMenus (); menu.Frame = new (0, 0, menu.Frame.Width, menu.Frame.Height); - ((FakeDriver)Application.Driver).SetBufferSize (7, 5); + ((FakeDriver)Application.Driver!).SetBufferSize (7, 5); menu.OpenMenu (); Application.Refresh (); @@ -821,7 +821,7 @@ public class MenuBarTests (ITestOutputHelper output) menu.CloseAllMenus (); menu.Frame = new (0, 0, menu.Frame.Width, menu.Frame.Height); - ((FakeDriver)Application.Driver).SetBufferSize (7, 3); + ((FakeDriver)Application.Driver!).SetBufferSize (7, 3); menu.OpenMenu (); Application.Refresh (); @@ -878,7 +878,7 @@ wo menu.CloseAllMenus (); menu.Frame = new (0, 0, menu.Frame.Width, menu.Frame.Height); - ((FakeDriver)Application.Driver).SetBufferSize (3, 2); + ((FakeDriver)Application.Driver!).SetBufferSize (3, 2); menu.OpenMenu (); Application.Refresh (); @@ -891,7 +891,7 @@ wo menu.CloseAllMenus (); menu.Frame = new (0, 0, menu.Frame.Width, menu.Frame.Height); - ((FakeDriver)Application.Driver).SetBufferSize (3, 1); + ((FakeDriver)Application.Driver!).SetBufferSize (3, 1); menu.OpenMenu (); Application.Refresh (); @@ -1519,7 +1519,7 @@ wo Toplevel top = new (); top.Add (win); Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (40, 8); + ((FakeDriver)Application.Driver!).SetBufferSize (40, 8); TestHelpers.AssertDriverContentsWithFrameAre ( @" @@ -1630,7 +1630,7 @@ wo Application.Iteration += (s, a) => { - ((FakeDriver)Application.Driver).SetBufferSize (40, 8); + ((FakeDriver)Application.Driver!).SetBufferSize (40, 8); TestHelpers.AssertDriverContentsWithFrameAre ( @" @@ -1741,7 +1741,7 @@ wo ] }; win.Add (menu); - ((FakeDriver)Application.Driver).SetBufferSize (40, 8); + ((FakeDriver)Application.Driver!).SetBufferSize (40, 8); Application.Begin (win); TestHelpers.AssertDriverContentsWithFrameAre ( @@ -1827,7 +1827,7 @@ wo [AutoInitShutdown] public void MenuBar_In_Window_Without_Other_Views_Without_Top_Init_With_Run_T () { - ((FakeDriver)Application.Driver).SetBufferSize (40, 8); + ((FakeDriver)Application.Driver!).SetBufferSize (40, 8); Application.Iteration += (s, a) => { @@ -2758,7 +2758,7 @@ Edit output ); - ((FakeDriver)Application.Driver).SetBufferSize (20, 15); + ((FakeDriver)Application.Driver!).SetBufferSize (20, 15); firstIteration = false; Application.RunIteration (ref rs, ref firstIteration); diff --git a/UnitTests/Views/OverlappedTests.cs b/UnitTests/Views/OverlappedTests.cs index 9c7202d68..5ca0fc566 100644 --- a/UnitTests/Views/OverlappedTests.cs +++ b/UnitTests/Views/OverlappedTests.cs @@ -881,7 +881,7 @@ public class OverlappedTests var overlapped = new Overlapped (); var win1 = new Window { Width = 5, Height = 5, Visible = false }; var win2 = new Window { X = 1, Y = 1, Width = 5, Height = 5 }; - ((FakeDriver)Application.Driver).SetBufferSize (10, 10); + ((FakeDriver)Application.Driver!).SetBufferSize (10, 10); RunState rsOverlapped = Application.Begin (overlapped); // Need to fool MainLoop into thinking it's running diff --git a/UnitTests/Views/RadioGroupTests.cs b/UnitTests/Views/RadioGroupTests.cs index 75aab7b18..4c828f2c1 100644 --- a/UnitTests/Views/RadioGroupTests.cs +++ b/UnitTests/Views/RadioGroupTests.cs @@ -219,7 +219,7 @@ public class RadioGroupTests (ITestOutputHelper output) top.Add (win); Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (30, 5); + ((FakeDriver)Application.Driver!).SetBufferSize (30, 5); Assert.Equal (Orientation.Vertical, rg.Orientation); Assert.Equal (2, rg.RadioLabels.Length); diff --git a/UnitTests/Views/ScrollBarViewTests.cs b/UnitTests/Views/ScrollBarViewTests.cs index f3f9f7c24..15c463d78 100644 --- a/UnitTests/Views/ScrollBarViewTests.cs +++ b/UnitTests/Views/ScrollBarViewTests.cs @@ -173,7 +173,7 @@ public class ScrollBarViewTests super.Add (vert); Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (width, height); + ((FakeDriver)Application.Driver!).SetBufferSize (width, height); var expected = @" ┌─┐ @@ -703,7 +703,7 @@ This is a test var sbv = new ScrollBarView { Id = "sbv", Size = width * 2, ShowScrollIndicator = true }; super.Add (sbv); Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (width, height); + ((FakeDriver)Application.Driver!).SetBufferSize (width, height); var expected = @" ┌──────────────────────────────────────┐ @@ -829,7 +829,7 @@ This is a test top.Add (win); Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (45, 20); + ((FakeDriver)Application.Driver!).SetBufferSize (45, 20); Assert.True (scrollBar.AutoHideScrollBars); Assert.False (scrollBar.ShowScrollIndicator); @@ -867,7 +867,7 @@ This is a test Assert.Equal (new Rectangle (0, 0, 45, 20), pos); textView.WordWrap = true; - ((FakeDriver)Application.Driver).SetBufferSize (26, 20); + ((FakeDriver)Application.Driver!).SetBufferSize (26, 20); Application.Refresh (); Assert.True (textView.WordWrap); @@ -904,7 +904,7 @@ This is a test pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, _output); Assert.Equal (new Rectangle (0, 0, 26, 20), pos); - ((FakeDriver)Application.Driver).SetBufferSize (10, 10); + ((FakeDriver)Application.Driver!).SetBufferSize (10, 10); Application.Refresh (); Assert.True (textView.WordWrap); @@ -1229,7 +1229,7 @@ This is a test ", super.Add (sbv); Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (width, height); + ((FakeDriver)Application.Driver!).SetBufferSize (width, height); var expected = @" ┌─┐ diff --git a/UnitTests/Views/ScrollViewTests.cs b/UnitTests/Views/ScrollViewTests.cs index ceab4e63a..c4d14c235 100644 --- a/UnitTests/Views/ScrollViewTests.cs +++ b/UnitTests/Views/ScrollViewTests.cs @@ -362,7 +362,7 @@ public class ScrollViewTests (ITestOutputHelper output) [SetupFakeDriver] public void ContentBottomRightCorner_Draw () { - ((FakeDriver)Application.Driver).SetBufferSize (30, 30); + ((FakeDriver)Application.Driver!).SetBufferSize (30, 30); var top = new View { Width = 30, Height = 30, ColorScheme = new() { Normal = Attribute.Default } }; diff --git a/UnitTests/Views/TableViewTests.cs b/UnitTests/Views/TableViewTests.cs index 515e3f8bd..6740120f6 100644 --- a/UnitTests/Views/TableViewTests.cs +++ b/UnitTests/Views/TableViewTests.cs @@ -2196,7 +2196,7 @@ public class TableViewTests (ITestOutputHelper output) [SetupFakeDriver] public void TestEnumerableDataSource_BasicTypes () { - ((FakeDriver)Application.Driver).SetBufferSize(100,100); + ((FakeDriver)Application.Driver!).SetBufferSize(100,100); var tv = new TableView (); tv.ColorScheme = Colors.ColorSchemes ["TopLevel"]; tv.Viewport = new (0, 0, 50, 6); diff --git a/UnitTests/Views/TextFieldTests.cs b/UnitTests/Views/TextFieldTests.cs index e50617933..ed3a35775 100644 --- a/UnitTests/Views/TextFieldTests.cs +++ b/UnitTests/Views/TextFieldTests.cs @@ -67,7 +67,7 @@ public class TextFieldTests (ITestOutputHelper output) for (var i = 0; i < 16; i++) { - item += Application.Driver.Contents [0, i].Rune; + item += Application.Driver?.Contents [0, i].Rune; } return item; @@ -164,7 +164,7 @@ public class TextFieldTests (ITestOutputHelper output) // Caption has no effect when focused tf.Caption = caption; - Application.Driver.SendKeys ('\t', ConsoleKey.Tab, false, false, false); + Application.Driver?.SendKeys ('\t', ConsoleKey.Tab, false, false, false); Assert.False (tf.HasFocus); tf.Draw (); @@ -184,7 +184,7 @@ public class TextFieldTests (ITestOutputHelper output) TextField tf = GetTextFieldsInView (); tf.Caption = caption; - Application.Driver.SendKeys ('\t', ConsoleKey.Tab, false, false, false); + Application.Driver?.SendKeys ('\t', ConsoleKey.Tab, false, false, false); Assert.False (tf.HasFocus); tf.Draw (); @@ -205,7 +205,7 @@ public class TextFieldTests (ITestOutputHelper output) TestHelpers.AssertDriverContentsAre ("", output); tf.Caption = "Enter txt"; - Application.Driver.SendKeys ('\t', ConsoleKey.Tab, false, false, false); + Application.Driver?.SendKeys ('\t', ConsoleKey.Tab, false, false, false); // Caption should appear when not focused and no text Assert.False (tf.HasFocus); @@ -234,7 +234,7 @@ public class TextFieldTests (ITestOutputHelper output) tf.Draw (); TestHelpers.AssertDriverContentsAre ("", output); - Application.Driver.SendKeys ('\t', ConsoleKey.Tab, false, false, false); + Application.Driver?.SendKeys ('\t', ConsoleKey.Tab, false, false, false); Assert.False (tf.HasFocus); tf.Draw (); @@ -347,7 +347,7 @@ public class TextFieldTests (ITestOutputHelper output) Assert.Equal ( "TextField with some more test text. Unicode shouldn't 𝔹Aℝ𝔽!", - Application.Driver.Clipboard.GetClipboardData () + Application.Driver?.Clipboard.GetClipboardData () ); Assert.Equal (string.Empty, _textField.Text); _textField.Paste (); @@ -374,7 +374,7 @@ public class TextFieldTests (ITestOutputHelper output) Assert.Equal (32, _textField.CursorPosition); _textField.SelectAll (); _textField.Cut (); - Assert.Equal ("TAB to jump between text fields.", Application.Driver.Clipboard.GetClipboardData ()); + Assert.Equal ("TAB to jump between text fields.", Application.Driver?.Clipboard.GetClipboardData ()); Assert.Equal (string.Empty, _textField.Text); Assert.Equal (0, _textField.CursorPosition); _textField.Paste (); diff --git a/UnitTests/Views/TextViewTests.cs b/UnitTests/Views/TextViewTests.cs index f260dd162..f9ff520dd 100644 --- a/UnitTests/Views/TextViewTests.cs +++ b/UnitTests/Views/TextViewTests.cs @@ -609,7 +609,7 @@ public class TextViewTests Assert.Equal ( "TextView with some more test text. Unicode shouldn't 𝔹Aℝ𝔽!", - Application.Driver.Clipboard.GetClipboardData () + Application.Driver?.Clipboard.GetClipboardData () ); Assert.Equal (string.Empty, _textView.Text); _textView.Paste (); @@ -1018,7 +1018,7 @@ This is the second line. tv.NewMouseEvent (new MouseEvent { Flags = MouseFlags.WheeledRight }); Assert.Equal (Math.Min (i + 1, 11), tv.LeftColumn); Application.PositionCursor (top); - Application.Driver.GetCursorVisibility (out CursorVisibility cursorVisibility); + Application.Driver!.GetCursorVisibility (out CursorVisibility cursorVisibility); Assert.Equal (CursorVisibility.Invisible, cursorVisibility); } @@ -1028,7 +1028,7 @@ This is the second line. Assert.Equal (i - 1, tv.LeftColumn); Application.PositionCursor (top); - Application.Driver.GetCursorVisibility (out CursorVisibility cursorVisibility); + Application.Driver!.GetCursorVisibility (out CursorVisibility cursorVisibility); if (i - 1 == 0) { @@ -1070,7 +1070,7 @@ This is the second line. tv.NewMouseEvent (new MouseEvent { Flags = MouseFlags.WheeledDown }); Application.PositionCursor (top); Assert.Equal (i + 1, tv.TopRow); - Application.Driver.GetCursorVisibility (out CursorVisibility cursorVisibility); + Application.Driver!.GetCursorVisibility (out CursorVisibility cursorVisibility); Assert.Equal (CursorVisibility.Invisible, cursorVisibility); } @@ -1081,7 +1081,7 @@ This is the second line. Assert.Equal (i - 1, tv.TopRow); Application.PositionCursor (top); - Application.Driver.GetCursorVisibility (out CursorVisibility cursorVisibility); + Application.Driver!.GetCursorVisibility (out CursorVisibility cursorVisibility); if (i - 1 == 0) { @@ -6697,7 +6697,7 @@ TAB to jump between text field", var top = new Toplevel (); top.Add (win); Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (15, 15); + ((FakeDriver)Application.Driver!).SetBufferSize (15, 15); Application.Refresh (); //this passes @@ -6774,7 +6774,7 @@ TAB to jump between text field", var top = new Toplevel (); top.Add (win); Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (15, 15); + ((FakeDriver)Application.Driver!).SetBufferSize (15, 15); Application.Refresh (); //this passes @@ -6899,8 +6899,8 @@ This is the second line. _output ); - ((FakeDriver)Application.Driver).SetBufferSize (6, 25); - tv.SetRelativeLayout (Application.Driver.Screen.Size); + ((FakeDriver)Application.Driver!).SetBufferSize (6, 25); + tv.SetRelativeLayout (Application.Driver!.Screen.Size); tv.Draw (); Assert.Equal (new Point (4, 2), tv.CursorPosition); Assert.Equal (new Point (12, 0), cp); diff --git a/UnitTests/Views/ToplevelTests.cs b/UnitTests/Views/ToplevelTests.cs index abbfdeb59..c19c72bb1 100644 --- a/UnitTests/Views/ToplevelTests.cs +++ b/UnitTests/Views/ToplevelTests.cs @@ -44,8 +44,8 @@ public partial class ToplevelTests (ITestOutputHelper output) Assert.Equal ("Top1", Application.Top.Text); Assert.Equal (0, Application.Top.Frame.X); Assert.Equal (0, Application.Top.Frame.Y); - Assert.Equal (Application.Driver.Cols, Application.Top.Frame.Width); - Assert.Equal (Application.Driver.Rows, Application.Top.Frame.Height); + Assert.Equal (Application.Driver!.Cols, Application.Top.Frame.Width); + Assert.Equal (Application.Driver!.Rows, Application.Top.Frame.Height); Application.OnKeyPressed (new (Key.CtrlMask | Key.R)); @@ -54,8 +54,8 @@ public partial class ToplevelTests (ITestOutputHelper output) Assert.Equal ("Top2", Application.Top.Text); Assert.Equal (0, Application.Top.Frame.X); Assert.Equal (0, Application.Top.Frame.Y); - Assert.Equal (Application.Driver.Cols, Application.Top.Frame.Width); - Assert.Equal (Application.Driver.Rows, Application.Top.Frame.Height); + Assert.Equal (Application.Driver!.Cols, Application.Top.Frame.Width); + Assert.Equal (Application.Driver!.Rows, Application.Top.Frame.Height); Application.OnKeyPressed (new (Key.CtrlMask | Key.C)); @@ -64,8 +64,8 @@ public partial class ToplevelTests (ITestOutputHelper output) Assert.Equal ("Top1", Application.Top.Text); Assert.Equal (0, Application.Top.Frame.X); Assert.Equal (0, Application.Top.Frame.Y); - Assert.Equal (Application.Driver.Cols, Application.Top.Frame.Width); - Assert.Equal (Application.Driver.Rows, Application.Top.Frame.Height); + Assert.Equal (Application.Driver!.Cols, Application.Top.Frame.Width); + Assert.Equal (Application.Driver!.Rows, Application.Top.Frame.Height); Application.OnKeyPressed (new (Key.CtrlMask | Key.R)); @@ -74,8 +74,8 @@ public partial class ToplevelTests (ITestOutputHelper output) Assert.Equal ("Top2", Application.Top.Text); Assert.Equal (0, Application.Top.Frame.X); Assert.Equal (0, Application.Top.Frame.Y); - Assert.Equal (Application.Driver.Cols, Application.Top.Frame.Width); - Assert.Equal (Application.Driver.Rows, Application.Top.Frame.Height); + Assert.Equal (Application.Driver!.Cols, Application.Top.Frame.Width); + Assert.Equal (Application.Driver!.Rows, Application.Top.Frame.Height); Application.OnKeyPressed (new (Key.CtrlMask | Key.C)); @@ -84,8 +84,8 @@ public partial class ToplevelTests (ITestOutputHelper output) Assert.Equal ("Top1", Application.Top.Text); Assert.Equal (0, Application.Top.Frame.X); Assert.Equal (0, Application.Top.Frame.Y); - Assert.Equal (Application.Driver.Cols, Application.Top.Frame.Width); - Assert.Equal (Application.Driver.Rows, Application.Top.Frame.Height); + Assert.Equal (Application.Driver!.Cols, Application.Top.Frame.Width); + Assert.Equal (Application.Driver!.Rows, Application.Top.Frame.Height); Application.OnKeyPressed (new (Key.CtrlMask | Key.Q)); @@ -675,7 +675,7 @@ public partial class ToplevelTests (ITestOutputHelper output) if (iterations == 0) { - ((FakeDriver)Application.Driver).SetBufferSize (15, 7); + ((FakeDriver)Application.Driver!).SetBufferSize (15, 7); // Don't use MessageBox here; it's too complicated for this unit test; just use Window testWindow = new () @@ -794,7 +794,7 @@ public partial class ToplevelTests (ITestOutputHelper output) if (iterations == 0) { - ((FakeDriver)Application.Driver).SetBufferSize (30, 10); + ((FakeDriver)Application.Driver!).SetBufferSize (30, 10); } else if (iterations == 1) { @@ -896,10 +896,10 @@ public partial class ToplevelTests (ITestOutputHelper output) top.BeginInit (); top.EndInit (); - Exception exception = Record.Exception (() => ((FakeDriver)Application.Driver).SetBufferSize (0, 10)); + Exception exception = Record.Exception (() => ((FakeDriver)Application.Driver!).SetBufferSize (0, 10)); Assert.Null (exception); - exception = Record.Exception (() => ((FakeDriver)Application.Driver).SetBufferSize (10, 0)); + exception = Record.Exception (() => ((FakeDriver)Application.Driver!).SetBufferSize (10, 0)); Assert.Null (exception); } @@ -1085,13 +1085,13 @@ public partial class ToplevelTests (ITestOutputHelper output) Assert.True (tf.HasFocus); Application.PositionCursor (top); - Application.Driver.GetCursorVisibility (out CursorVisibility cursor); + Application.Driver!.GetCursorVisibility (out CursorVisibility cursor); Assert.Equal (CursorVisibility.Default, cursor); view.Enabled = false; Assert.False (tf.HasFocus); Application.PositionCursor (top); - Application.Driver.GetCursorVisibility (out cursor); + Application.Driver!.GetCursorVisibility (out cursor); Assert.Equal (CursorVisibility.Invisible, cursor); top.Dispose (); } @@ -1209,7 +1209,7 @@ public partial class ToplevelTests (ITestOutputHelper output) Toplevel top = new (); var window = new Window { Width = 20, Height = 3, Arrangement = ViewArrangement.Movable }; RunState rsTop = Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (40, 10); + ((FakeDriver)Application.Driver!).SetBufferSize (40, 10); RunState rsWindow = Application.Begin (window); Application.Refresh (); Assert.Equal (new (0, 0, 40, 10), top.Frame); @@ -1232,7 +1232,7 @@ public partial class ToplevelTests (ITestOutputHelper output) Assert.Equal (new (0, 0, 20, 3), window.Frame); // Changes Top size to same size as Dialog more menu and scroll bar - ((FakeDriver)Application.Driver).SetBufferSize (20, 3); + ((FakeDriver)Application.Driver!).SetBufferSize (20, 3); Application.OnMouseEvent ( new () @@ -1245,7 +1245,7 @@ public partial class ToplevelTests (ITestOutputHelper output) Assert.Equal (new (0, 0, 20, 3), window.Frame); // Changes Top size smaller than Dialog size - ((FakeDriver)Application.Driver).SetBufferSize (19, 2); + ((FakeDriver)Application.Driver!).SetBufferSize (19, 2); Application.OnMouseEvent ( new () @@ -1338,7 +1338,7 @@ public partial class ToplevelTests (ITestOutputHelper output) { Toplevel top = new (); RunState rsTop = Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (20, 20); + ((FakeDriver)Application.Driver!).SetBufferSize (20, 20); var testWindow = new Window { X = 2, Y = 1, Width = 15, Height = 10 }; Assert.Equal (new (2, 1, 15, 10), testWindow.Frame); @@ -1360,7 +1360,7 @@ public partial class ToplevelTests (ITestOutputHelper output) var win = new Window (); top.Add (win); RunState rsTop = Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (20, 20); + ((FakeDriver)Application.Driver!).SetBufferSize (20, 20); Assert.Equal (new (0, 0, 20, 20), win.Frame); @@ -1389,8 +1389,8 @@ public partial class ToplevelTests (ITestOutputHelper output) { Assert.Equal (new (1, 3, 18, 16), viewAddedToTop.Frame); - Rectangle savedClip = Application.Driver.Clip; - Application.Driver.Clip = top.Frame; + Rectangle savedClip = Application.Driver!.Clip; + Application.Driver!.Clip = top.Frame; viewAddedToTop.Draw (); top.Move (2, 15); View.Driver.AddStr ("One"); @@ -1398,7 +1398,7 @@ public partial class ToplevelTests (ITestOutputHelper output) View.Driver.AddStr ("Two"); top.Move (2, 17); View.Driver.AddStr ("Three"); - Application.Driver.Clip = savedClip; + Application.Driver!.Clip = savedClip; Application.Current.DrawContentComplete -= OnDrawContentComplete; } diff --git a/UnitTests/Views/TreeTableSourceTests.cs b/UnitTests/Views/TreeTableSourceTests.cs index 0b54be84d..39e18327b 100644 --- a/UnitTests/Views/TreeTableSourceTests.cs +++ b/UnitTests/Views/TreeTableSourceTests.cs @@ -29,7 +29,7 @@ public class TreeTableSourceTests : IDisposable [SetupFakeDriver] public void TestTreeTableSource_BasicExpanding_WithKeyboard () { - ((FakeDriver)Application.Driver).SetBufferSize (100, 100); + ((FakeDriver)Application.Driver!).SetBufferSize (100, 100); TableView tv = GetTreeTable (out _); tv.Style.GetOrCreateColumnStyle (1).MinAcceptableWidth = 1; @@ -88,7 +88,7 @@ public class TreeTableSourceTests : IDisposable [SetupFakeDriver] public void TestTreeTableSource_BasicExpanding_WithMouse () { - ((FakeDriver)Application.Driver).SetBufferSize (100, 100); + ((FakeDriver)Application.Driver!).SetBufferSize (100, 100); TableView tv = GetTreeTable (out _); diff --git a/UnitTests/Views/TreeViewTests.cs b/UnitTests/Views/TreeViewTests.cs index 11d85acdb..a770d1d9d 100644 --- a/UnitTests/Views/TreeViewTests.cs +++ b/UnitTests/Views/TreeViewTests.cs @@ -114,7 +114,7 @@ public class TreeViewTests tv.SelectAll (); tv.CursorVisibility = CursorVisibility.Default; Application.PositionCursor (top); - Application.Driver.GetCursorVisibility (out CursorVisibility visibility); + Application.Driver!.GetCursorVisibility (out CursorVisibility visibility); Assert.Equal (CursorVisibility.Default, tv.CursorVisibility); Assert.Equal (CursorVisibility.Default, visibility); top.Dispose (); diff --git a/UnitTests/Views/WindowTests.cs b/UnitTests/Views/WindowTests.cs index 24ea77876..6df5361e3 100644 --- a/UnitTests/Views/WindowTests.cs +++ b/UnitTests/Views/WindowTests.cs @@ -53,7 +53,7 @@ public class WindowTests Toplevel top = new (); top.Add (win); Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (20, 10); + ((FakeDriver)Application.Driver!).SetBufferSize (20, 10); TestHelpers.AssertDriverContentsWithFrameAre ( @" @@ -70,7 +70,7 @@ public class WindowTests _output ); - ((FakeDriver)Application.Driver).SetBufferSize (40, 20); + ((FakeDriver)Application.Driver!).SetBufferSize (40, 20); TestHelpers.AssertDriverContentsWithFrameAre ( @" @@ -97,7 +97,7 @@ public class WindowTests _output ); - ((FakeDriver)Application.Driver).SetBufferSize (20, 10); + ((FakeDriver)Application.Driver!).SetBufferSize (20, 10); TestHelpers.AssertDriverContentsWithFrameAre ( @" From 2e0a9a7c688f822adec71c98a6205a17d5c65f6f Mon Sep 17 00:00:00 2001 From: Tig Date: Mon, 22 Jul 2024 16:59:10 -0600 Subject: [PATCH 03/33] Fixed nullable warnings --- Terminal.Gui/Application/Application.Mouse.cs | 2 +- Terminal.Gui/Application/Application.cs | 4 ++-- Terminal.Gui/View/Layout/ViewLayout.cs | 6 +++--- Terminal.sln.DotSettings | 3 ++- UICatalog/UICatalog.cs | 10 +++++----- UnitTests/ConsoleDrivers/ClipRegionTests.cs | 4 ++-- 6 files changed, 15 insertions(+), 14 deletions(-) diff --git a/Terminal.Gui/Application/Application.Mouse.cs b/Terminal.Gui/Application/Application.Mouse.cs index 4d3fb6129..7cad055d5 100644 --- a/Terminal.Gui/Application/Application.Mouse.cs +++ b/Terminal.Gui/Application/Application.Mouse.cs @@ -195,7 +195,7 @@ public static partial class Application // Mouse handling { // This occurs when there are multiple overlapped "tops" // E.g. "Mdi" - in the Background Worker Scenario - View? top = FindDeepestTop (Top, mouseEvent.Position); + View? top = FindDeepestTop (Top!, mouseEvent.Position); view = View.FindDeepestView (top, mouseEvent.Position); if (view is { } && view != OverlappedTop && top != Current && top is { }) diff --git a/Terminal.Gui/Application/Application.cs b/Terminal.Gui/Application/Application.cs index 1561413e9..c8aa2536a 100644 --- a/Terminal.Gui/Application/Application.cs +++ b/Terminal.Gui/Application/Application.cs @@ -19,7 +19,7 @@ namespace Terminal.Gui; public static partial class Application { /// Gets all cultures supported by the application without the invariant language. - public static List SupportedCultures { get; private set; } + public static List? SupportedCultures { get; private set; } internal static List GetSupportedCultures () { @@ -257,7 +257,7 @@ public static partial class Application foreach (Toplevel? t in savedToplevels) { - if (!t.Modal && t != Current && t != top && t != savedToplevels [index]) + if (!t!.Modal && t != Current && t != top && t != savedToplevels [index]) { lock (_topLevels) { diff --git a/Terminal.Gui/View/Layout/ViewLayout.cs b/Terminal.Gui/View/Layout/ViewLayout.cs index 637d1f0ca..4e2531745 100644 --- a/Terminal.Gui/View/Layout/ViewLayout.cs +++ b/Terminal.Gui/View/Layout/ViewLayout.cs @@ -344,7 +344,7 @@ public partial class View if (found is { }) { start = found; - viewportOffset = found.Parent.Frame.Location; + viewportOffset = found.Parent?.Frame.Location ?? Point.Empty; } int startOffsetX = currentLocation.X - (start.Frame.X + viewportOffset.X); @@ -796,7 +796,7 @@ public partial class View //} if (dv.Target != this) { - nEdges.Add ((dv.Target, from)); + nEdges.Add ((dv.Target!, from)); } return; @@ -819,7 +819,7 @@ public partial class View //} if (pv.Target != this) { - nEdges.Add ((pv.Target, from)); + nEdges.Add ((pv.Target!, from)); } return; diff --git a/Terminal.sln.DotSettings b/Terminal.sln.DotSettings index e83ed7026..bf87e5ea9 100644 --- a/Terminal.sln.DotSettings +++ b/Terminal.sln.DotSettings @@ -1,4 +1,4 @@ - + BackingField Inherit True @@ -390,6 +390,7 @@ <Policy><Descriptor Staticness="Instance" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Instance fields (not private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /></Policy> <Policy><Descriptor Staticness="Static" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Static fields (not private)"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /></Policy> <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Types and namespaces"><ElementKinds><Kind Name="NAMESPACE" /><Kind Name="CLASS" /><Kind Name="STRUCT" /><Kind Name="ENUM" /><Kind Name="DELEGATE" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="" Style="AaBb_AaBb" /></Policy></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Local constants"><ElementKinds><Kind Name="LOCAL_CONSTANT" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /></Policy> <Policy><Descriptor Staticness="Static" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Static readonly fields (not private)"><ElementKinds><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /></Policy> True ..\Terminal.sln.ToDo.DotSettings diff --git a/UICatalog/UICatalog.cs b/UICatalog/UICatalog.cs index dd4184cb4..f9ac4a503 100644 --- a/UICatalog/UICatalog.cs +++ b/UICatalog/UICatalog.cs @@ -666,7 +666,7 @@ internal class UICatalogApp MiIsMouseDisabled!.Checked = Application.IsMouseDisabled; - Application.Top.SetNeedsDisplay (); + Application.Top!.SetNeedsDisplay (); } public MenuItem []? CreateThemeMenuItems () @@ -835,7 +835,7 @@ internal class UICatalogApp } Diagnostics = _diagnosticFlags; - Application.Top.SetNeedsDisplay (); + Application.Top!.SetNeedsDisplay (); }; menuItems.Add (item); } @@ -1061,7 +1061,7 @@ internal class UICatalogApp ShowStatusBar = StatusBar.Visible; int height = StatusBar.Visible ? 1 : 0; - CategoryList.Height = Dim.Fill (height); + CategoryList!.Height = Dim.Fill (height); ScenarioList.Height = Dim.Fill (height); // ContentPane.Height = Dim.Fill (height); @@ -1071,7 +1071,7 @@ internal class UICatalogApp } Loaded -= LoadedHandler; - CategoryList.EnsureSelectedItemVisible (); + CategoryList!.EnsureSelectedItemVisible (); ScenarioList.EnsureSelectedCellIsVisible (); } @@ -1082,7 +1082,7 @@ internal class UICatalogApp if (_selectedScenario is null) { // Save selected item state - _cachedCategoryIndex = CategoryList.SelectedItem; + _cachedCategoryIndex = CategoryList!.SelectedItem; _cachedScenarioIndex = ScenarioList.SelectedRow; // Create new instance of scenario (even though Scenarios contains instances) diff --git a/UnitTests/ConsoleDrivers/ClipRegionTests.cs b/UnitTests/ConsoleDrivers/ClipRegionTests.cs index 8a90f2e4d..73eff741d 100644 --- a/UnitTests/ConsoleDrivers/ClipRegionTests.cs +++ b/UnitTests/ConsoleDrivers/ClipRegionTests.cs @@ -7,12 +7,12 @@ namespace Terminal.Gui.DriverTests; public class ClipRegionTests { - private readonly ITestOutputHelper output; + private readonly ITestOutputHelper _output; public ClipRegionTests (ITestOutputHelper output) { ConsoleDriver.RunningUnitTests = true; - this.output = output; + this._output = output; } [Theory] From 0b8e4342d4aff89afbe09f872e510e19892ea189 Mon Sep 17 00:00:00 2001 From: Tig Date: Mon, 22 Jul 2024 17:05:06 -0600 Subject: [PATCH 04/33] Applicaation Toplevel handling moved to separate file --- Terminal.Gui/Application/Application.Run.cs | 5 + .../Application/Application.Toplevel.cs | 214 ++++++++++++++++++ Terminal.Gui/Application/Application.cs | 7 +- 3 files changed, 224 insertions(+), 2 deletions(-) create mode 100644 Terminal.Gui/Application/Application.Toplevel.cs diff --git a/Terminal.Gui/Application/Application.Run.cs b/Terminal.Gui/Application/Application.Run.cs index 34189d9fc..06c191230 100644 --- a/Terminal.Gui/Application/Application.Run.cs +++ b/Terminal.Gui/Application/Application.Run.cs @@ -5,6 +5,8 @@ namespace Terminal.Gui; public static partial class Application // Run (Begin, Run, End, Stop) { + // When `End ()` is called, it is possible `RunState.Toplevel` is a different object than `Top`. + // This variable is set in `End` in this case so that `Begin` correctly sets `Top`. private static Toplevel _cachedRunStateToplevel; /// @@ -485,6 +487,9 @@ public static partial class Application // Run (Begin, Run, End, Stop) ); } + // TODO: Determine if this is really needed. The only code that calls WakeUp I can find + // is ProgressBarStyles, and it's not clear it needs to. + /// Wakes up the running application that might be waiting on input. public static void Wakeup () { MainLoop?.Wakeup (); } diff --git a/Terminal.Gui/Application/Application.Toplevel.cs b/Terminal.Gui/Application/Application.Toplevel.cs new file mode 100644 index 000000000..6d25f7d02 --- /dev/null +++ b/Terminal.Gui/Application/Application.Toplevel.cs @@ -0,0 +1,214 @@ +namespace Terminal.Gui; + +public static partial class Application // Toplevel handling +{ + /// Holds the stack of TopLevel views. + + // BUGBUG: Technically, this is not the full lst of TopLevels. There be dragons here, e.g. see how Toplevel.Id is used. What + // about TopLevels that are just a SubView of another View? + internal static readonly Stack _topLevels = new (); + + /// The object used for the application on startup () + /// The top. + public static Toplevel Top { get; private set; } + + /// + /// The current object. This is updated in enters and leaves to + /// point to the current + /// . + /// + /// + /// Only relevant in scenarios where is . + /// + /// The current. + public static Toplevel Current { get; private set; } + + private static void EnsureModalOrVisibleAlwaysOnTop (Toplevel topLevel) + { + if (!topLevel.Running + || (topLevel == Current && topLevel.Visible) + || OverlappedTop == null + || _topLevels.Peek ().Modal) + { + return; + } + + foreach (Toplevel top in _topLevels.Reverse ()) + { + if (top.Modal && top != Current) + { + MoveCurrent (top); + + return; + } + } + + if (!topLevel.Visible && topLevel == Current) + { + OverlappedMoveNext (); + } + } + + private static Toplevel FindDeepestTop (Toplevel start, in Point location) + { + if (!start.Frame.Contains (location)) + { + return null; + } + + if (_topLevels is { Count: > 0 }) + { + int rx = location.X - start.Frame.X; + int ry = location.Y - start.Frame.Y; + + foreach (Toplevel t in _topLevels) + { + if (t != Current) + { + if (t != start && t.Visible && t.Frame.Contains (rx, ry)) + { + start = t; + + break; + } + } + } + } + + return start; + } + + private static View FindTopFromView (View view) + { + View top = view?.SuperView is { } && view?.SuperView != Top + ? view.SuperView + : view; + + while (top?.SuperView is { } && top?.SuperView != Top) + { + top = top.SuperView; + } + + return top; + } + + private static bool MoveCurrent (Toplevel top) + { + // The Current is modal and the top is not modal Toplevel then + // the Current must be moved above the first not modal Toplevel. + if (OverlappedTop is { } + && top != OverlappedTop + && top != Current + && Current?.Modal == true + && !_topLevels.Peek ().Modal) + { + lock (_topLevels) + { + _topLevels.MoveTo (Current, 0, new ToplevelEqualityComparer ()); + } + + var index = 0; + Toplevel [] savedToplevels = _topLevels.ToArray (); + + foreach (Toplevel t in savedToplevels) + { + if (!t!.Modal && t != Current && t != top && t != savedToplevels [index]) + { + lock (_topLevels) + { + _topLevels.MoveTo (top, index, new ToplevelEqualityComparer ()); + } + } + + index++; + } + + return false; + } + + // The Current and the top are both not running Toplevel then + // the top must be moved above the first not running Toplevel. + if (OverlappedTop is { } + && top != OverlappedTop + && top != Current + && Current?.Running == false + && top?.Running == false) + { + lock (_topLevels) + { + _topLevels.MoveTo (Current, 0, new ToplevelEqualityComparer ()); + } + + var index = 0; + + foreach (Toplevel t in _topLevels.ToArray ()) + { + if (!t.Running && t != Current && index > 0) + { + lock (_topLevels) + { + _topLevels.MoveTo (top, index - 1, new ToplevelEqualityComparer ()); + } + } + + index++; + } + + return false; + } + + if ((OverlappedTop is { } && top?.Modal == true && _topLevels.Peek () != top) + || (OverlappedTop is { } && Current != OverlappedTop && Current?.Modal == false && top == OverlappedTop) + || (OverlappedTop is { } && Current?.Modal == false && top != Current) + || (OverlappedTop is { } && Current?.Modal == true && top == OverlappedTop)) + { + lock (_topLevels) + { + _topLevels.MoveTo (top, 0, new ToplevelEqualityComparer ()); + Current = top; + } + } + + return true; + } + + /// Invoked when the terminal's size changed. The new size of the terminal is provided. + /// + /// Event handlers can set to to prevent + /// from changing it's size to match the new terminal size. + /// + public static event EventHandler SizeChanging; + + /// + /// Called when the application's size changes. Sets the size of all s and fires the + /// event. + /// + /// The new size. + /// if the size was changed. + public static bool OnSizeChanging (SizeChangedEventArgs args) + { + SizeChanging?.Invoke (null, args); + + if (args.Cancel || args.Size is null) + { + return false; + } + + foreach (Toplevel t in _topLevels) + { + t.SetRelativeLayout (args.Size.Value); + t.LayoutSubviews (); + t.PositionToplevels (); + t.OnSizeChanging (new (args.Size)); + + if (PositionCursor (t)) + { + Driver.UpdateCursor (); + } + } + + Refresh (); + + return true; + } +} diff --git a/Terminal.Gui/Application/Application.cs b/Terminal.Gui/Application/Application.cs index c8aa2536a..202b66044 100644 --- a/Terminal.Gui/Application/Application.cs +++ b/Terminal.Gui/Application/Application.cs @@ -9,13 +9,16 @@ namespace Terminal.Gui; /// /// /// Application.Init(); -/// var win = new Window ($"Example App ({Application.QuitKey} to quit)"); +/// var win = new Window() +/// { +/// Title = $"Example App ({Application.QuitKey} to quit)" +/// }; /// Application.Run(win); /// win.Dispose(); /// Application.Shutdown(); /// /// -/// TODO: Flush this out. +/// public static partial class Application { /// Gets all cultures supported by the application without the invariant language. From 2939108bfe248475a0ce0a8cd2bbfb86cc4555c2 Mon Sep 17 00:00:00 2001 From: Tig Date: Mon, 22 Jul 2024 17:06:21 -0600 Subject: [PATCH 05/33] Added Toplevel to spelling dict --- Terminal.sln.DotSettings | 1 + 1 file changed, 1 insertion(+) diff --git a/Terminal.sln.DotSettings b/Terminal.sln.DotSettings index bf87e5ea9..0142f001b 100644 --- a/Terminal.sln.DotSettings +++ b/Terminal.sln.DotSettings @@ -405,6 +405,7 @@ True True True + True True True From 250050c8a2729846cfd4a43373a18feb92df31fd Mon Sep 17 00:00:00 2001 From: Tig Date: Mon, 22 Jul 2024 17:50:45 -0600 Subject: [PATCH 06/33] Toplevel cleanup --- .../Application/Application.Initialization.cs | 1 - .../Application/Application.Keyboard.cs | 3 + Terminal.Gui/Application/Application.cs | 218 ------------------ Terminal.Gui/Views/Toplevel.cs | 55 ++--- 4 files changed, 28 insertions(+), 249 deletions(-) diff --git a/Terminal.Gui/Application/Application.Initialization.cs b/Terminal.Gui/Application/Application.Initialization.cs index a971850e3..16b7b83d4 100644 --- a/Terminal.Gui/Application/Application.Initialization.cs +++ b/Terminal.Gui/Application/Application.Initialization.cs @@ -40,7 +40,6 @@ public static partial class Application // Initialization (Init/Shutdown) internal static bool _initialized; internal static int _mainThreadId = -1; - // INTERNAL function for initializing an app with a Toplevel factory object, driver, and mainloop. // // Called from: diff --git a/Terminal.Gui/Application/Application.Keyboard.cs b/Terminal.Gui/Application/Application.Keyboard.cs index e8d698286..703a84982 100644 --- a/Terminal.Gui/Application/Application.Keyboard.cs +++ b/Terminal.Gui/Application/Application.Keyboard.cs @@ -25,6 +25,7 @@ public static partial class Application // Keyboard handling private static void OnAlternateForwardKeyChanged (KeyChangedEventArgs e) { + // TODO: The fact Top has it's own AlternateForwardKey and events is needlessly complex. Remove it. foreach (Toplevel top in _topLevels.ToArray ()) { top.OnAlternateForwardKeyChanged (e); @@ -52,6 +53,7 @@ public static partial class Application // Keyboard handling private static void OnAlternateBackwardKeyChanged (KeyChangedEventArgs oldKey) { + // TODO: The fact Top has it's own AlternateBackwardKey and events is needlessly complex. Remove it. foreach (Toplevel top in _topLevels.ToArray ()) { top.OnAlternateBackwardKeyChanged (oldKey); @@ -79,6 +81,7 @@ public static partial class Application // Keyboard handling private static void OnQuitKeyChanged (KeyChangedEventArgs e) { + // TODO: The fact Top has it's own QuitKey and events is needlessly complex. Remove it. // Duplicate the list so if it changes during enumeration we're safe foreach (Toplevel top in _topLevels.ToArray ()) { diff --git a/Terminal.Gui/Application/Application.cs b/Terminal.Gui/Application/Application.cs index 202b66044..3b4c9d9f7 100644 --- a/Terminal.Gui/Application/Application.cs +++ b/Terminal.Gui/Application/Application.cs @@ -137,232 +137,14 @@ public static partial class Application SynchronizationContext.SetSynchronizationContext (null); } - // When `End ()` is called, it is possible `RunState.Toplevel` is a different object than `Top`. - // This field is set in `End` in this case so that `Begin` correctly sets `Top`. - - // TODO: Determine if this is really needed. The only code that calls WakeUp I can find - // is ProgressBarStyles, and it's not clear it needs to. - - #region Toplevel handling - - /// Holds the stack of TopLevel views. - - // BUGBUG: Technically, this is not the full lst of TopLevels. There be dragons here, e.g. see how Toplevel.Id is used. What - // about TopLevels that are just a SubView of another View? - internal static readonly Stack _topLevels = new (); - - /// The object used for the application on startup () - /// The top. - public static Toplevel? Top { get; private set; } - - /// - /// The current object. This is updated in enters and leaves to - /// point to the current - /// . - /// - /// - /// Only relevant in scenarios where is . - /// - /// The current. - public static Toplevel? Current { get; private set; } - - private static void EnsureModalOrVisibleAlwaysOnTop (Toplevel topLevel) - { - if (!topLevel.Running - || (topLevel == Current && topLevel.Visible) - || OverlappedTop == null - || _topLevels.Peek ().Modal) - { - return; - } - - foreach (Toplevel? top in _topLevels.Reverse ()) - { - if (top.Modal && top != Current) - { - MoveCurrent (top); - - return; - } - } - - if (!topLevel.Visible && topLevel == Current) - { - OverlappedMoveNext (); - } - } - #nullable enable - private static Toplevel? FindDeepestTop (Toplevel start, in Point location) - { - if (!start.Frame.Contains (location)) - { - return null; - } - - if (_topLevels is { Count: > 0 }) - { - int rx = location.X - start.Frame.X; - int ry = location.Y - start.Frame.Y; - - foreach (Toplevel? t in _topLevels) - { - if (t != Current) - { - if (t != start && t.Visible && t.Frame.Contains (rx, ry)) - { - start = t; - - break; - } - } - } - } - - return start; - } #nullable restore - private static View FindTopFromView (View view) - { - View top = view?.SuperView is { } && view?.SuperView != Top - ? view.SuperView - : view; - - while (top?.SuperView is { } && top?.SuperView != Top) - { - top = top.SuperView; - } - - return top; - } - #nullable enable // Only return true if the Current has changed. - private static bool MoveCurrent (Toplevel top) - { - // The Current is modal and the top is not modal Toplevel then - // the Current must be moved above the first not modal Toplevel. - if (OverlappedTop is { } - && top != OverlappedTop - && top != Current - && Current?.Modal == true - && !_topLevels.Peek ().Modal) - { - lock (_topLevels) - { - _topLevels.MoveTo (Current, 0, new ToplevelEqualityComparer ()); - } - - var index = 0; - Toplevel? [] savedToplevels = _topLevels.ToArray (); - - foreach (Toplevel? t in savedToplevels) - { - if (!t!.Modal && t != Current && t != top && t != savedToplevels [index]) - { - lock (_topLevels) - { - _topLevels.MoveTo (top, index, new ToplevelEqualityComparer ()); - } - } - - index++; - } - - return false; - } - - // The Current and the top are both not running Toplevel then - // the top must be moved above the first not running Toplevel. - if (OverlappedTop is { } - && top != OverlappedTop - && top != Current - && Current?.Running == false - && top?.Running == false) - { - lock (_topLevels) - { - _topLevels.MoveTo (Current, 0, new ToplevelEqualityComparer ()); - } - - var index = 0; - - foreach (Toplevel? t in _topLevels.ToArray ()) - { - if (!t.Running && t != Current && index > 0) - { - lock (_topLevels) - { - _topLevels.MoveTo (top, index - 1, new ToplevelEqualityComparer ()); - } - } - - index++; - } - - return false; - } - - if ((OverlappedTop is { } && top?.Modal == true && _topLevels.Peek () != top) - || (OverlappedTop is { } && Current != OverlappedTop && Current?.Modal == false && top == OverlappedTop) - || (OverlappedTop is { } && Current?.Modal == false && top != Current) - || (OverlappedTop is { } && Current?.Modal == true && top == OverlappedTop)) - { - lock (_topLevels) - { - _topLevels.MoveTo (top, 0, new ToplevelEqualityComparer ()); - Current = top; - } - } - - return true; - } #nullable restore - /// Invoked when the terminal's size changed. The new size of the terminal is provided. - /// - /// Event handlers can set to to prevent - /// from changing it's size to match the new terminal size. - /// - public static event EventHandler SizeChanging; - - /// - /// Called when the application's size changes. Sets the size of all s and fires the - /// event. - /// - /// The new size. - /// if the size was changed. - public static bool OnSizeChanging (SizeChangedEventArgs args) - { - SizeChanging?.Invoke (null, args); - - if (args.Cancel || args.Size is null) - { - return false; - } - - foreach (Toplevel t in _topLevels) - { - t.SetRelativeLayout (args.Size.Value); - t.LayoutSubviews (); - t.PositionToplevels (); - t.OnSizeChanging (new (args.Size)); - - if (PositionCursor (t)) - { - Driver.UpdateCursor (); - } - } - - Refresh (); - - return true; - } - - #endregion Toplevel handling - /// /// Gets a string representation of the Application as rendered by . /// diff --git a/Terminal.Gui/Views/Toplevel.cs b/Terminal.Gui/Views/Toplevel.cs index 617dfa7fc..32d8f8a89 100644 --- a/Terminal.Gui/Views/Toplevel.cs +++ b/Terminal.Gui/Views/Toplevel.cs @@ -6,7 +6,7 @@ namespace Terminal.Gui; /// /// /// -/// Toplevels can run as modal (popup) views, started by calling +/// Toplevel views can run as modal (popup) views, started by calling /// . They return control to the caller when /// has been called (which sets the /// property to false). @@ -14,7 +14,7 @@ namespace Terminal.Gui; /// /// A Toplevel is created when an application initializes Terminal.Gui by calling . /// The application Toplevel can be accessed via . Additional Toplevels can be created -/// and run (e.g. s. To run a Toplevel, create the and call +/// and run (e.g. s). To run a Toplevel, create the and call /// . /// /// @@ -259,32 +259,30 @@ public partial class Toplevel : View return; } - if (NeedsDisplay || SubViewNeedsDisplay || LayoutNeeded) + if (NeedsDisplay || SubViewNeedsDisplay /*|| LayoutNeeded*/) { - //Driver.SetAttribute (GetNormalColor ()); - // TODO: It's bad practice for views to always clear. Defeats the purpose of clipping etc... Clear (); - LayoutSubviews (); - PositionToplevels (); + //LayoutSubviews (); + //PositionToplevels (); - if (this == Application.OverlappedTop) - { - foreach (Toplevel top in Application.OverlappedChildren.AsEnumerable ().Reverse ()) - { - if (top.Frame.IntersectsWith (Viewport)) - { - if (top != this && !top.IsCurrentTop && !OutsideTopFrame (top) && top.Visible) - { - top.SetNeedsLayout (); - top.SetNeedsDisplay (top.Viewport); - top.Draw (); - top.OnRenderLineCanvas (); - } - } - } - } + //if (this == Application.OverlappedTop) + //{ + // foreach (Toplevel top in Application.OverlappedChildren.AsEnumerable ().Reverse ()) + // { + // if (top.Frame.IntersectsWith (Viewport)) + // { + // if (top != this && !top.IsCurrentTop && !OutsideTopFrame (top) && top.Visible) + // { + // top.SetNeedsLayout (); + // top.SetNeedsDisplay (top.Viewport); + // top.Draw (); + // top.OnRenderLineCanvas (); + // } + // } + // } + //} - // This should not be here, but in base + // BUGBUG: This appears to be a hack to get ScrollBarViews to render correctly. foreach (View view in Subviews) { if (view.Frame.IntersectsWith (Viewport) && !OutsideTopFrame (this)) @@ -296,12 +294,6 @@ public partial class Toplevel : View } base.OnDrawContent (viewport); - - // This is causing the menus drawn incorrectly if UseSubMenusSingleFrame is true - //if (this.MenuBar is { } && this.MenuBar.IsMenuOpen && this.MenuBar.openMenu is { }) { - // // TODO: Hack until we can get compositing working right. - // this.MenuBar.openMenu.Redraw (this.MenuBar.openMenu.Viewport); - //} } } @@ -315,6 +307,9 @@ public partial class Toplevel : View /// Called from before the redraws for the first /// time. /// + /// + /// Overrides must call base.OnLoaded() to ensure any Toplevel subviews are initialized properly and the event is raised. + /// public virtual void OnLoaded () { IsLoaded = true; From 47010805cdb3312135da0b3367ebbc1cac54e7e5 Mon Sep 17 00:00:00 2001 From: Tig Date: Tue, 23 Jul 2024 09:41:21 -0600 Subject: [PATCH 07/33] Toplevel.cs organization --- Terminal.Gui/Views/Toplevel.cs | 708 +++++++++++++++++---------------- 1 file changed, 376 insertions(+), 332 deletions(-) diff --git a/Terminal.Gui/Views/Toplevel.cs b/Terminal.Gui/Views/Toplevel.cs index 32d8f8a89..fa526904d 100644 --- a/Terminal.Gui/Views/Toplevel.cs +++ b/Terminal.Gui/Views/Toplevel.cs @@ -27,12 +27,45 @@ public partial class Toplevel : View /// public Toplevel () { + CanFocus = true; Arrangement = ViewArrangement.Fixed; Width = Dim.Fill (); Height = Dim.Fill (); ColorScheme = Colors.ColorSchemes ["TopLevel"]; + ConfigureKeyBindings (); + + MouseClick += Toplevel_MouseClick; + } + + // TODO: IRunnable: Re-implement - Modal means IRunnable, ViewArrangement.Overlapped where modalView.Z > allOtherViews.Max (v = v.Z). + /// + /// Determines whether the is modal or not. If set to false (the default): + /// + /// + /// events will propagate keys upwards. + /// + /// + /// The Toplevel will act as an embedded view (not a modal/pop-up). + /// + /// + /// If set to true: + /// + /// + /// events will NOT propagate keys upwards. + /// + /// + /// The Toplevel will and look like a modal (pop-up) (e.g. see . + /// + /// + /// + public bool Modal { get; set; } + + #region Keyboard & Mouse + + private void ConfigureKeyBindings () + { // Things this view knows how to do AddCommand ( Command.QuitToplevel, @@ -134,107 +167,19 @@ public partial class Toplevel : View KeyBindings.Add (Key.I.WithCtrl, Command.NextView); // Unix KeyBindings.Add (Key.B.WithCtrl, Command.PreviousView); // Unix #endif - MouseClick += Toplevel_MouseClick; - - CanFocus = true; } - private void Toplevel_MouseClick (object sender, MouseEventEventArgs e) - { - e.Handled = InvokeCommand (Command.HotKey) == true; - } - - /// - /// if was already loaded by the - /// , otherwise. - /// - public bool IsLoaded { get; private set; } - - /// Gets or sets the menu for this Toplevel. - public virtual MenuBar MenuBar { get; set; } - - /// - /// Determines whether the is modal or not. If set to false (the default): - /// - /// - /// events will propagate keys upwards. - /// - /// - /// The Toplevel will act as an embedded view (not a modal/pop-up). - /// - /// - /// If set to true: - /// - /// - /// events will NOT propagate keys upwards. - /// - /// - /// The Toplevel will and look like a modal (pop-up) (e.g. see . - /// - /// - /// - public bool Modal { get; set; } - - /// Gets or sets whether the main loop for this is running or not. - /// Setting this property directly is discouraged. Use instead. - public bool Running { get; set; } - - /// Gets or sets the status bar for this Toplevel. - public virtual StatusBar StatusBar { get; set; } - - /// Invoked when the Toplevel becomes the Toplevel. - public event EventHandler Activate; - - /// - public override View Add (View view) - { - CanFocus = true; - AddMenuStatusBar (view); - return base.Add (view); - } - - /// - /// Invoked when the last child of the Toplevel is closed from by - /// . - /// - public event EventHandler AllChildClosed; + private void Toplevel_MouseClick (object sender, MouseEventEventArgs e) { e.Handled = InvokeCommand (Command.HotKey) == true; } + // TODO: Deprecate - No need for this at View level; having at Application is sufficient. /// Invoked when the is changed. public event EventHandler AlternateBackwardKeyChanged; + // TODO: Deprecate - No need for this at View level; having at Application is sufficient. /// Invoked when the is changed. public event EventHandler AlternateForwardKeyChanged; - /// - /// Invoked when a child of the Toplevel is closed by - /// . - /// - public event EventHandler ChildClosed; - - /// Invoked when a child Toplevel's has been loaded. - public event EventHandler ChildLoaded; - - /// Invoked when a cjhild Toplevel's has been unloaded. - public event EventHandler ChildUnloaded; - - /// Invoked when the Toplevel's is closed by . - public event EventHandler Closed; - - /// - /// Invoked when the Toplevel's is being closed by - /// . - /// - public event EventHandler Closing; - - /// Invoked when the Toplevel ceases to be the Toplevel. - public event EventHandler Deactivate; - - /// - /// Invoked when the has begun to be loaded. A Loaded event handler - /// is a good place to finalize initialization before calling . - /// - public event EventHandler Loaded; - + // TODO: Deprecate - No need for this at View level; having at Application is sufficient. /// Virtual method to invoke the event. /// public virtual void OnAlternateBackwardKeyChanged (KeyChangedEventArgs e) @@ -243,6 +188,7 @@ public partial class Toplevel : View AlternateBackwardKeyChanged?.Invoke (this, e); } + // TODO: Deprecate - No need for this at View level; having at Application is sufficient. /// Virtual method to invoke the event. /// public virtual void OnAlternateForwardKeyChanged (KeyChangedEventArgs e) @@ -251,77 +197,6 @@ public partial class Toplevel : View AlternateForwardKeyChanged?.Invoke (this, e); } - /// - public override void OnDrawContent (Rectangle viewport) - { - if (!Visible) - { - return; - } - - if (NeedsDisplay || SubViewNeedsDisplay /*|| LayoutNeeded*/) - { - Clear (); - //LayoutSubviews (); - //PositionToplevels (); - - //if (this == Application.OverlappedTop) - //{ - // foreach (Toplevel top in Application.OverlappedChildren.AsEnumerable ().Reverse ()) - // { - // if (top.Frame.IntersectsWith (Viewport)) - // { - // if (top != this && !top.IsCurrentTop && !OutsideTopFrame (top) && top.Visible) - // { - // top.SetNeedsLayout (); - // top.SetNeedsDisplay (top.Viewport); - // top.Draw (); - // top.OnRenderLineCanvas (); - // } - // } - // } - //} - - // BUGBUG: This appears to be a hack to get ScrollBarViews to render correctly. - foreach (View view in Subviews) - { - if (view.Frame.IntersectsWith (Viewport) && !OutsideTopFrame (this)) - { - //view.SetNeedsLayout (); - view.SetNeedsDisplay (); - view.SetSubViewNeedsDisplay (); - } - } - - base.OnDrawContent (viewport); - } - } - - /// - public override bool OnEnter (View view) { return MostFocused?.OnEnter (view) ?? base.OnEnter (view); } - - /// - public override bool OnLeave (View view) { return MostFocused?.OnLeave (view) ?? base.OnLeave (view); } - - /// - /// Called from before the redraws for the first - /// time. - /// - /// - /// Overrides must call base.OnLoaded() to ensure any Toplevel subviews are initialized properly and the event is raised. - /// - public virtual void OnLoaded () - { - IsLoaded = true; - - foreach (Toplevel tl in Subviews.Where (v => v is Toplevel)) - { - tl.OnLoaded (); - } - - Loaded?.Invoke (this, EventArgs.Empty); - } - /// Virtual method to invoke the event. /// public virtual void OnQuitKeyChanged (KeyChangedEventArgs e) @@ -330,120 +205,29 @@ public partial class Toplevel : View QuitKeyChanged?.Invoke (this, e); } - /// - public override Point? PositionCursor () - { - if (!IsOverlappedContainer) - { - if (Focused is null) - { - EnsureFocus (); - } - - return null; - } - - // This code path only happens when the Toplevel is an Overlapped container - - if (Focused is null) - { - // TODO: this is an Overlapped hack - foreach (Toplevel top in Application.OverlappedChildren) - { - if (top != this && top.Visible) - { - top.SetFocus (); - - return null; - } - } - } - - var cursor2 = base.PositionCursor (); - - return null; - } - - /// - /// Adjusts the location and size of within this Toplevel. Virtual method enabling - /// implementation of specific positions for inherited views. - /// - /// The Toplevel to adjust. - public virtual void PositionToplevel (Toplevel top) - { - - View superView = GetLocationEnsuringFullVisibility ( - top, - top.Frame.X, - top.Frame.Y, - out int nx, - out int ny, - out StatusBar sb - ); - - if (superView is null) - { - return; - } - - var layoutSubviews = false; - var maxWidth = 0; - - if (superView.Margin is { } && superView == top.SuperView) - { - maxWidth -= superView.GetAdornmentsThickness ().Left + superView.GetAdornmentsThickness ().Right; - } - - if ((superView != top || top?.SuperView is { } || (top != Application.Top && top.Modal) || (top?.SuperView is null && top.IsOverlapped)) - && (top.Frame.X + top.Frame.Width > maxWidth || ny > top.Frame.Y)) - { - if ((top.X is null || top.X is PosAbsolute) && top.Frame.X != nx) - { - top.X = nx; - layoutSubviews = true; - } - - if ((top.Y is null || top.Y is PosAbsolute) && top.Frame.Y != ny) - { - top.Y = ny; - layoutSubviews = true; - } - } - - // TODO: v2 - This is a hack to get the StatusBar to be positioned correctly. - if (sb != null - && !top.Subviews.Contains (sb) - && ny + top.Frame.Height != superView.Frame.Height - (sb.Visible ? 1 : 0) - && top.Height is DimFill - && -top.Height.GetAnchor (0) < 1) - { - top.Height = Dim.Fill (sb.Visible ? 1 : 0); - layoutSubviews = true; - } - - if (superView.LayoutNeeded || layoutSubviews) - { - superView.LayoutSubviews (); - } - - if (LayoutNeeded) - { - LayoutSubviews (); - } - } - /// Invoked when the is changed. public event EventHandler QuitKeyChanged; - /// - /// Invoked when the main loop has started it's first iteration. Subscribe to this event to - /// perform tasks when the has been laid out and focus has been set. changes. - /// - /// A Ready event handler is a good place to finalize initialization after calling - /// on this . - /// - /// - public event EventHandler Ready; + #endregion + + #region Subviews + + // TODO: Deprecate - Any view can host a menubar in v2 + /// Gets or sets the menu for this Toplevel. + public virtual MenuBar MenuBar { get; set; } + + // TODO: Deprecate - Any view can host a statusbar in v2 + /// Gets or sets the status bar for this Toplevel. + public virtual StatusBar StatusBar { get; set; } + + /// + public override View Add (View view) + { + CanFocus = true; + AddMenuStatusBar (view); + + return base.Add (view); + } /// public override View Remove (View view) @@ -470,6 +254,125 @@ public partial class Toplevel : View base.RemoveAll (); } + internal void AddMenuStatusBar (View view) + { + if (view is MenuBar) + { + MenuBar = view as MenuBar; + } + + if (view is StatusBar) + { + StatusBar = view as StatusBar; + } + } + + internal void RemoveMenuStatusBar (View view) + { + if (view is MenuBar) + { + MenuBar?.Dispose (); + MenuBar = null; + } + + if (view is StatusBar) + { + StatusBar?.Dispose (); + StatusBar = null; + } + } + + // TODO: Overlapped - Rename to AllSubviewsClosed - Move to View? + /// + /// Invoked when the last child of the Toplevel is closed from by + /// . + /// + public event EventHandler AllChildClosed; + + // TODO: Overlapped - Rename to *Subviews* - Move to View? + /// + /// Invoked when a child of the Toplevel is closed by + /// . + /// + public event EventHandler ChildClosed; + + // TODO: Overlapped - Rename to *Subviews* - Move to View? + /// Invoked when a child Toplevel's has been loaded. + public event EventHandler ChildLoaded; + + // TODO: Overlapped - Rename to *Subviews* - Move to View? + /// Invoked when a cjhild Toplevel's has been unloaded. + public event EventHandler ChildUnloaded; + + #endregion + + #region Life Cycle + + // TODO: IRunnable: Re-implement as a property on IRunnable + /// Gets or sets whether the main loop for this is running or not. + /// Setting this property directly is discouraged. Use instead. + public bool Running { get; set; } + + // TODO: IRunnable: Re-implement in IRunnable + /// + /// if was already loaded by the + /// , otherwise. + /// + public bool IsLoaded { get; private set; } + + // TODO: IRunnable: Re-implement as an event on IRunnable; IRunnable.Activating/Activate + /// Invoked when the Toplevel becomes the Toplevel. + public event EventHandler Activate; + + // TODO: IRunnable: Re-implement as an event on IRunnable; IRunnable.Deactivating/Deactivate? + /// Invoked when the Toplevel ceases to be the Toplevel. + public event EventHandler Deactivate; + + /// Invoked when the Toplevel's is closed by . + public event EventHandler Closed; + + /// + /// Invoked when the Toplevel's is being closed by + /// . + /// + public event EventHandler Closing; + + /// + /// Invoked when the has begun to be loaded. A Loaded event handler + /// is a good place to finalize initialization before calling . + /// + public event EventHandler Loaded; + + /// + /// Called from before the redraws for the first + /// time. + /// + /// + /// Overrides must call base.OnLoaded() to ensure any Toplevel subviews are initialized properly and the + /// event is raised. + /// + public virtual void OnLoaded () + { + IsLoaded = true; + + foreach (Toplevel tl in Subviews.Where (v => v is Toplevel)) + { + tl.OnLoaded (); + } + + Loaded?.Invoke (this, EventArgs.Empty); + } + + /// + /// Invoked when the main loop has started it's first iteration. Subscribe to this event to + /// perform tasks when the has been laid out and focus has been set. changes. + /// + /// A Ready event handler is a good place to finalize initialization after calling + /// on this . + /// + /// + public event EventHandler Ready; + /// /// Stops and closes this . If this Toplevel is the top-most Toplevel, /// will be called, causing the application to exit. @@ -527,6 +430,14 @@ public partial class Toplevel : View } } + /// + /// Invoked when the Toplevel has been unloaded. A Unloaded event handler is a good place + /// to dispose objects after calling . + /// + public event EventHandler Unloaded; + + internal virtual void OnActivate (Toplevel deactivated) { Activate?.Invoke (this, new (deactivated)); } + /// /// Stops and closes the specified by . If is /// the top-most Toplevel, will be called, causing the application to @@ -535,29 +446,6 @@ public partial class Toplevel : View /// The Toplevel to request stop. public virtual void RequestStop (Toplevel top) { top.RequestStop (); } - /// Invoked when the terminal has been resized. The new of the terminal is provided. - public event EventHandler SizeChanging; - - /// - /// Invoked when the Toplevel has been unloaded. A Unloaded event handler is a good place - /// to dispose objects after calling . - /// - public event EventHandler Unloaded; - - internal void AddMenuStatusBar (View view) - { - if (view is MenuBar) - { - MenuBar = view as MenuBar; - } - - if (view is StatusBar) - { - StatusBar = view as StatusBar; - } - } - - internal virtual void OnActivate (Toplevel deactivated) { Activate?.Invoke (this, new ToplevelEventArgs (deactivated)); } internal virtual void OnAllChildClosed () { AllChildClosed?.Invoke (this, EventArgs.Empty); } internal virtual void OnChildClosed (Toplevel top) @@ -567,12 +455,12 @@ public partial class Toplevel : View SetSubViewNeedsDisplay (); } - ChildClosed?.Invoke (this, new ToplevelEventArgs (top)); + ChildClosed?.Invoke (this, new (top)); } - internal virtual void OnChildLoaded (Toplevel top) { ChildLoaded?.Invoke (this, new ToplevelEventArgs (top)); } - internal virtual void OnChildUnloaded (Toplevel top) { ChildUnloaded?.Invoke (this, new ToplevelEventArgs (top)); } - internal virtual void OnClosed (Toplevel top) { Closed?.Invoke (this, new ToplevelEventArgs (top)); } + internal virtual void OnChildLoaded (Toplevel top) { ChildLoaded?.Invoke (this, new (top)); } + internal virtual void OnChildUnloaded (Toplevel top) { ChildUnloaded?.Invoke (this, new (top)); } + internal virtual void OnClosed (Toplevel top) { Closed?.Invoke (this, new (top)); } internal virtual bool OnClosing (ToplevelClosingEventArgs ev) { @@ -581,7 +469,7 @@ public partial class Toplevel : View return ev.Cancel; } - internal virtual void OnDeactivate (Toplevel activated) { Deactivate?.Invoke (this, new ToplevelEventArgs (activated)); } + internal virtual void OnDeactivate (Toplevel activated) { Deactivate?.Invoke (this, new (activated)); } /// /// Called from after the has entered the first iteration @@ -597,9 +485,6 @@ public partial class Toplevel : View Ready?.Invoke (this, EventArgs.Empty); } - // TODO: Make cancelable? - internal virtual void OnSizeChanging (SizeChangedEventArgs size) { SizeChanging?.Invoke (this, size); } - /// Called from before the is disposed. internal virtual void OnUnloaded () { @@ -611,34 +496,78 @@ public partial class Toplevel : View Unloaded?.Invoke (this, EventArgs.Empty); } - // TODO: v2 - Not sure this is needed anymore. - internal void PositionToplevels () + private void QuitToplevel () { - PositionToplevel (this); - - foreach (View top in Subviews) + if (Application.OverlappedTop is { }) { - if (top is Toplevel) + RequestStop (this); + } + else + { + Application.RequestStop (); + } + } + + #endregion + + #region Draw + + /// + public override void OnDrawContent (Rectangle viewport) + { + if (!Visible) + { + return; + } + + if (NeedsDisplay || SubViewNeedsDisplay /*|| LayoutNeeded*/) + { + Clear (); + + //LayoutSubviews (); + //PositionToplevels (); + + //if (this == Application.OverlappedTop) + //{ + // foreach (Toplevel top in Application.OverlappedChildren.AsEnumerable ().Reverse ()) + // { + // if (top.Frame.IntersectsWith (Viewport)) + // { + // if (top != this && !top.IsCurrentTop && !OutsideTopFrame (top) && top.Visible) + // { + // top.SetNeedsLayout (); + // top.SetNeedsDisplay (top.Viewport); + // top.Draw (); + // top.OnRenderLineCanvas (); + // } + // } + // } + //} + + // BUGBUG: This appears to be a hack to get ScrollBarViews to render correctly. + foreach (View view in Subviews) { - PositionToplevel ((Toplevel)top); + if (view.Frame.IntersectsWith (Viewport) && !OutsideTopFrame (this)) + { + //view.SetNeedsLayout (); + view.SetNeedsDisplay (); + view.SetSubViewNeedsDisplay (); + } } + + base.OnDrawContent (viewport); } } - internal void RemoveMenuStatusBar (View view) - { - if (view is MenuBar) - { - MenuBar?.Dispose (); - MenuBar = null; - } + #endregion - if (view is StatusBar) - { - StatusBar?.Dispose (); - StatusBar = null; - } - } + #region Focus + + /// + public override bool OnEnter (View view) { return MostFocused?.OnEnter (view) ?? base.OnEnter (view); } + + /// + public override bool OnLeave (View view) { return MostFocused?.OnLeave (view) ?? base.OnLeave (view); } private void FocusNearestView (IEnumerable views, NavigationDirection direction) { @@ -785,6 +714,117 @@ public partial class Toplevel : View } } + #endregion + + #region Size / Position Management + + // TODO: Make cancelable? + internal virtual void OnSizeChanging (SizeChangedEventArgs size) { SizeChanging?.Invoke (this, size); } + + /// + public override Point? PositionCursor () + { + if (!IsOverlappedContainer) + { + if (Focused is null) + { + EnsureFocus (); + } + + return null; + } + + // This code path only happens when the Toplevel is an Overlapped container + + if (Focused is null) + { + // TODO: this is an Overlapped hack + foreach (Toplevel top in Application.OverlappedChildren) + { + if (top != this && top.Visible) + { + top.SetFocus (); + + return null; + } + } + } + + Point? cursor2 = base.PositionCursor (); + + return null; + } + + /// + /// Adjusts the location and size of within this Toplevel. Virtual method enabling + /// implementation of specific positions for inherited views. + /// + /// The Toplevel to adjust. + public virtual void PositionToplevel (Toplevel top) + { + View superView = GetLocationEnsuringFullVisibility ( + top, + top.Frame.X, + top.Frame.Y, + out int nx, + out int ny, + out StatusBar sb + ); + + if (superView is null) + { + return; + } + + var layoutSubviews = false; + var maxWidth = 0; + + if (superView.Margin is { } && superView == top.SuperView) + { + maxWidth -= superView.GetAdornmentsThickness ().Left + superView.GetAdornmentsThickness ().Right; + } + + if ((superView != top || top?.SuperView is { } || (top != Application.Top && top.Modal) || (top?.SuperView is null && top.IsOverlapped)) + && (top.Frame.X + top.Frame.Width > maxWidth || ny > top.Frame.Y)) + { + if ((top.X is null || top.X is PosAbsolute) && top.Frame.X != nx) + { + top.X = nx; + layoutSubviews = true; + } + + if ((top.Y is null || top.Y is PosAbsolute) && top.Frame.Y != ny) + { + top.Y = ny; + layoutSubviews = true; + } + } + + // TODO: v2 - This is a hack to get the StatusBar to be positioned correctly. + if (sb != null + && !top.Subviews.Contains (sb) + && ny + top.Frame.Height != superView.Frame.Height - (sb.Visible ? 1 : 0) + && top.Height is DimFill + && -top.Height.GetAnchor (0) < 1) + { + top.Height = Dim.Fill (sb.Visible ? 1 : 0); + layoutSubviews = true; + } + + if (superView.LayoutNeeded || layoutSubviews) + { + superView.LayoutSubviews (); + } + + if (LayoutNeeded) + { + LayoutSubviews (); + } + } + + /// Invoked when the terminal has been resized. The new of the terminal is provided. + public event EventHandler SizeChanging; + private bool OutsideTopFrame (Toplevel top) { if (top.Frame.X > Driver.Cols || top.Frame.Y > Driver.Rows) @@ -795,17 +835,21 @@ public partial class Toplevel : View return false; } - private void QuitToplevel () + // TODO: v2 - Not sure this is needed anymore. + internal void PositionToplevels () { - if (Application.OverlappedTop is { }) + PositionToplevel (this); + + foreach (View top in Subviews) { - RequestStop (this); - } - else - { - Application.RequestStop (); + if (top is Toplevel) + { + PositionToplevel ((Toplevel)top); + } } } + + #endregion } /// From d44e8d3b81b0d98f9c022d1ef2c8839728c1f9df Mon Sep 17 00:00:00 2001 From: Tig Date: Tue, 23 Jul 2024 09:59:30 -0600 Subject: [PATCH 08/33] More Toplevel.cs organization & docs --- Terminal.Gui/Input/Command.cs | 1 + Terminal.Gui/Views/Toplevel.cs | 59 ++++++++++++++++++---------------- 2 files changed, 33 insertions(+), 27 deletions(-) diff --git a/Terminal.Gui/Input/Command.cs b/Terminal.Gui/Input/Command.cs index 566cc9336..c6a03606b 100644 --- a/Terminal.Gui/Input/Command.cs +++ b/Terminal.Gui/Input/Command.cs @@ -221,6 +221,7 @@ public enum Command /// Pastes the current selection. Paste, + // TODO: IRunnable - Should be renamed QuitRunnable /// Quit a . QuitToplevel, diff --git a/Terminal.Gui/Views/Toplevel.cs b/Terminal.Gui/Views/Toplevel.cs index fa526904d..e14d02509 100644 --- a/Terminal.Gui/Views/Toplevel.cs +++ b/Terminal.Gui/Views/Toplevel.cs @@ -39,7 +39,10 @@ public partial class Toplevel : View MouseClick += Toplevel_MouseClick; } - // TODO: IRunnable: Re-implement - Modal means IRunnable, ViewArrangement.Overlapped where modalView.Z > allOtherViews.Max (v = v.Z). + + #region Keyboard & Mouse + + // TODO: IRunnable: Re-implement - Modal means IRunnable, ViewArrangement.Overlapped where modalView.Z > allOtherViews.Max (v = v.Z), and exclusive key/mouse input. /// /// Determines whether the is modal or not. If set to false (the default): /// @@ -62,13 +65,12 @@ public partial class Toplevel : View /// public bool Modal { get; set; } - #region Keyboard & Mouse - + // TODO: Overlapped: Figure out how these keybindings should work. private void ConfigureKeyBindings () { // Things this view knows how to do AddCommand ( - Command.QuitToplevel, + Command.QuitToplevel, // TODO: IRunnable: Rename to Command.Quit to make more generic. () => { QuitToplevel (); @@ -77,8 +79,10 @@ public partial class Toplevel : View } ); + /// TODO: Overlapped: Add Command.ShowHide + AddCommand ( - Command.Suspend, + Command.Suspend, // TODO: Move to Application () => { Driver.Suspend (); @@ -89,7 +93,7 @@ public partial class Toplevel : View ); AddCommand ( - Command.NextView, + Command.NextView, // TODO: Figure out how to move this to the View that is at the root of the view hierarchy (currently Application.Top) () => { MoveNextView (); @@ -99,7 +103,7 @@ public partial class Toplevel : View ); AddCommand ( - Command.PreviousView, + Command.PreviousView,// TODO: Figure out how to move this to the View that is at the root of the view hierarchy (currently Application.Top) () => { MovePreviousView (); @@ -109,7 +113,7 @@ public partial class Toplevel : View ); AddCommand ( - Command.NextViewOrTop, + Command.NextViewOrTop,// TODO: Figure out how to move this to the View that is at the root of the view hierarchy (currently Application.Top) () => { MoveNextViewOrTop (); @@ -119,7 +123,7 @@ public partial class Toplevel : View ); AddCommand ( - Command.PreviousViewOrTop, + Command.PreviousViewOrTop,// TODO: Figure out how to move this to the View that is at the root of the view hierarchy (currently Application.Top) () => { MovePreviousViewOrTop (); @@ -132,7 +136,7 @@ public partial class Toplevel : View Command.Refresh, () => { - Application.Refresh (); + Application.Refresh (); // TODO: Move to Application return true; } @@ -525,24 +529,25 @@ public partial class Toplevel : View Clear (); //LayoutSubviews (); - //PositionToplevels (); + PositionToplevels (); - //if (this == Application.OverlappedTop) - //{ - // foreach (Toplevel top in Application.OverlappedChildren.AsEnumerable ().Reverse ()) - // { - // if (top.Frame.IntersectsWith (Viewport)) - // { - // if (top != this && !top.IsCurrentTop && !OutsideTopFrame (top) && top.Visible) - // { - // top.SetNeedsLayout (); - // top.SetNeedsDisplay (top.Viewport); - // top.Draw (); - // top.OnRenderLineCanvas (); - // } - // } - // } - //} + if (this == Application.OverlappedTop) + { + // This enables correct draw behavior when switching between overlapped subviews + foreach (Toplevel top in Application.OverlappedChildren.AsEnumerable ().Reverse ()) + { + if (top.Frame.IntersectsWith (Viewport)) + { + if (top != this && !top.IsCurrentTop && !OutsideTopFrame (top) && top.Visible) + { + top.SetNeedsLayout (); + top.SetNeedsDisplay (top.Viewport); + top.Draw (); + top.OnRenderLineCanvas (); + } + } + } + } // BUGBUG: This appears to be a hack to get ScrollBarViews to render correctly. foreach (View view in Subviews) From d813b1f137c87c86271d71539285cc64343b1826 Mon Sep 17 00:00:00 2001 From: Tig Date: Tue, 23 Jul 2024 14:19:57 -0600 Subject: [PATCH 09/33] Fixed dumb enum cast in KeyBinding code --- Terminal.Gui/Application/Application.Keyboard.cs | 3 ++- Terminal.Gui/Input/Command.cs | 4 +++- Terminal.Gui/Input/KeyBindingScope.cs | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Terminal.Gui/Application/Application.Keyboard.cs b/Terminal.Gui/Application/Application.Keyboard.cs index 703a84982..59fecc14a 100644 --- a/Terminal.Gui/Application/Application.Keyboard.cs +++ b/Terminal.Gui/Application/Application.Keyboard.cs @@ -144,7 +144,8 @@ public static partial class Application // Keyboard handling { foreach (View view in binding.Value) { - if (view is {} && view.KeyBindings.TryGet (binding.Key, (KeyBindingScope)0xFFFF, out KeyBinding kb)) + if (view is { } + && view.KeyBindings.TryGet (binding.Key, KeyBindingScope.Focused | KeyBindingScope.HotKey | KeyBindingScope.Application, out KeyBinding kb)) { //bool? handled = view.InvokeCommands (kb.Commands, binding.Key, kb); bool? handled = view?.OnInvokingKeyBindings (keyEvent, kb.Scope); diff --git a/Terminal.Gui/Input/Command.cs b/Terminal.Gui/Input/Command.cs index c6a03606b..42dcb16e6 100644 --- a/Terminal.Gui/Input/Command.cs +++ b/Terminal.Gui/Input/Command.cs @@ -221,10 +221,12 @@ public enum Command /// Pastes the current selection. Paste, - // TODO: IRunnable - Should be renamed QuitRunnable + /// TODO: IRunnable: Rename to Command.Quit to make more generic. /// Quit a . QuitToplevel, + /// TODO: Overlapped: Add Command.ShowHide + /// Suspend an application (Only implemented in ). Suspend, diff --git a/Terminal.Gui/Input/KeyBindingScope.cs b/Terminal.Gui/Input/KeyBindingScope.cs index 0c75299c7..633e6d7b0 100644 --- a/Terminal.Gui/Input/KeyBindingScope.cs +++ b/Terminal.Gui/Input/KeyBindingScope.cs @@ -45,5 +45,5 @@ public enum KeyBindingScope /// any of its subviews, and if the key was not bound to a . /// /// - Application = 4 + Application = 4, } From fe5cbe4df3192d22474f3f0fd834004dc3d84711 Mon Sep 17 00:00:00 2001 From: Tig Date: Tue, 23 Jul 2024 14:42:50 -0600 Subject: [PATCH 10/33] More Toplevel.cs organization & docs --- Terminal.Gui/Application/Application.Keyboard.cs | 9 +++++++-- Terminal.Gui/Application/Application.Run.cs | 1 + Terminal.Gui/Application/Application.Toplevel.cs | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Terminal.Gui/Application/Application.Keyboard.cs b/Terminal.Gui/Application/Application.Keyboard.cs index 59fecc14a..009d8c425 100644 --- a/Terminal.Gui/Application/Application.Keyboard.cs +++ b/Terminal.Gui/Application/Application.Keyboard.cs @@ -230,9 +230,14 @@ public static partial class Application // Keyboard handling /// This is an internal method used by the class to add Application key bindings. /// /// The key being bound. - /// The view that is bound to the key. - internal static void AddKeyBinding (Key key, View view) + /// The view that is bound to the key. If , will be used. + internal static void AddKeyBinding (Key key, [CanBeNull] View view) { + if (view is null) + { + view = Current; + } + if (!_keyBindings.ContainsKey (key)) { _keyBindings [key] = []; diff --git a/Terminal.Gui/Application/Application.Run.cs b/Terminal.Gui/Application/Application.Run.cs index 06c191230..e5e51a0fb 100644 --- a/Terminal.Gui/Application/Application.Run.cs +++ b/Terminal.Gui/Application/Application.Run.cs @@ -577,6 +577,7 @@ public static partial class Application // Run (Begin, Run, End, Stop) Iteration?.Invoke (null, new ()); EnsureModalOrVisibleAlwaysOnTop (state.Toplevel); + // TODO: Overlapped - Move elsewhere if (state.Toplevel != Current) { OverlappedTop?.OnDeactivate (state.Toplevel); diff --git a/Terminal.Gui/Application/Application.Toplevel.cs b/Terminal.Gui/Application/Application.Toplevel.cs index 6d25f7d02..b08f9e960 100644 --- a/Terminal.Gui/Application/Application.Toplevel.cs +++ b/Terminal.Gui/Application/Application.Toplevel.cs @@ -18,7 +18,7 @@ public static partial class Application // Toplevel handling /// . /// /// - /// Only relevant in scenarios where is . + /// This will only be distinct from in scenarios where is . /// /// The current. public static Toplevel Current { get; private set; } From f8e8aff29f46a1e9cbd15a7137313d46f67abb8e Mon Sep 17 00:00:00 2001 From: Tig Date: Tue, 23 Jul 2024 15:36:36 -0600 Subject: [PATCH 11/33] More Toplevel.cs organization & docs --- .../Application/Application.Initialization.cs | 3 + .../Application/Application.Keyboard.cs | 46 +++- Terminal.Gui/Application/Application.Mouse.cs | 4 +- .../Application/Application.Navigation.cs | 211 ++++++++++++++++++ .../Application/Application.Overlapped.cs | 7 + Terminal.Gui/Application/Application.Run.cs | 1 + .../Application/Application.Toplevel.cs | 22 +- Terminal.Gui/View/ViewSubViews.cs | 32 ++- Terminal.Gui/Views/Toplevel.cs | 27 ++- Terminal.Gui/Views/ToplevelOverlapped.cs | 209 ----------------- 10 files changed, 319 insertions(+), 243 deletions(-) create mode 100644 Terminal.Gui/Application/Application.Navigation.cs create mode 100644 Terminal.Gui/Application/Application.Overlapped.cs diff --git a/Terminal.Gui/Application/Application.Initialization.cs b/Terminal.Gui/Application/Application.Initialization.cs index 16b7b83d4..0434eb873 100644 --- a/Terminal.Gui/Application/Application.Initialization.cs +++ b/Terminal.Gui/Application/Application.Initialization.cs @@ -1,3 +1,4 @@ +#nullable enable using System.Diagnostics.CodeAnalysis; using System.Reflection; @@ -88,6 +89,8 @@ public static partial class Application // Initialization (Init/Shutdown) Load (); Apply (); + AddToplevelKeyBindings (); + // Ignore Configuration for ForceDriver if driverName is specified if (!string.IsNullOrEmpty (driverName)) { diff --git a/Terminal.Gui/Application/Application.Keyboard.cs b/Terminal.Gui/Application/Application.Keyboard.cs index 009d8c425..a299a7aee 100644 --- a/Terminal.Gui/Application/Application.Keyboard.cs +++ b/Terminal.Gui/Application/Application.Keyboard.cs @@ -1,4 +1,5 @@ -using System.Text.Json.Serialization; +#nullable enable +using System.Text.Json.Serialization; namespace Terminal.Gui; @@ -140,7 +141,7 @@ public static partial class Application // Keyboard handling // Invoke any global (Application-scoped) KeyBindings. // The first view that handles the key will stop the loop. - foreach (KeyValuePair> binding in _keyBindings.Where (b => b.Key == keyEvent.KeyCode)) + foreach (KeyValuePair> binding in _keyBindings.Where (b => b.Key == keyEvent.KeyCode)) { foreach (View view in binding.Value) { @@ -154,7 +155,6 @@ public static partial class Application // Keyboard handling { return true; } - } } } @@ -216,12 +216,12 @@ public static partial class Application // Keyboard handling /// /// The key bindings. /// - private static readonly Dictionary> _keyBindings = new (); + private static readonly Dictionary> _keyBindings = new (); /// /// Gets the list of key bindings. /// - public static Dictionary> GetKeyBindings () { return _keyBindings; } + public static Dictionary> GetKeyBindings () { return _keyBindings; } /// /// Adds an scoped key binding. @@ -231,13 +231,8 @@ public static partial class Application // Keyboard handling /// /// The key being bound. /// The view that is bound to the key. If , will be used. - internal static void AddKeyBinding (Key key, [CanBeNull] View view) + internal static void AddKeyBinding (Key key, View? view) { - if (view is null) - { - view = Current; - } - if (!_keyBindings.ContainsKey (key)) { _keyBindings [key] = []; @@ -246,6 +241,35 @@ public static partial class Application // Keyboard handling _keyBindings [key].Add (view); } + internal static void AddToplevelKeyBindings () + { +// // Default keybindings for this view +// AddKeyBinding (Application.QuitKey, null); +// AddKeyBinding (Key.CursorRight, null); +// AddKeyBinding (Key.CursorDown, null); +// AddKeyBinding (Key.CursorLeft, null); +// AddKeyBinding (Key.CursorUp, null); +// AddKeyBinding (Key.Tab, null); +// AddKeyBinding (Key.Tab.WithShift, null); +// AddKeyBinding (Key.Tab.WithCtrl, null); +// AddKeyBinding (Key.Tab.WithShift.WithCtrl, null); +// AddKeyBinding (Key.F5, null); +// AddKeyBinding (Application.AlternateForwardKey, null); // Needed on Unix +// AddKeyBinding (Application.AlternateBackwardKey, null); // Needed on Unix + +// if (Environment.OSVersion.Platform == PlatformID.Unix) +// { +// AddKeyBinding (Key.Z.WithCtrl, null); +// } + +//#if UNIX_KEY_BINDINGS +// KeyBindings.Add (Key.L.WithCtrl, Command.Refresh); // Unix +// KeyBindings.Add (Key.F.WithCtrl, Command.NextView); // Unix +// KeyBindings.Add (Key.I.WithCtrl, Command.NextView); // Unix +// KeyBindings.Add (Key.B.WithCtrl, Command.PreviousView); // Unix +//#endif + } + /// /// Gets the list of Views that have key bindings. /// diff --git a/Terminal.Gui/Application/Application.Mouse.cs b/Terminal.Gui/Application/Application.Mouse.cs index 7cad055d5..61fc6d63e 100644 --- a/Terminal.Gui/Application/Application.Mouse.cs +++ b/Terminal.Gui/Application/Application.Mouse.cs @@ -1,5 +1,5 @@ -namespace Terminal.Gui; - +#nullable enable +namespace Terminal.Gui; public static partial class Application // Mouse handling { #region Mouse handling diff --git a/Terminal.Gui/Application/Application.Navigation.cs b/Terminal.Gui/Application/Application.Navigation.cs new file mode 100644 index 000000000..0bbce67e6 --- /dev/null +++ b/Terminal.Gui/Application/Application.Navigation.cs @@ -0,0 +1,211 @@ +namespace Terminal.Gui; + +public static partial class Application +{ + /// + /// Gets the list of the Overlapped children which are not modal from the + /// . + /// + public static List OverlappedChildren + { + get + { + if (OverlappedTop is { }) + { + List _overlappedChildren = new (); + + foreach (Toplevel top in _topLevels) + { + if (top != OverlappedTop && !top.Modal) + { + _overlappedChildren.Add (top); + } + } + + return _overlappedChildren; + } + + return null; + } + } + +#nullable enable + /// + /// The object used for the application on startup which + /// is true. + /// + public static Toplevel? OverlappedTop + { + get + { + if (Top is { IsOverlappedContainer: true }) + { + return Top; + } + + return null; + } + } +#nullable restore + + /// Brings the superview of the most focused overlapped view is on front. + public static void BringOverlappedTopToFront () + { + if (OverlappedTop is { }) + { + return; + } + + View top = FindTopFromView (Top?.MostFocused); + + if (top is Toplevel && Top.Subviews.Count > 1 && Top.Subviews [^1] != top) + { + Top.BringSubviewToFront (top); + } + } + + /// Gets the current visible Toplevel overlapped child that matches the arguments pattern. + /// The type. + /// The strings to exclude. + /// The matched view. + public static Toplevel GetTopOverlappedChild (Type type = null, string [] exclude = null) + { + if (OverlappedTop is null) + { + return null; + } + + foreach (Toplevel top in OverlappedChildren) + { + if (type is { } && top.GetType () == type && exclude?.Contains (top.Data.ToString ()) == false) + { + return top; + } + + if ((type is { } && top.GetType () != type) || exclude?.Contains (top.Data.ToString ()) == true) + { + continue; + } + + return top; + } + + return null; + } + + /// + /// Move to the next Overlapped child from the and set it as the if + /// it is not already. + /// + /// + /// + public static bool MoveToOverlappedChild (Toplevel top) + { + if (top.Visible && OverlappedTop is { } && Current?.Modal == false) + { + lock (_topLevels) + { + _topLevels.MoveTo (top, 0, new ToplevelEqualityComparer ()); + Current = top; + } + + return true; + } + + return false; + } + + /// Move to the next Overlapped child from the . + public static void OverlappedMoveNext () + { + if (OverlappedTop is { } && !Current.Modal) + { + lock (_topLevels) + { + _topLevels.MoveNext (); + var isOverlapped = false; + + while (_topLevels.Peek () == OverlappedTop || !_topLevels.Peek ().Visible) + { + if (!isOverlapped && _topLevels.Peek () == OverlappedTop) + { + isOverlapped = true; + } + else if (isOverlapped && _topLevels.Peek () == OverlappedTop) + { + MoveCurrent (Top); + + break; + } + + _topLevels.MoveNext (); + } + + Current = _topLevels.Peek (); + } + } + } + + /// Move to the previous Overlapped child from the . + public static void OverlappedMovePrevious () + { + if (OverlappedTop is { } && !Current.Modal) + { + lock (_topLevels) + { + _topLevels.MovePrevious (); + var isOverlapped = false; + + while (_topLevels.Peek () == OverlappedTop || !_topLevels.Peek ().Visible) + { + if (!isOverlapped && _topLevels.Peek () == OverlappedTop) + { + isOverlapped = true; + } + else if (isOverlapped && _topLevels.Peek () == OverlappedTop) + { + MoveCurrent (Top); + + break; + } + + _topLevels.MovePrevious (); + } + + Current = _topLevels.Peek (); + } + } + } + + private static bool OverlappedChildNeedsDisplay () + { + if (OverlappedTop is null) + { + return false; + } + + foreach (Toplevel top in _topLevels) + { + if (top != Current && top.Visible && (top.NeedsDisplay || top.SubViewNeedsDisplay || top.LayoutNeeded)) + { + OverlappedTop.SetSubViewNeedsDisplay (); + + return true; + } + } + + return false; + } + + private static bool SetCurrentOverlappedAsTop () + { + if (OverlappedTop is null && Current != Top && Current?.SuperView is null && Current?.Modal == false) + { + Top = Current; + + return true; + } + + return false; + } +} \ No newline at end of file diff --git a/Terminal.Gui/Application/Application.Overlapped.cs b/Terminal.Gui/Application/Application.Overlapped.cs new file mode 100644 index 000000000..58eccfa3a --- /dev/null +++ b/Terminal.Gui/Application/Application.Overlapped.cs @@ -0,0 +1,7 @@ +#nullable enable +namespace Terminal.Gui; + +public static partial class Application // App-level View Navigation +{ + +} \ No newline at end of file diff --git a/Terminal.Gui/Application/Application.Run.cs b/Terminal.Gui/Application/Application.Run.cs index e5e51a0fb..541ad7141 100644 --- a/Terminal.Gui/Application/Application.Run.cs +++ b/Terminal.Gui/Application/Application.Run.cs @@ -1,3 +1,4 @@ +#nullable enable using System.Diagnostics; using System.Diagnostics.CodeAnalysis; diff --git a/Terminal.Gui/Application/Application.Toplevel.cs b/Terminal.Gui/Application/Application.Toplevel.cs index b08f9e960..d8996a383 100644 --- a/Terminal.Gui/Application/Application.Toplevel.cs +++ b/Terminal.Gui/Application/Application.Toplevel.cs @@ -1,10 +1,10 @@ +#nullable enable namespace Terminal.Gui; public static partial class Application // Toplevel handling { - /// Holds the stack of TopLevel views. - // BUGBUG: Technically, this is not the full lst of TopLevels. There be dragons here, e.g. see how Toplevel.Id is used. What + /// Holds the stack of TopLevel views. // about TopLevels that are just a SubView of another View? internal static readonly Stack _topLevels = new (); @@ -12,6 +12,7 @@ public static partial class Application // Toplevel handling /// The top. public static Toplevel Top { get; private set; } + // TODO: Determine why this can't just return _topLevels.Peek()? /// /// The current object. This is updated in enters and leaves to /// point to the current @@ -23,6 +24,9 @@ public static partial class Application // Toplevel handling /// The current. public static Toplevel Current { get; private set; } + /// + /// If is not already Current and visible, finds the last Modal Toplevel in the stack and makes it Current. + /// private static void EnsureModalOrVisibleAlwaysOnTop (Toplevel topLevel) { if (!topLevel.Running @@ -49,6 +53,12 @@ public static partial class Application // Toplevel handling } } + /// + /// Finds the first Toplevel in the stack that is Visible and who's Frame contains the . + /// + /// + /// + /// private static Toplevel FindDeepestTop (Toplevel start, in Point location) { if (!start.Frame.Contains (location)) @@ -78,6 +88,9 @@ public static partial class Application // Toplevel handling return start; } + /// + /// Given , returns the first Superview up the chain that is . + /// private static View FindTopFromView (View view) { View top = view?.SuperView is { } && view?.SuperView != Top @@ -92,6 +105,11 @@ public static partial class Application // Toplevel handling return top; } + /// + /// If the is not the then is moved to the top of the Toplevel stack and made Current. + /// + /// + /// private static bool MoveCurrent (Toplevel top) { // The Current is modal and the top is not modal Toplevel then diff --git a/Terminal.Gui/View/ViewSubViews.cs b/Terminal.Gui/View/ViewSubViews.cs index 99f9e119b..38863828a 100644 --- a/Terminal.Gui/View/ViewSubViews.cs +++ b/Terminal.Gui/View/ViewSubViews.cs @@ -627,7 +627,7 @@ public partial class View } } - /// Causes the specified view and the entire parent hierarchy to have the focused order updated. + /// Causes this view to be focused and entire Superview hierarchy to have the focused order updated. public void SetFocus () { if (!CanBeVisible (this) || !Enabled) @@ -651,7 +651,7 @@ public partial class View } /// - /// Finds the first view in the hierarchy that wants to get the focus if nothing is currently focused, otherwise, + /// If there is no focused subview, calls or based on . /// does nothing. /// public void EnsureFocus () @@ -669,7 +669,9 @@ public partial class View } } - /// Focuses the first focusable subview if one exists. + /// + /// Focuses the last focusable view in if one exists. If there are no views in then the focus is set to the view itself. + /// public void FocusFirst () { if (!CanBeVisible (this)) @@ -695,7 +697,9 @@ public partial class View } } - /// Focuses the last focusable subview if one exists. + /// + /// Focuses the last focusable view in if one exists. If there are no views in then the focus is set to the view itself. + /// public void FocusLast () { if (!CanBeVisible (this)) @@ -725,7 +729,9 @@ public partial class View } } - /// Focuses the previous view. + /// + /// Focuses the previous view in . If there is no previous view, the focus is set to the view itself. + /// /// if previous was focused, otherwise. public bool FocusPrev () { @@ -736,7 +742,7 @@ public partial class View FocusDirection = NavigationDirection.Backward; - if (_tabIndexes is null || _tabIndexes.Count == 0) + if (TabIndexes is null || TabIndexes.Count == 0) { return false; } @@ -750,10 +756,10 @@ public partial class View int focusedIdx = -1; - for (int i = _tabIndexes.Count; i > 0;) + for (int i = TabIndexes.Count; i > 0;) { i--; - View w = _tabIndexes [i]; + View w = TabIndexes [i]; if (w.HasFocus) { @@ -791,7 +797,9 @@ public partial class View return false; } - /// Focuses the next view. + /// + /// Focuses the previous view in . If there is no previous view, the focus is set to the view itself. + /// /// if next was focused, otherwise. public bool FocusNext () { @@ -802,7 +810,7 @@ public partial class View FocusDirection = NavigationDirection.Forward; - if (_tabIndexes is null || _tabIndexes.Count == 0) + if (TabIndexes is null || TabIndexes.Count == 0) { return false; } @@ -816,9 +824,9 @@ public partial class View int focusedIdx = -1; - for (var i = 0; i < _tabIndexes.Count; i++) + for (var i = 0; i < TabIndexes.Count; i++) { - View w = _tabIndexes [i]; + View w = TabIndexes [i]; if (w.HasFocus) { diff --git a/Terminal.Gui/Views/Toplevel.cs b/Terminal.Gui/Views/Toplevel.cs index e14d02509..0e7090492 100644 --- a/Terminal.Gui/Views/Toplevel.cs +++ b/Terminal.Gui/Views/Toplevel.cs @@ -80,7 +80,7 @@ public partial class Toplevel : View ); /// TODO: Overlapped: Add Command.ShowHide - + AddCommand ( Command.Suspend, // TODO: Move to Application () => @@ -566,7 +566,7 @@ public partial class Toplevel : View #endregion - #region Focus + #region Navigation /// public override bool OnEnter (View view) { return MostFocused?.OnEnter (view) ?? base.OnEnter (view); } @@ -574,9 +574,14 @@ public partial class Toplevel : View /// public override bool OnLeave (View view) { return MostFocused?.OnLeave (view) ?? base.OnLeave (view); } - private void FocusNearestView (IEnumerable views, NavigationDirection direction) + /// + /// Sets the focus to the next view in the list. If the last view is focused, the first view is focused. + /// + /// + /// + private void FocusNearestView (IEnumerable viewsInTabIndexes, NavigationDirection direction) { - if (views is null) + if (viewsInTabIndexes is null) { return; } @@ -585,7 +590,7 @@ public partial class Toplevel : View var focusProcessed = false; var idx = 0; - foreach (View v in views) + foreach (View v in viewsInTabIndexes) { if (v == this) { @@ -610,15 +615,20 @@ public partial class Toplevel : View return; } } - else if (found && !focusProcessed && idx == views.Count () - 1) + else if (found && !focusProcessed && idx == viewsInTabIndexes.Count () - 1) { - views.ToList () [0].SetFocus (); + viewsInTabIndexes.ToList () [0].SetFocus (); } idx++; } } + /// + /// Gets the deepest focused subview of the specified . + /// + /// + /// private View GetDeepestFocusedSubview (View view) { if (view is null) @@ -637,6 +647,9 @@ public partial class Toplevel : View return view; } + /// + /// Moves the focus to + /// private void MoveNextView () { View old = GetDeepestFocusedSubview (Focused); diff --git a/Terminal.Gui/Views/ToplevelOverlapped.cs b/Terminal.Gui/Views/ToplevelOverlapped.cs index 0d93ef79f..3541ea091 100644 --- a/Terminal.Gui/Views/ToplevelOverlapped.cs +++ b/Terminal.Gui/Views/ToplevelOverlapped.cs @@ -9,212 +9,3 @@ public partial class Toplevel public bool IsOverlappedContainer { get; set; } } -public static partial class Application -{ - /// - /// Gets the list of the Overlapped children which are not modal from the - /// . - /// - public static List OverlappedChildren - { - get - { - if (OverlappedTop is { }) - { - List _overlappedChildren = new (); - - foreach (Toplevel top in _topLevels) - { - if (top != OverlappedTop && !top.Modal) - { - _overlappedChildren.Add (top); - } - } - - return _overlappedChildren; - } - - return null; - } - } - - #nullable enable - /// - /// The object used for the application on startup which - /// is true. - /// - public static Toplevel? OverlappedTop - { - get - { - if (Top is { IsOverlappedContainer: true }) - { - return Top; - } - - return null; - } - } - #nullable restore - - /// Brings the superview of the most focused overlapped view is on front. - public static void BringOverlappedTopToFront () - { - if (OverlappedTop is { }) - { - return; - } - - View top = FindTopFromView (Top?.MostFocused); - - if (top is Toplevel && Top.Subviews.Count > 1 && Top.Subviews [^1] != top) - { - Top.BringSubviewToFront (top); - } - } - - /// Gets the current visible Toplevel overlapped child that matches the arguments pattern. - /// The type. - /// The strings to exclude. - /// The matched view. - public static Toplevel GetTopOverlappedChild (Type type = null, string [] exclude = null) - { - if (OverlappedTop is null) - { - return null; - } - - foreach (Toplevel top in OverlappedChildren) - { - if (type is { } && top.GetType () == type && exclude?.Contains (top.Data.ToString ()) == false) - { - return top; - } - - if ((type is { } && top.GetType () != type) || exclude?.Contains (top.Data.ToString ()) == true) - { - continue; - } - - return top; - } - - return null; - } - - /// - /// Move to the next Overlapped child from the and set it as the if - /// it is not already. - /// - /// - /// - public static bool MoveToOverlappedChild (Toplevel top) - { - if (top.Visible && OverlappedTop is { } && Current?.Modal == false) - { - lock (_topLevels) - { - _topLevels.MoveTo (top, 0, new ToplevelEqualityComparer ()); - Current = top; - } - - return true; - } - - return false; - } - - /// Move to the next Overlapped child from the . - public static void OverlappedMoveNext () - { - if (OverlappedTop is { } && !Current.Modal) - { - lock (_topLevels) - { - _topLevels.MoveNext (); - var isOverlapped = false; - - while (_topLevels.Peek () == OverlappedTop || !_topLevels.Peek ().Visible) - { - if (!isOverlapped && _topLevels.Peek () == OverlappedTop) - { - isOverlapped = true; - } - else if (isOverlapped && _topLevels.Peek () == OverlappedTop) - { - MoveCurrent (Top); - - break; - } - - _topLevels.MoveNext (); - } - - Current = _topLevels.Peek (); - } - } - } - - /// Move to the previous Overlapped child from the . - public static void OverlappedMovePrevious () - { - if (OverlappedTop is { } && !Current.Modal) - { - lock (_topLevels) - { - _topLevels.MovePrevious (); - var isOverlapped = false; - - while (_topLevels.Peek () == OverlappedTop || !_topLevels.Peek ().Visible) - { - if (!isOverlapped && _topLevels.Peek () == OverlappedTop) - { - isOverlapped = true; - } - else if (isOverlapped && _topLevels.Peek () == OverlappedTop) - { - MoveCurrent (Top); - - break; - } - - _topLevels.MovePrevious (); - } - - Current = _topLevels.Peek (); - } - } - } - - private static bool OverlappedChildNeedsDisplay () - { - if (OverlappedTop is null) - { - return false; - } - - foreach (Toplevel top in _topLevels) - { - if (top != Current && top.Visible && (top.NeedsDisplay || top.SubViewNeedsDisplay || top.LayoutNeeded)) - { - OverlappedTop.SetSubViewNeedsDisplay (); - - return true; - } - } - - return false; - } - - private static bool SetCurrentOverlappedAsTop () - { - if (OverlappedTop is null && Current != Top && Current?.SuperView is null && Current?.Modal == false) - { - Top = Current; - - return true; - } - - return false; - } -} From feaf5c0f6c34f0767d660fa5916580b8835e7569 Mon Sep 17 00:00:00 2001 From: Tig Date: Tue, 23 Jul 2024 18:12:43 -0600 Subject: [PATCH 12/33] WIP (Very Broken) try to move keybindings out of Toplevel to app --- .../Application/Application.Initialization.cs | 2 +- .../Application/Application.Keyboard.cs | 203 ++++++++++++++---- Terminal.Gui/Input/KeyBindings.cs | 19 +- Terminal.Gui/View/ViewKeyboard.cs | 2 +- 4 files changed, 177 insertions(+), 49 deletions(-) diff --git a/Terminal.Gui/Application/Application.Initialization.cs b/Terminal.Gui/Application/Application.Initialization.cs index 0434eb873..2e184b285 100644 --- a/Terminal.Gui/Application/Application.Initialization.cs +++ b/Terminal.Gui/Application/Application.Initialization.cs @@ -89,7 +89,7 @@ public static partial class Application // Initialization (Init/Shutdown) Load (); Apply (); - AddToplevelKeyBindings (); + AddApplicationKeyBindings (); // Ignore Configuration for ForceDriver if driverName is specified if (!string.IsNullOrEmpty (driverName)) diff --git a/Terminal.Gui/Application/Application.Keyboard.cs b/Terminal.Gui/Application/Application.Keyboard.cs index a299a7aee..209520f47 100644 --- a/Terminal.Gui/Application/Application.Keyboard.cs +++ b/Terminal.Gui/Application/Application.Keyboard.cs @@ -213,61 +213,172 @@ public static partial class Application // Keyboard handling return false; } - /// - /// The key bindings. - /// - private static readonly Dictionary> _keyBindings = new (); + /// Gets the key bindings for this view. + public static KeyBindings KeyBindings { get; internal set; } = new (); /// - /// Gets the list of key bindings. + /// Commands for Application. /// - public static Dictionary> GetKeyBindings () { return _keyBindings; } + private static Dictionary> CommandImplementations { get; } = new (); /// - /// Adds an scoped key binding. + /// + /// Sets the function that will be invoked for a . + /// + /// + /// If AddCommand has already been called for will + /// replace the old one. + /// /// /// - /// This is an internal method used by the class to add Application key bindings. + /// + /// This version of AddCommand is for commands that do not require a . + /// /// - /// The key being bound. - /// The view that is bound to the key. If , will be used. - internal static void AddKeyBinding (Key key, View? view) + /// The command. + /// The function. + private static void AddCommand (Command command, Func f) { - if (!_keyBindings.ContainsKey (key)) - { - _keyBindings [key] = []; - } - - _keyBindings [key].Add (view); + CommandImplementations [command] = ctx => f (); } - internal static void AddToplevelKeyBindings () + ///// + ///// The key bindings. + ///// + //private static readonly Dictionary> _keyBindings = new (); + + ///// + ///// Gets the list of key bindings. + ///// + //public static Dictionary> GetKeyBindings () { return _keyBindings; } + + ///// + ///// Adds an scoped key binding. + ///// + ///// + ///// This is an internal method used by the class to add Application key bindings. + ///// + ///// The key being bound. + ///// The view that is bound to the key. If , will be used. + //internal static void AddKeyBinding (Key key, View? view) + //{ + // if (!_keyBindings.ContainsKey (key)) + // { + // _keyBindings [key] = []; + // } + + // _keyBindings [key].Add (view); + //} + + internal static void AddApplicationKeyBindings () { -// // Default keybindings for this view -// AddKeyBinding (Application.QuitKey, null); -// AddKeyBinding (Key.CursorRight, null); -// AddKeyBinding (Key.CursorDown, null); -// AddKeyBinding (Key.CursorLeft, null); -// AddKeyBinding (Key.CursorUp, null); -// AddKeyBinding (Key.Tab, null); -// AddKeyBinding (Key.Tab.WithShift, null); -// AddKeyBinding (Key.Tab.WithCtrl, null); -// AddKeyBinding (Key.Tab.WithShift.WithCtrl, null); -// AddKeyBinding (Key.F5, null); -// AddKeyBinding (Application.AlternateForwardKey, null); // Needed on Unix -// AddKeyBinding (Application.AlternateBackwardKey, null); // Needed on Unix + // Things this view knows how to do + AddCommand ( + Command.QuitToplevel, // TODO: IRunnable: Rename to Command.Quit to make more generic. + () => + { + if (OverlappedTop is { }) + { + RequestStop (Current); + } + else + { + Application.RequestStop (); + } -// if (Environment.OSVersion.Platform == PlatformID.Unix) -// { -// AddKeyBinding (Key.Z.WithCtrl, null); -// } + return true; + } + ); -//#if UNIX_KEY_BINDINGS -// KeyBindings.Add (Key.L.WithCtrl, Command.Refresh); // Unix -// KeyBindings.Add (Key.F.WithCtrl, Command.NextView); // Unix -// KeyBindings.Add (Key.I.WithCtrl, Command.NextView); // Unix -// KeyBindings.Add (Key.B.WithCtrl, Command.PreviousView); // Unix -//#endif + AddCommand ( + Command.Suspend, + () => + { + Driver?.Suspend (); + + return true; + } + ); + + AddCommand ( + Command.NextView, // TODO: Figure out how to move this to the View that is at the root of the view hierarchy (currently Application.Top) + () => + { + Current.MoveNextView (); + + return true; + } + ); + + AddCommand ( + Command.PreviousView,// TODO: Figure out how to move this to the View that is at the root of the view hierarchy (currently Application.Top) + () => + { + Current.MovePreviousView (); + + return true; + } + ); + + AddCommand ( + Command.NextViewOrTop,// TODO: Figure out how to move this to the View that is at the root of the view hierarchy (currently Application.Top) + () => + { + Current.MoveNextViewOrTop (); + + return true; + } + ); + + AddCommand ( + Command.PreviousViewOrTop,// TODO: Figure out how to move this to the View that is at the root of the view hierarchy (currently Application.Top) + () => + { + Current.MovePreviousViewOrTop (); + + return true; + } + ); + + AddCommand ( + Command.Refresh, + () => + { + Refresh (); + + return true; + } + ); + + + KeyBindings.Add (Application.QuitKey, KeyBindingScope.Application, Command.QuitToplevel); + + KeyBindings.Add (Key.CursorRight, KeyBindingScope.Application, Command.NextView); + KeyBindings.Add (Key.CursorDown, KeyBindingScope.Application, Command.NextView); + KeyBindings.Add (Key.CursorLeft, KeyBindingScope.Application, Command.PreviousView); + KeyBindings.Add (Key.CursorUp, KeyBindingScope.Application, Command.PreviousView); + + KeyBindings.Add (Key.Tab, KeyBindingScope.Application, Command.NextView); + KeyBindings.Add (Key.Tab.WithShift, KeyBindingScope.Application, Command.PreviousView); + KeyBindings.Add (Key.Tab.WithCtrl, KeyBindingScope.Application, Command.NextViewOrTop); + KeyBindings.Add (Key.Tab.WithShift.WithCtrl, KeyBindingScope.Application, Command.PreviousViewOrTop); + + // TODO: Refresh Key should be configurable + KeyBindings.Add (Key.F5, KeyBindingScope.Application, Command.Refresh); + KeyBindings.Add (Application.AlternateForwardKey, KeyBindingScope.Application, Command.NextViewOrTop); // Needed on Unix + KeyBindings.Add (Application.AlternateBackwardKey, KeyBindingScope.Application, Command.PreviousViewOrTop); // Needed on Unix + + if (Environment.OSVersion.Platform == PlatformID.Unix) + { + KeyBindings.Add (Key.Z.WithCtrl, Command.Suspend); + } + +#if UNIX_KEY_BINDINGS + KeyBindings.Add (Key.L.WithCtrl, Command.Refresh); // Unix + KeyBindings.Add (Key.F.WithCtrl, Command.NextView); // Unix + KeyBindings.Add (Key.I.WithCtrl, Command.NextView); // Unix + KeyBindings.Add (Key.B.WithCtrl, Command.PreviousView); // Unix +#endif } /// @@ -277,7 +388,15 @@ public static partial class Application // Keyboard handling /// This is an internal method used by the class to add Application key bindings. /// /// The list of Views that have Application-scoped key bindings. - internal static List GetViewsWithKeyBindings () { return _keyBindings.Values.SelectMany (v => v).ToList (); } + internal static List GetViewKeyBindings () + { + // Get the list of views that do not have Application-scoped key bindings + return KeyBindings.Bindings + .Where (kv => kv.Value.Scope != KeyBindingScope.Application) + .Select (kv => kv.Value) + .Distinct () + .ToList (); + } /// /// Gets the list of Views that have key bindings for the specified key. diff --git a/Terminal.Gui/Input/KeyBindings.cs b/Terminal.Gui/Input/KeyBindings.cs index a55169606..0ea164a40 100644 --- a/Terminal.Gui/Input/KeyBindings.cs +++ b/Terminal.Gui/Input/KeyBindings.cs @@ -11,7 +11,7 @@ public class KeyBindings { /// /// Initializes a new instance. This constructor is used when the are not bound to a - /// , such as in unit tests. + /// . This is used for Application.KeyBindings and unit tests. /// public KeyBindings () { } @@ -21,6 +21,9 @@ public class KeyBindings /// /// 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 @@ -33,6 +36,11 @@ public class KeyBindings /// public void Add (Key key, KeyBinding binding) { + if (BoundView is { } && binding.Scope.FastHasFlags (KeyBindingScope.Application)) + { + throw new ArgumentException ("Application scoped KeyBindings must be added via Application.KeyBindings.Add"); + } + if (TryGet (key, out KeyBinding _)) { Bindings [key] = binding; @@ -40,10 +48,6 @@ public class KeyBindings else { Bindings.Add (key, binding); - if (binding.Scope.HasFlag (KeyBindingScope.Application)) - { - Application.AddKeyBinding (key, BoundView); - } } } @@ -67,6 +71,11 @@ public class KeyBindings /// public void Add (Key key, KeyBindingScope scope, params Command [] commands) { + if (BoundView is { } && scope.FastHasFlags (KeyBindingScope.Application)) + { + throw new ArgumentException ("Application scoped KeyBindings must be added via Application.KeyBindings.Add"); + } + if (key is null || !key.IsValid) { //throw new ArgumentException ("Invalid Key", nameof (commands)); diff --git a/Terminal.Gui/View/ViewKeyboard.cs b/Terminal.Gui/View/ViewKeyboard.cs index d103d894d..36ec6e661 100644 --- a/Terminal.Gui/View/ViewKeyboard.cs +++ b/Terminal.Gui/View/ViewKeyboard.cs @@ -641,7 +641,7 @@ public partial class View /// public virtual bool? OnInvokingKeyBindings (Key keyEvent, KeyBindingScope scope) { - // fire event only if there's an hotkey binding for the key + // fire event only if there's a hotkey binding for the key if (KeyBindings.TryGet (keyEvent, scope, out KeyBinding kb)) { InvokingKeyBindings?.Invoke (this, keyEvent); From c03dd320318f3a2d287dfed973c6df013871ce99 Mon Sep 17 00:00:00 2001 From: Tig Date: Wed, 24 Jul 2024 12:28:30 -0600 Subject: [PATCH 13/33] Moved Toplevel keybindings out of Toplevel to Application. Still need to move navigation code out of Toplevel --- .../Application/Application.Keyboard.cs | 145 ++-- Terminal.Gui/Application/Application.cs | 2 +- Terminal.Gui/Input/KeyBinding.cs | 16 + Terminal.Gui/Input/KeyBindings.cs | 138 +++- Terminal.Gui/View/ViewKeyboard.cs | 15 +- Terminal.Gui/Views/DateField.cs | 26 +- Terminal.Gui/Views/FileDialog.cs | 10 +- Terminal.Gui/Views/Menu/Menu.cs | 3 + Terminal.Gui/Views/Menu/MenuBarItem.cs | 1 + Terminal.Gui/Views/MenuBarv2.cs | 2 - Terminal.Gui/Views/Shortcut.cs | 644 +++++++++--------- Terminal.Gui/Views/Slider.cs | 4 + Terminal.Gui/Views/StatusBar.cs | 2 - .../TableView/CheckBoxTableSourceWrapper.cs | 2 +- Terminal.Gui/Views/TableView/TableView.cs | 2 +- Terminal.Gui/Views/TextField.cs | 5 +- Terminal.Gui/Views/TextValidateField.cs | 1 - Terminal.Gui/Views/TextView.cs | 18 +- Terminal.Gui/Views/TimeField.cs | 26 +- Terminal.Gui/Views/Toplevel.cs | 16 +- Terminal.Gui/Views/TreeView/TreeView.cs | 2 +- UICatalog/Scenarios/KeyBindings.cs | 37 +- UICatalog/Scenarios/ListColumns.cs | 2 +- UICatalog/Scenarios/TableEditor.cs | 2 +- UICatalog/UICatalog.cs | 2 + UnitTests/Application/ApplicationTests.cs | 4 +- UnitTests/Application/KeyboardTests.cs | 154 ++--- UnitTests/Input/KeyBindingTests.cs | 64 +- UnitTests/View/MouseTests.cs | 188 +---- UnitTests/View/NavigationTests.cs | 223 +++++- UnitTests/View/ViewKeyBindingTests.cs | 5 +- UnitTests/Views/AllViewsTests.cs | 8 +- UnitTests/Views/OverlappedTests.cs | 94 ++- UnitTests/Views/TableViewTests.cs | 8 +- UnitTests/Views/ToplevelTests.cs | 32 +- UnitTests/Views/TreeTableSourceTests.cs | 2 +- 36 files changed, 1035 insertions(+), 870 deletions(-) diff --git a/Terminal.Gui/Application/Application.Keyboard.cs b/Terminal.Gui/Application/Application.Keyboard.cs index 209520f47..c725b13bd 100644 --- a/Terminal.Gui/Application/Application.Keyboard.cs +++ b/Terminal.Gui/Application/Application.Keyboard.cs @@ -1,5 +1,6 @@ #nullable enable using System.Text.Json.Serialization; +using static System.Formats.Asn1.AsnWriter; namespace Terminal.Gui; @@ -19,6 +20,15 @@ public static partial class Application // Keyboard handling { Key oldKey = _alternateForwardKey; _alternateForwardKey = value; + + if (_alternateForwardKey == Key.Empty) + { + KeyBindings.Remove (_alternateForwardKey); + } + else + { + KeyBindings.ReplaceKey (oldKey, _alternateForwardKey); + } OnAlternateForwardKeyChanged (new (oldKey, value)); } } @@ -47,6 +57,16 @@ public static partial class Application // Keyboard handling { Key oldKey = _alternateBackwardKey; _alternateBackwardKey = value; + + if (_alternateBackwardKey == Key.Empty) + { + KeyBindings.Remove (_alternateBackwardKey); + } + else + { + KeyBindings.ReplaceKey (oldKey, _alternateBackwardKey); + } + OnAlternateBackwardKeyChanged (new (oldKey, value)); } } @@ -75,6 +95,14 @@ public static partial class Application // Keyboard handling { Key oldKey = _quitKey; _quitKey = value; + if (_quitKey == Key.Empty) + { + KeyBindings.Remove (_quitKey); + } + else + { + KeyBindings.ReplaceKey (oldKey, _quitKey); + } OnQuitKeyChanged (new (oldKey, value)); } } @@ -139,26 +167,55 @@ public static partial class Application // Keyboard handling } } - // Invoke any global (Application-scoped) KeyBindings. + // Invoke any Application-scoped KeyBindings. // The first view that handles the key will stop the loop. - foreach (KeyValuePair> binding in _keyBindings.Where (b => b.Key == keyEvent.KeyCode)) + foreach (var binding in KeyBindings.Bindings.Where (b => b.Key == keyEvent.KeyCode)) { - foreach (View view in binding.Value) + if (binding.Value.BoundView is { }) { - if (view is { } - && view.KeyBindings.TryGet (binding.Key, KeyBindingScope.Focused | KeyBindingScope.HotKey | KeyBindingScope.Application, out KeyBinding kb)) - { - //bool? handled = view.InvokeCommands (kb.Commands, binding.Key, kb); - bool? handled = view?.OnInvokingKeyBindings (keyEvent, kb.Scope); + bool? handled = binding.Value.BoundView?.InvokeCommands (binding.Value.Commands, binding.Key, binding.Value); - if (handled != null && (bool)handled) - { - return true; - } + if (handled != null && (bool)handled) + { + return true; } } + else + { + if (!KeyBindings.TryGet (keyEvent, KeyBindingScope.Application, out KeyBinding appBinding)) + { + continue; + } + + bool? toReturn = null; + + foreach (Command command in appBinding.Commands) + { + if (!CommandImplementations.ContainsKey (command)) + { + throw new NotSupportedException ( + @$"A KeyBinding was set up for the command {command} ({keyEvent}) but that command is not supported by Application." + ); + } + + if (CommandImplementations.TryGetValue (command, out Func? implementation)) + { + var context = new CommandContext (command, keyEvent, appBinding); // Create the context here + toReturn = implementation (context); + } + + // if ever see a true then that's what we will return + if (toReturn ?? false) + { + toReturn = true; + } + } + + return toReturn ?? true; + } } + return false; } @@ -344,7 +401,7 @@ public static partial class Application // Keyboard handling Command.Refresh, () => { - Refresh (); + Refresh (); return true; } @@ -370,7 +427,7 @@ public static partial class Application // Keyboard handling if (Environment.OSVersion.Platform == PlatformID.Unix) { - KeyBindings.Add (Key.Z.WithCtrl, Command.Suspend); + KeyBindings.Add (Key.Z.WithCtrl, KeyBindingScope.Application, Command.Suspend); } #if UNIX_KEY_BINDINGS @@ -398,37 +455,16 @@ public static partial class Application // Keyboard handling .ToList (); } - /// - /// Gets the list of Views that have key bindings for the specified key. - /// - /// - /// This is an internal method used by the class to add Application key bindings. - /// - /// The key to check. - /// Outputs the list of views bound to - /// if successful. - internal static bool TryGetKeyBindings (Key key, out List views) { return _keyBindings.TryGetValue (key, out views); } - - /// - /// Removes an scoped key binding. - /// - /// - /// This is an internal method used by the class to remove Application key bindings. - /// - /// The key that was bound. - /// The view that is bound to the key. - internal static void RemoveKeyBinding (Key key, View view) - { - if (_keyBindings.TryGetValue (key, out List views)) - { - views.Remove (view); - - if (views.Count == 0) - { - _keyBindings.Remove (key); - } - } - } + ///// + ///// Gets the list of Views that have key bindings for the specified key. + ///// + ///// + ///// This is an internal method used by the class to add Application key bindings. + ///// + ///// The key to check. + ///// Outputs the list of views bound to + ///// if successful. + //internal static bool TryGetKeyBindings (Key key, out List views) { return _keyBindings.TryGetValue (key, out views); } /// /// Removes all scoped key bindings for the specified view. @@ -437,19 +473,12 @@ public static partial class Application // Keyboard handling /// This is an internal method used by the class to remove Application key bindings. /// /// The view that is bound to the key. - internal static void ClearKeyBindings (View view) + internal static void RemoveKeyBindings (View view) { - foreach (Key key in _keyBindings.Keys) - { - _keyBindings [key].Remove (view); - } + var list = KeyBindings.Bindings + .Where (kv => kv.Value.Scope != KeyBindingScope.Application) + .Select (kv => kv.Value) + .Distinct () + .ToList (); } - - /// - /// Removes all scoped key bindings for the specified view. - /// - /// - /// This is an internal method used by the class to remove Application key bindings. - /// - internal static void ClearKeyBindings () { _keyBindings.Clear (); } } diff --git a/Terminal.Gui/Application/Application.cs b/Terminal.Gui/Application/Application.cs index 3b4c9d9f7..76c05922b 100644 --- a/Terminal.Gui/Application/Application.cs +++ b/Terminal.Gui/Application/Application.cs @@ -126,7 +126,7 @@ public static partial class Application KeyDown = null; KeyUp = null; SizeChanging = null; - ClearKeyBindings (); + KeyBindings.Clear (); Colors.Reset (); diff --git a/Terminal.Gui/Input/KeyBinding.cs b/Terminal.Gui/Input/KeyBinding.cs index baac07384..8b5a4201d 100644 --- a/Terminal.Gui/Input/KeyBinding.cs +++ b/Terminal.Gui/Input/KeyBinding.cs @@ -21,12 +21,28 @@ public record struct KeyBinding Context = context; } + /// Initializes a new instance. + /// The commands this key binding will invoke. + /// The scope of the . + /// The view the key binding is bound to. + /// Arbitrary context that can be associated with this key binding. + public KeyBinding (Command [] commands, KeyBindingScope scope, View? boundView, object? context = null) + { + Commands = commands; + Scope = scope; + BoundView = boundView; + Context = context; + } + /// The commands this key binding will invoke. public Command [] Commands { get; set; } /// The scope of the . public KeyBindingScope Scope { get; set; } + /// The view the key binding is bound to. + public View? BoundView { get; set; } + /// /// Arbitrary context that can be associated with this key binding. /// diff --git a/Terminal.Gui/Input/KeyBindings.cs b/Terminal.Gui/Input/KeyBindings.cs index 0ea164a40..8ca45568e 100644 --- a/Terminal.Gui/Input/KeyBindings.cs +++ b/Terminal.Gui/Input/KeyBindings.cs @@ -1,6 +1,7 @@ #nullable enable using System.Diagnostics; +using Microsoft.CodeAnalysis; namespace Terminal.Gui; @@ -34,7 +35,8 @@ public class KeyBindings /// Adds a to the collection. /// /// - public void Add (Key key, KeyBinding binding) + /// Optional View for bindings. + public void Add (Key key, KeyBinding binding, View? boundViewForAppScope = null) { if (BoundView is { } && binding.Scope.FastHasFlags (KeyBindingScope.Application)) { @@ -43,10 +45,19 @@ public class KeyBindings if (TryGet (key, out KeyBinding _)) { - Bindings [key] = binding; + throw new InvalidOperationException(@$"A key binding for {key} exists ({binding})."); + //Bindings [key] = binding; } else { + if (BoundView is { }) + { + binding.BoundView = BoundView; + } + else + { + binding.BoundView = boundViewForAppScope; + } Bindings.Add (key, binding); } } @@ -64,12 +75,13 @@ public class KeyBindings /// /// The key to check. /// The scope for the command. + /// Optional View for bindings. /// /// The command to invoked on the when is pressed. When /// 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, View? boundViewForAppScope = null, params Command [] commands) { if (BoundView is { } && scope.FastHasFlags (KeyBindingScope.Application)) { @@ -87,13 +99,43 @@ public class KeyBindings throw new ArgumentException (@"At least one command must be specified", nameof (commands)); } - if (TryGet (key, out KeyBinding _)) + if (TryGet (key, out KeyBinding binding)) { - Bindings [key] = new (commands, scope); + throw new InvalidOperationException (@$"A key binding for {key} exists ({binding})."); + //Bindings [key] = new (commands, scope, BoundView); } else { - Add (key, new KeyBinding (commands, scope)); + Add (key, new KeyBinding (commands, scope, BoundView), boundViewForAppScope); + } + } + + public void Add (Key key, KeyBindingScope scope, params Command [] commands) + { + if (BoundView is { } && scope.FastHasFlags (KeyBindingScope.Application)) + { + throw new ArgumentException ("Application scoped KeyBindings must be added via Application.KeyBindings.Add"); + } + + if (key is null || !key.IsValid) + { + //throw new ArgumentException ("Invalid Key", nameof (commands)); + return; + } + + if (commands.Length == 0) + { + throw new ArgumentException (@"At least one command must be specified", nameof (commands)); + } + + 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), null); } } @@ -103,8 +145,42 @@ public class KeyBindings /// View - see ). /// /// - /// This is a helper function for for - /// scoped commands. + /// 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 . + /// + /// + /// If the key is already bound to a different array of s it will be rebound + /// . + /// + /// + /// + /// Commands are only ever applied to the current (i.e. this feature cannot be used to switch + /// focus to another view and perform multiple commands there). + /// + /// The key to check. + /// Optional View for bindings. + /// + /// The command to invoked on the when is pressed. When + /// 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, View? boundViewForAppScope = null, params Command [] commands) + { + if (BoundView is null && boundViewForAppScope is null) + { + 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); + } + + /// + /// + /// Adds a new key combination that will trigger the commands in (if supported by the + /// View - see ). + /// + /// + /// 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 . /// /// /// If the key is already bound to a different array of s it will be rebound @@ -123,14 +199,16 @@ public class KeyBindings /// public void Add (Key key, params Command [] commands) { - Add (key, KeyBindingScope.Focused, commands); + if (BoundView is null) + { + throw new ArgumentException (@"Application scoped KeyBindings must provide a boundViewForAppScope to Add."); + } + Add (key, BoundView is { } ? KeyBindingScope.Focused : KeyBindingScope.Application, null, commands); } /// Removes all objects from the collection. public void Clear () { - Application.ClearKeyBindings (BoundView); - Bindings.Clear (); } @@ -201,17 +279,23 @@ public class KeyBindings /// Removes a from the collection. /// - public void Remove (Key key) + /// Optional View for bindings. + public void Remove (Key key, View? boundViewForAppScope = null) { + + if (!TryGet (key, out KeyBinding binding)) + { + return; + } + Bindings.Remove (key); - Application.RemoveKeyBinding (key, BoundView); } /// Replaces a key combination already bound to a set of s. /// /// The key to be replaced. /// The new key to be used. - public void Replace (Key oldKey, Key newKey) + public void ReplaceKey (Key oldKey, Key newKey) { if (!TryGet (oldKey, out KeyBinding _)) { @@ -223,6 +307,26 @@ 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. @@ -233,13 +337,12 @@ public class KeyBindings /// if the Key is bound; otherwise . public bool TryGet (Key key, out KeyBinding binding) { + binding = new (Array.Empty (), KeyBindingScope.Disabled, null); if (key.IsValid) { return Bindings.TryGetValue (key, out binding); } - binding = new (Array.Empty (), KeyBindingScope.Focused); - return false; } @@ -254,6 +357,7 @@ public class KeyBindings /// if the Key is bound; otherwise . 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)) @@ -262,8 +366,6 @@ public class KeyBindings } } - binding = new (Array.Empty (), KeyBindingScope.Focused); - return false; } } diff --git a/Terminal.Gui/View/ViewKeyboard.cs b/Terminal.Gui/View/ViewKeyboard.cs index 36ec6e661..7a905f129 100644 --- a/Terminal.Gui/View/ViewKeyboard.cs +++ b/Terminal.Gui/View/ViewKeyboard.cs @@ -27,7 +27,7 @@ public partial class View private void DisposeKeyboard () { TitleTextFormatter.HotKeyChanged -= TitleTextFormatter_HotKeyChanged; - KeyBindings.Clear (); + Application.RemoveKeyBindings (this); } #region HotKey Support @@ -197,13 +197,17 @@ public partial class View { KeyBinding keyBinding = new ([Command.HotKey], KeyBindingScope.HotKey, context); // Add the base and Alt key + KeyBindings.Remove (newKey); KeyBindings.Add (newKey, keyBinding); + KeyBindings.Remove (newKey.WithAlt); KeyBindings.Add (newKey.WithAlt, keyBinding); // If the Key is A..Z, add ShiftMask and AltMask | ShiftMask if (newKey.IsKeyCodeAtoZ) { + KeyBindings.Remove (newKey.WithShift); KeyBindings.Add (newKey.WithShift, keyBinding); + KeyBindings.Remove (newKey.WithShift.WithAlt); KeyBindings.Add (newKey.WithShift.WithAlt, keyBinding); } } @@ -800,11 +804,12 @@ public partial class View #if DEBUG // TODO: Determine if App scope bindings should be fired first or last (currently last). - if (Application.TryGetKeyBindings (key, out List views)) + if (Application.KeyBindings.TryGet (key, KeyBindingScope.Focused | KeyBindingScope.HotKey, out KeyBinding b)) { - var boundView = views [0]; - var commandBinding = boundView.KeyBindings.Get (key); - Debug.WriteLine ($"WARNING: InvokeKeyBindings ({key}) - An Application scope binding exists for this key. The registered view will not invoke Command.{commandBinding.Commands [0]}: {boundView}."); + //var boundView = views [0]; + //var commandBinding = boundView.KeyBindings.Get (key); + Debug.WriteLine ( + $"WARNING: InvokeKeyBindings ({key}) - An Application scope binding exists for this key. The registered view will not invoke Command.");//{commandBinding.Commands [0]}: {boundView}."); } // TODO: This is a "prototype" debug check. It may be too annoying vs. useful. diff --git a/Terminal.Gui/Views/DateField.cs b/Terminal.Gui/Views/DateField.cs index 9627b2558..0bb3d9ad2 100644 --- a/Terminal.Gui/Views/DateField.cs +++ b/Terminal.Gui/Views/DateField.cs @@ -400,26 +400,26 @@ public class DateField : TextField AddCommand (Command.RightEnd, () => MoveEnd ()); AddCommand (Command.Right, () => MoveRight ()); - // Default keybindings for this view - KeyBindings.Add (Key.Delete, Command.DeleteCharRight); - KeyBindings.Add (Key.D.WithCtrl, Command.DeleteCharRight); + // Replace the commands defined in TextField + KeyBindings.ReplaceCommands (Key.Delete, Command.DeleteCharRight); + KeyBindings.ReplaceCommands (Key.D.WithCtrl, Command.DeleteCharRight); - KeyBindings.Add (Key.Backspace, Command.DeleteCharLeft); + KeyBindings.ReplaceCommands (Key.Backspace, Command.DeleteCharLeft); - KeyBindings.Add (Key.Home, Command.LeftHome); - KeyBindings.Add (Key.A.WithCtrl, Command.LeftHome); + KeyBindings.ReplaceCommands (Key.Home, Command.LeftHome); + KeyBindings.ReplaceCommands (Key.A.WithCtrl, Command.LeftHome); - KeyBindings.Add (Key.CursorLeft, Command.Left); - KeyBindings.Add (Key.B.WithCtrl, Command.Left); + KeyBindings.ReplaceCommands (Key.CursorLeft, Command.Left); + KeyBindings.ReplaceCommands (Key.B.WithCtrl, Command.Left); - KeyBindings.Add (Key.End, Command.RightEnd); - KeyBindings.Add (Key.E.WithCtrl, Command.RightEnd); + KeyBindings.ReplaceCommands (Key.End, Command.RightEnd); + KeyBindings.ReplaceCommands (Key.E.WithCtrl, Command.RightEnd); - KeyBindings.Add (Key.CursorRight, Command.Right); - KeyBindings.Add (Key.F.WithCtrl, Command.Right); + KeyBindings.ReplaceCommands (Key.CursorRight, Command.Right); + KeyBindings.ReplaceCommands (Key.F.WithCtrl, Command.Right); #if UNIX_KEY_BINDINGS - KeyBindings.Add (Key.D.WithAlt, Command.DeleteCharLeft); + KeyBindings.ReplaceCommands (Key.D.WithAlt, Command.DeleteCharLeft); #endif } diff --git a/Terminal.Gui/Views/FileDialog.cs b/Terminal.Gui/Views/FileDialog.cs index d167f87e3..4aa7b88e8 100644 --- a/Terminal.Gui/Views/FileDialog.cs +++ b/Terminal.Gui/Views/FileDialog.cs @@ -134,7 +134,7 @@ public class FileDialog : Dialog FullRowSelect = true, CollectionNavigator = new FileDialogCollectionNavigator (this) }; - _tableView.KeyBindings.Add (Key.Space, Command.Select); + _tableView.KeyBindings.ReplaceCommands (Key.Space, Command.Select); _tableView.MouseClick += OnTableViewMouseClick; _tableView.Style.InvertSelectedCellFirstCharacter = true; Style.TableStyle = _tableView.Style; @@ -254,10 +254,10 @@ public class FileDialog : Dialog _tableView.KeyUp += (s, k) => k.Handled = TableView_KeyUp (k); _tableView.SelectedCellChanged += TableView_SelectedCellChanged; - _tableView.KeyBindings.Add (Key.Home, Command.TopHome); - _tableView.KeyBindings.Add (Key.End, Command.BottomEnd); - _tableView.KeyBindings.Add (Key.Home.WithShift, Command.TopHomeExtend); - _tableView.KeyBindings.Add (Key.End.WithShift, Command.BottomEndExtend); + _tableView.KeyBindings.ReplaceCommands (Key.Home, Command.TopHome); + _tableView.KeyBindings.ReplaceCommands (Key.End, Command.BottomEnd); + _tableView.KeyBindings.ReplaceCommands (Key.Home.WithShift, Command.TopHomeExtend); + _tableView.KeyBindings.ReplaceCommands (Key.End.WithShift, Command.BottomEndExtend); _treeView.KeyDown += (s, k) => { diff --git a/Terminal.Gui/Views/Menu/Menu.cs b/Terminal.Gui/Views/Menu/Menu.cs index 34e8dab30..3e4923019 100644 --- a/Terminal.Gui/Views/Menu/Menu.cs +++ b/Terminal.Gui/Views/Menu/Menu.cs @@ -192,13 +192,16 @@ internal sealed class Menu : View if ((KeyCode)menuItem.HotKey.Value != KeyCode.Null) { + KeyBindings.Remove ((KeyCode)menuItem.HotKey.Value); KeyBindings.Add ((KeyCode)menuItem.HotKey.Value, keyBinding); + KeyBindings.Remove ((KeyCode)menuItem.HotKey.Value | KeyCode.AltMask); KeyBindings.Add ((KeyCode)menuItem.HotKey.Value | KeyCode.AltMask, keyBinding); } if (menuItem.Shortcut != KeyCode.Null) { keyBinding = new ([Command.Select], KeyBindingScope.HotKey, menuItem); + KeyBindings.Remove (menuItem.Shortcut); KeyBindings.Add (menuItem.Shortcut, keyBinding); } diff --git a/Terminal.Gui/Views/Menu/MenuBarItem.cs b/Terminal.Gui/Views/Menu/MenuBarItem.cs index ea5c35f15..81e755737 100644 --- a/Terminal.Gui/Views/Menu/MenuBarItem.cs +++ b/Terminal.Gui/Views/Menu/MenuBarItem.cs @@ -103,6 +103,7 @@ public class MenuBarItem : MenuItem if (menuItem.Shortcut != KeyCode.Null) { KeyBinding keyBinding = new ([Command.Select], KeyBindingScope.HotKey, menuItem); + menuBar.KeyBindings.Remove (menuItem.Shortcut); menuBar.KeyBindings.Add (menuItem.Shortcut, keyBinding); } diff --git a/Terminal.Gui/Views/MenuBarv2.cs b/Terminal.Gui/Views/MenuBarv2.cs index 12278a24c..d4c598dfe 100644 --- a/Terminal.Gui/Views/MenuBarv2.cs +++ b/Terminal.Gui/Views/MenuBarv2.cs @@ -43,8 +43,6 @@ public class MenuBarv2 : Bar if (view is Shortcut shortcut) { - shortcut.KeyBindingScope = KeyBindingScope.Application; - // TODO: not happy about using AlignmentModes for this. Too implied. // TODO: instead, add a property (a style enum?) to Shortcut to control this //shortcut.AlignmentModes = AlignmentModes.EndToStart; diff --git a/Terminal.Gui/Views/Shortcut.cs b/Terminal.Gui/Views/Shortcut.cs index 7ddbe7c5a..a26a9684d 100644 --- a/Terminal.Gui/Views/Shortcut.cs +++ b/Terminal.Gui/Views/Shortcut.cs @@ -440,353 +440,361 @@ public class Shortcut : View } private void SetCommandViewDefaultLayout () -{ - CommandView.Margin.Thickness = GetMarginThickness (); - CommandView.X = Pos.Align (Alignment.End, AlignmentModes); - CommandView.Y = 0; //Pos.Center (); -} - -private void Shortcut_TitleChanged (object sender, EventArgs e) -{ - // If the Title changes, update the CommandView text. - // This is a helper to make it easier to set the CommandView text. - // CommandView is public and replaceable, but this is a convenience. - _commandView.Text = Title; -} - -#endregion Command - -#region Help - -/// -/// The subview that displays the help text for the command. Internal for unit testing. -/// -internal View HelpView { get; } = new (); - -private void SetHelpViewDefaultLayout () -{ - HelpView.Margin.Thickness = GetMarginThickness (); - HelpView.X = Pos.Align (Alignment.End, AlignmentModes); - HelpView.Y = 0; //Pos.Center (); - HelpView.Width = Dim.Auto (DimAutoStyle.Text); - HelpView.Height = CommandView?.Visible == true ? Dim.Height (CommandView) : 1; - - HelpView.Visible = true; - HelpView.VerticalTextAlignment = Alignment.Center; -} - -/// -/// Gets or sets the help text displayed in the middle of the Shortcut. Identical in function to -/// . -/// -public override string Text -{ - get => HelpView?.Text; - set { - if (HelpView != null) + CommandView.Margin.Thickness = GetMarginThickness (); + CommandView.X = Pos.Align (Alignment.End, AlignmentModes); + CommandView.Y = 0; //Pos.Center (); + } + + private void Shortcut_TitleChanged (object sender, EventArgs e) + { + // If the Title changes, update the CommandView text. + // This is a helper to make it easier to set the CommandView text. + // CommandView is public and replaceable, but this is a convenience. + _commandView.Text = Title; + } + + #endregion Command + + #region Help + + /// + /// The subview that displays the help text for the command. Internal for unit testing. + /// + internal View HelpView { get; } = new (); + + private void SetHelpViewDefaultLayout () + { + HelpView.Margin.Thickness = GetMarginThickness (); + HelpView.X = Pos.Align (Alignment.End, AlignmentModes); + HelpView.Y = 0; //Pos.Center (); + HelpView.Width = Dim.Auto (DimAutoStyle.Text); + HelpView.Height = CommandView?.Visible == true ? Dim.Height (CommandView) : 1; + + HelpView.Visible = true; + HelpView.VerticalTextAlignment = Alignment.Center; + } + + /// + /// Gets or sets the help text displayed in the middle of the Shortcut. Identical in function to + /// . + /// + public override string Text + { + get => HelpView?.Text; + set { - HelpView.Text = value; - ShowHide (); - } - } -} - -/// -/// Gets or sets the help text displayed in the middle of the Shortcut. -/// -public string HelpText -{ - get => HelpView?.Text; - set - { - if (HelpView != null) - { - HelpView.Text = value; - ShowHide (); - } - } -} - -#endregion Help - -#region Key - -private Key _key = Key.Empty; - -/// -/// Gets or sets the that will be bound to the command. -/// -public Key Key -{ - get => _key; - set - { - if (value == null) - { - throw new ArgumentNullException (); - } - - _key = value; - - UpdateKeyBinding (); - - KeyView.Text = Key == Key.Empty ? string.Empty : $"{Key}"; - ShowHide (); - } -} - -private KeyBindingScope _keyBindingScope = KeyBindingScope.HotKey; - -/// -/// Gets or sets the scope for the key binding for how is bound to . -/// -public KeyBindingScope KeyBindingScope -{ - get => _keyBindingScope; - set - { - _keyBindingScope = value; - - UpdateKeyBinding (); - } -} - -/// -/// Gets the subview that displays the key. Internal for unit testing. -/// - -internal View KeyView { get; } = new (); - -private int _minimumKeyTextSize; - -/// -/// Gets or sets the minimum size of the key text. Useful for aligning the key text with other s. -/// -public int MinimumKeyTextSize -{ - get => _minimumKeyTextSize; - set - { - if (value == _minimumKeyTextSize) - { - //return; - } - - _minimumKeyTextSize = value; - SetKeyViewDefaultLayout (); - CommandView.SetNeedsLayout (); - HelpView.SetNeedsLayout (); - KeyView.SetNeedsLayout (); - SetSubViewNeedsDisplay (); - } -} - -private int GetMinimumKeyViewSize () { return MinimumKeyTextSize; } - -private void SetKeyViewDefaultLayout () -{ - KeyView.Margin.Thickness = GetMarginThickness (); - KeyView.X = Pos.Align (Alignment.End, AlignmentModes); - KeyView.Y = 0; //Pos.Center (); - KeyView.Width = Dim.Auto (DimAutoStyle.Text, Dim.Func (GetMinimumKeyViewSize)); - KeyView.Height = CommandView?.Visible == true ? Dim.Height (CommandView) : 1; - - KeyView.Visible = true; - - // Right align the text in the keyview - KeyView.TextAlignment = Alignment.End; - KeyView.VerticalTextAlignment = Alignment.Center; - KeyView.KeyBindings.Clear (); -} - -private void UpdateKeyBinding () -{ - if (Key != null) - { - // Disable the command view key bindings - CommandView.KeyBindings.Remove (Key); - CommandView.KeyBindings.Remove (CommandView.HotKey); - KeyBindings.Remove (Key); - KeyBindings.Add (Key, KeyBindingScope | KeyBindingScope.HotKey, Command.Accept); - //KeyBindings.Add (Key, KeyBindingScope.HotKey, Command.Accept); - } -} - -#endregion Key - -#region Accept Handling - -/// -/// Called when the command is received. This -/// occurs -/// - if the user clicks anywhere on the shortcut with the mouse -/// - if the user presses Key -/// - if the user presses the HotKey specified by CommandView -/// - if HasFocus and the user presses Space or Enter (or any other key bound to Command.Accept). -/// -protected bool? OnAccept (CommandContext ctx) -{ - var cancel = false; - - switch (ctx.KeyBinding?.Scope) - { - case KeyBindingScope.Application: - cancel = base.OnAccept () == true; - - break; - - case KeyBindingScope.Focused: - base.OnAccept (); - - // cancel if we're focused - cancel = true; - - break; - - case KeyBindingScope.HotKey: - cancel = base.OnAccept () == true; - - if (CanFocus) + if (HelpView != null) { - SetFocus (); - cancel = true; + HelpView.Text = value; + ShowHide (); + } + } + } + + /// + /// Gets or sets the help text displayed in the middle of the Shortcut. + /// + public string HelpText + { + get => HelpView?.Text; + set + { + if (HelpView != null) + { + HelpView.Text = value; + ShowHide (); + } + } + } + + #endregion Help + + #region Key + + private Key _key = Key.Empty; + + /// + /// Gets or sets the that will be bound to the command. + /// + public Key Key + { + get => _key; + set + { + if (value == null) + { + throw new ArgumentNullException (); } - break; + _key = value; - default: - // Mouse - cancel = base.OnAccept () == true; + UpdateKeyBinding (); - break; + KeyView.Text = Key == Key.Empty ? string.Empty : $"{Key}"; + ShowHide (); + } } - CommandView.InvokeCommand (Command.Accept, ctx.Key, ctx.KeyBinding); + private KeyBindingScope _keyBindingScope = KeyBindingScope.HotKey; - if (Action is { }) + /// + /// Gets or sets the scope for the key binding for how is bound to . + /// + public KeyBindingScope KeyBindingScope { - Action.Invoke (); - // Assume if there's a subscriber to Action, it's handled. - cancel = true; + get => _keyBindingScope; + set + { + _keyBindingScope = value; + + UpdateKeyBinding (); + } } - return cancel; -} + /// + /// Gets the subview that displays the key. Internal for unit testing. + /// -/// -/// Gets or sets the action to be invoked when the shortcut key is pressed or the shortcut is clicked on with the -/// mouse. -/// -/// -/// Note, the event is fired first, and if cancelled, the event will not be invoked. -/// -[CanBeNull] -public Action Action { get; set; } + internal View KeyView { get; } = new (); -#endregion Accept Handling + private int _minimumKeyTextSize; -private bool? OnSelect (CommandContext ctx) -{ - if (CommandView.GetSupportedCommands ().Contains (Command.Select)) + /// + /// Gets or sets the minimum size of the key text. Useful for aligning the key text with other s. + /// + public int MinimumKeyTextSize { - return CommandView.InvokeCommand (Command.Select, ctx.Key, ctx.KeyBinding); + get => _minimumKeyTextSize; + set + { + if (value == _minimumKeyTextSize) + { + //return; + } + + _minimumKeyTextSize = value; + SetKeyViewDefaultLayout (); + CommandView.SetNeedsLayout (); + HelpView.SetNeedsLayout (); + KeyView.SetNeedsLayout (); + SetSubViewNeedsDisplay (); + } } - return false; -} + private int GetMinimumKeyViewSize () { return MinimumKeyTextSize; } - -#region Focus - -/// -public override ColorScheme ColorScheme -{ - get => base.ColorScheme; - set + private void SetKeyViewDefaultLayout () + { + KeyView.Margin.Thickness = GetMarginThickness (); + KeyView.X = Pos.Align (Alignment.End, AlignmentModes); + KeyView.Y = 0; //Pos.Center (); + KeyView.Width = Dim.Auto (DimAutoStyle.Text, Dim.Func (GetMinimumKeyViewSize)); + KeyView.Height = CommandView?.Visible == true ? Dim.Height (CommandView) : 1; + + KeyView.Visible = true; + + // Right align the text in the keyview + KeyView.TextAlignment = Alignment.End; + KeyView.VerticalTextAlignment = Alignment.Center; + KeyView.KeyBindings.Clear (); + } + + private void UpdateKeyBinding () + { + if (Key != null) + { + // Disable the command view key bindings + CommandView.KeyBindings.Remove (Key); + CommandView.KeyBindings.Remove (CommandView.HotKey); + + if (KeyBindingScope.FastHasFlags (KeyBindingScope.Application)) + { + Application.KeyBindings.Remove (Key); + Application.KeyBindings.Add (Key, this, Command.Accept); + } + else + { + KeyBindings.Remove (Key); + KeyBindings.Add (Key, KeyBindingScope | KeyBindingScope.HotKey, Command.Accept); + } + } + } + + #endregion Key + + #region Accept Handling + + /// + /// Called when the command is received. This + /// occurs + /// - if the user clicks anywhere on the shortcut with the mouse + /// - if the user presses Key + /// - if the user presses the HotKey specified by CommandView + /// - if HasFocus and the user presses Space or Enter (or any other key bound to Command.Accept). + /// + protected bool? OnAccept (CommandContext ctx) + { + var cancel = false; + + switch (ctx.KeyBinding?.Scope) + { + case KeyBindingScope.Application: + cancel = base.OnAccept () == true; + + break; + + case KeyBindingScope.Focused: + base.OnAccept (); + + // cancel if we're focused + cancel = true; + + break; + + case KeyBindingScope.HotKey: + cancel = base.OnAccept () == true; + + if (CanFocus) + { + SetFocus (); + cancel = true; + } + + break; + + default: + // Mouse + cancel = base.OnAccept () == true; + + break; + } + + CommandView.InvokeCommand (Command.Accept, ctx.Key, ctx.KeyBinding); + + if (Action is { }) + { + Action.Invoke (); + // Assume if there's a subscriber to Action, it's handled. + cancel = true; + } + + return cancel; + } + + /// + /// Gets or sets the action to be invoked when the shortcut key is pressed or the shortcut is clicked on with the + /// mouse. + /// + /// + /// Note, the event is fired first, and if cancelled, the event will not be invoked. + /// + [CanBeNull] + public Action Action { get; set; } + + #endregion Accept Handling + + private bool? OnSelect (CommandContext ctx) + { + if (CommandView.GetSupportedCommands ().Contains (Command.Select)) + { + return CommandView.InvokeCommand (Command.Select, ctx.Key, ctx.KeyBinding); + } + return false; + + } + + + #region Focus + + /// + public override ColorScheme ColorScheme + { + get => base.ColorScheme; + set + { + base.ColorScheme = value; + SetColors (); + } + } + + /// + /// + internal void SetColors () + { + // Border should match superview. + Border.ColorScheme = SuperView?.ColorScheme; + + if (HasFocus) + { + // When we have focus, we invert the colors + base.ColorScheme = new (base.ColorScheme) + { + Normal = base.ColorScheme.Focus, + HotNormal = base.ColorScheme.HotFocus, + HotFocus = base.ColorScheme.HotNormal, + Focus = base.ColorScheme.Normal + }; + } + else + { + base.ColorScheme = SuperView?.ColorScheme ?? base.ColorScheme; + } + + // Set KeyView's colors to show "hot" + if (IsInitialized && base.ColorScheme is { }) + { + var cs = new ColorScheme (base.ColorScheme) + { + Normal = base.ColorScheme.HotNormal, + HotNormal = base.ColorScheme.Normal + }; + KeyView.ColorScheme = cs; + } + } + + View _lastFocusedView; + /// + public override bool OnEnter (View view) { - base.ColorScheme = value; SetColors (); + _lastFocusedView = view; + + return base.OnEnter (view); } -} -/// -/// -internal void SetColors () -{ - // Border should match superview. - Border.ColorScheme = SuperView?.ColorScheme; - - if (HasFocus) + /// + public override bool OnLeave (View view) { - // When we have focus, we invert the colors - base.ColorScheme = new (base.ColorScheme) + SetColors (); + _lastFocusedView = this; + + return base.OnLeave (view); + } + + #endregion Focus + + /// + protected override void Dispose (bool disposing) + { + if (disposing) { - Normal = base.ColorScheme.Focus, - HotNormal = base.ColorScheme.HotFocus, - HotFocus = base.ColorScheme.HotNormal, - Focus = base.ColorScheme.Normal - }; - } - else - { - base.ColorScheme = SuperView?.ColorScheme ?? base.ColorScheme; - } + if (CommandView?.IsAdded == false) + { + CommandView.Dispose (); + } - // Set KeyView's colors to show "hot" - if (IsInitialized && base.ColorScheme is { }) - { - var cs = new ColorScheme (base.ColorScheme) - { - Normal = base.ColorScheme.HotNormal, - HotNormal = base.ColorScheme.Normal - }; - KeyView.ColorScheme = cs; - } -} + if (HelpView?.IsAdded == false) + { + HelpView.Dispose (); + } -View _lastFocusedView; -/// -public override bool OnEnter (View view) -{ - SetColors (); - _lastFocusedView = view; - - return base.OnEnter (view); -} - -/// -public override bool OnLeave (View view) -{ - SetColors (); - _lastFocusedView = this; - - return base.OnLeave (view); -} - -#endregion Focus - -/// -protected override void Dispose (bool disposing) -{ - if (disposing) - { - if (CommandView?.IsAdded == false) - { - CommandView.Dispose (); + if (KeyView?.IsAdded == false) + { + KeyView.Dispose (); + } } - if (HelpView?.IsAdded == false) - { - HelpView.Dispose (); - } - - if (KeyView?.IsAdded == false) - { - KeyView.Dispose (); - } + base.Dispose (disposing); } - - base.Dispose (disposing); -} } diff --git a/Terminal.Gui/Views/Slider.cs b/Terminal.Gui/Views/Slider.cs index 61ac401d6..73e479b74 100644 --- a/Terminal.Gui/Views/Slider.cs +++ b/Terminal.Gui/Views/Slider.cs @@ -1454,9 +1454,13 @@ public class Slider : View KeyBindings.Add (Key.CursorUp.WithCtrl, Command.LeftExtend); } + KeyBindings.Remove (Key.Home); KeyBindings.Add (Key.Home, Command.LeftHome); + KeyBindings.Remove (Key.End); KeyBindings.Add (Key.End, Command.RightEnd); + KeyBindings.Remove (Key.Enter); KeyBindings.Add (Key.Enter, Command.Accept); + KeyBindings.Remove (Key.Space); KeyBindings.Add (Key.Space, Command.Select); } diff --git a/Terminal.Gui/Views/StatusBar.cs b/Terminal.Gui/Views/StatusBar.cs index b4df14e6b..ce3ad268a 100644 --- a/Terminal.Gui/Views/StatusBar.cs +++ b/Terminal.Gui/Views/StatusBar.cs @@ -65,8 +65,6 @@ public class StatusBar : Bar if (view is Shortcut shortcut) { - shortcut.KeyBindingScope = KeyBindingScope.Application; - // TODO: not happy about using AlignmentModes for this. Too implied. // TODO: instead, add a property (a style enum?) to Shortcut to control this shortcut.AlignmentModes = AlignmentModes.EndToStart; diff --git a/Terminal.Gui/Views/TableView/CheckBoxTableSourceWrapper.cs b/Terminal.Gui/Views/TableView/CheckBoxTableSourceWrapper.cs index 7d2379af8..8be294c1a 100644 --- a/Terminal.Gui/Views/TableView/CheckBoxTableSourceWrapper.cs +++ b/Terminal.Gui/Views/TableView/CheckBoxTableSourceWrapper.cs @@ -26,7 +26,7 @@ public abstract class CheckBoxTableSourceWrapperBase : ITableSource Wrapping = toWrap; this.tableView = tableView; - tableView.KeyBindings.Add (Key.Space, Command.Select); + tableView.KeyBindings.ReplaceCommands (Key.Space, Command.Select); tableView.MouseClick += TableView_MouseClick; tableView.CellToggled += TableView_CellToggled; diff --git a/Terminal.Gui/Views/TableView/TableView.cs b/Terminal.Gui/Views/TableView/TableView.cs index 0807991d5..67974349f 100644 --- a/Terminal.Gui/Views/TableView/TableView.cs +++ b/Terminal.Gui/Views/TableView/TableView.cs @@ -319,7 +319,7 @@ public class TableView : View { if (cellActivationKey != value) { - KeyBindings.Replace (cellActivationKey, value); + KeyBindings.ReplaceKey (cellActivationKey, value); // of API user is mixing and matching old and new methods of keybinding then they may have lost // the old binding (e.g. with ClearKeybindings) so KeyBindings.Replace alone will fail diff --git a/Terminal.Gui/Views/TextField.cs b/Terminal.Gui/Views/TextField.cs index bdcb92dd5..b5d14db0a 100644 --- a/Terminal.Gui/Views/TextField.cs +++ b/Terminal.Gui/Views/TextField.cs @@ -1332,7 +1332,10 @@ public class TextField : View ); } - private void ContextMenu_KeyChanged (object sender, KeyChangedEventArgs e) { KeyBindings.Replace (e.OldKey.KeyCode, e.NewKey.KeyCode); } + private void ContextMenu_KeyChanged (object sender, KeyChangedEventArgs e) + { + KeyBindings.ReplaceKey (e.OldKey.KeyCode, e.NewKey.KeyCode); + } private List DeleteSelectedText () { diff --git a/Terminal.Gui/Views/TextValidateField.cs b/Terminal.Gui/Views/TextValidateField.cs index 35779d59b..045f6df3f 100644 --- a/Terminal.Gui/Views/TextValidateField.cs +++ b/Terminal.Gui/Views/TextValidateField.cs @@ -464,7 +464,6 @@ namespace Terminal.Gui KeyBindings.Add (Key.Home, Command.LeftHome); KeyBindings.Add (Key.End, Command.RightEnd); - KeyBindings.Add (Key.Delete, Command.DeleteCharRight); KeyBindings.Add (Key.Delete, Command.DeleteCharRight); KeyBindings.Add (Key.Backspace, Command.DeleteCharLeft); diff --git a/Terminal.Gui/Views/TextView.cs b/Terminal.Gui/Views/TextView.cs index bea620aee..598f78961 100644 --- a/Terminal.Gui/Views/TextView.cs +++ b/Terminal.Gui/Views/TextView.cs @@ -2369,8 +2369,8 @@ public class TextView : View ); AddCommand (Command.Tab, () => ProcessTab ()); AddCommand (Command.BackTab, () => ProcessBackTab ()); - AddCommand (Command.NextView, () => ProcessMoveNextView ()); - AddCommand (Command.PreviousView, () => ProcessMovePreviousView ()); + //AddCommand (Command.NextView, () => ProcessMoveNextView ()); + //AddCommand (Command.PreviousView, () => ProcessMovePreviousView ()); AddCommand ( Command.Undo, @@ -2503,11 +2503,11 @@ public class TextView : View KeyBindings.Add (Key.Tab, Command.Tab); KeyBindings.Add (Key.Tab.WithShift, Command.BackTab); - KeyBindings.Add (Key.Tab.WithCtrl, Command.NextView); - KeyBindings.Add (Application.AlternateForwardKey, Command.NextView); + //KeyBindings.Add (Key.Tab.WithCtrl, Command.NextView); + //KeyBindings.Add (Application.AlternateForwardKey, Command.NextView); - KeyBindings.Add (Key.Tab.WithCtrl.WithShift, Command.PreviousView); - KeyBindings.Add (Application.AlternateBackwardKey, Command.PreviousView); + //KeyBindings.Add (Key.Tab.WithCtrl.WithShift, Command.PreviousView); + //KeyBindings.Add (Application.AlternateBackwardKey, Command.PreviousView); KeyBindings.Add (Key.Z.WithCtrl, Command.Undo); KeyBindings.Add (Key.R.WithCtrl, Command.Redo); @@ -4318,7 +4318,7 @@ public class TextView : View DoNeededAction (); } - private void ContextMenu_KeyChanged (object sender, KeyChangedEventArgs e) { KeyBindings.Replace (e.OldKey, e.NewKey); } + private void ContextMenu_KeyChanged (object sender, KeyChangedEventArgs e) { KeyBindings.ReplaceKey (e.OldKey, e.NewKey); } private bool DeleteTextBackwards () { @@ -6393,8 +6393,8 @@ public class TextView : View _selectionStartRow = CurrentRow; } - private void Top_AlternateBackwardKeyChanged (object sender, KeyChangedEventArgs e) { KeyBindings.Replace (e.OldKey, e.NewKey); } - private void Top_AlternateForwardKeyChanged (object sender, KeyChangedEventArgs e) { KeyBindings.Replace (e.OldKey, e.NewKey); } + private void Top_AlternateBackwardKeyChanged (object sender, KeyChangedEventArgs e) { KeyBindings.ReplaceKey (e.OldKey, e.NewKey); } + private void Top_AlternateForwardKeyChanged (object sender, KeyChangedEventArgs e) { KeyBindings.ReplaceKey (e.OldKey, e.NewKey); } // Tries to snap the cursor to the tracking column private void TrackColumn () diff --git a/Terminal.Gui/Views/TimeField.cs b/Terminal.Gui/Views/TimeField.cs index 5f303cc98..a473fdcea 100644 --- a/Terminal.Gui/Views/TimeField.cs +++ b/Terminal.Gui/Views/TimeField.cs @@ -58,26 +58,26 @@ public class TimeField : TextField AddCommand (Command.RightEnd, () => MoveEnd ()); AddCommand (Command.Right, () => MoveRight ()); - // Default keybindings for this view - KeyBindings.Add (Key.Delete, Command.DeleteCharRight); - KeyBindings.Add (Key.D.WithCtrl, Command.DeleteCharRight); + // Replace the key bindings defined in TextField + KeyBindings.ReplaceCommands (Key.Delete, Command.DeleteCharRight); + KeyBindings.ReplaceCommands (Key.D.WithCtrl, Command.DeleteCharRight); - KeyBindings.Add (Key.Backspace, Command.DeleteCharLeft); + KeyBindings.ReplaceCommands (Key.Backspace, Command.DeleteCharLeft); - KeyBindings.Add (Key.Home, Command.LeftHome); - KeyBindings.Add (Key.A.WithCtrl, Command.LeftHome); + KeyBindings.ReplaceCommands (Key.Home, Command.LeftHome); + KeyBindings.ReplaceCommands (Key.A.WithCtrl, Command.LeftHome); - KeyBindings.Add (Key.CursorLeft, Command.Left); - KeyBindings.Add (Key.B.WithCtrl, Command.Left); + KeyBindings.ReplaceCommands (Key.CursorLeft, Command.Left); + KeyBindings.ReplaceCommands (Key.B.WithCtrl, Command.Left); - KeyBindings.Add (Key.End, Command.RightEnd); - KeyBindings.Add (Key.E.WithCtrl, Command.RightEnd); + KeyBindings.ReplaceCommands (Key.End, Command.RightEnd); + KeyBindings.ReplaceCommands (Key.E.WithCtrl, Command.RightEnd); - KeyBindings.Add (Key.CursorRight, Command.Right); - KeyBindings.Add (Key.F.WithCtrl, Command.Right); + KeyBindings.ReplaceCommands (Key.CursorRight, Command.Right); + KeyBindings.ReplaceCommands (Key.F.WithCtrl, Command.Right); #if UNIX_KEY_BINDINGS - KeyBindings.Add (Key.D.WithAlt, Command.DeleteCharLeft); + KeyBindings.ReplaceCommands (Key.D.WithAlt, Command.DeleteCharLeft); #endif } diff --git a/Terminal.Gui/Views/Toplevel.cs b/Terminal.Gui/Views/Toplevel.cs index 0e7090492..fb31efb73 100644 --- a/Terminal.Gui/Views/Toplevel.cs +++ b/Terminal.Gui/Views/Toplevel.cs @@ -34,7 +34,7 @@ public partial class Toplevel : View ColorScheme = Colors.ColorSchemes ["TopLevel"]; - ConfigureKeyBindings (); + //ConfigureKeyBindings (); MouseClick += Toplevel_MouseClick; } @@ -188,7 +188,7 @@ public partial class Toplevel : View /// public virtual void OnAlternateBackwardKeyChanged (KeyChangedEventArgs e) { - KeyBindings.Replace (e.OldKey, e.NewKey); + KeyBindings.ReplaceKey (e.OldKey, e.NewKey); AlternateBackwardKeyChanged?.Invoke (this, e); } @@ -197,7 +197,7 @@ public partial class Toplevel : View /// public virtual void OnAlternateForwardKeyChanged (KeyChangedEventArgs e) { - KeyBindings.Replace (e.OldKey, e.NewKey); + KeyBindings.ReplaceKey (e.OldKey, e.NewKey); AlternateForwardKeyChanged?.Invoke (this, e); } @@ -205,7 +205,7 @@ public partial class Toplevel : View /// public virtual void OnQuitKeyChanged (KeyChangedEventArgs e) { - KeyBindings.Replace (e.OldKey, e.NewKey); + KeyBindings.ReplaceKey (e.OldKey, e.NewKey); QuitKeyChanged?.Invoke (this, e); } @@ -650,7 +650,7 @@ public partial class Toplevel : View /// /// Moves the focus to /// - private void MoveNextView () + internal void MoveNextView () { View old = GetDeepestFocusedSubview (Focused); @@ -670,7 +670,7 @@ public partial class Toplevel : View } } - private void MoveNextViewOrTop () + internal void MoveNextViewOrTop () { if (Application.OverlappedTop is null) { @@ -691,7 +691,7 @@ public partial class Toplevel : View } } - private void MovePreviousView () + internal void MovePreviousView () { View old = GetDeepestFocusedSubview (Focused); @@ -711,7 +711,7 @@ public partial class Toplevel : View } } - private void MovePreviousViewOrTop () + internal void MovePreviousViewOrTop () { if (Application.OverlappedTop is null) { diff --git a/Terminal.Gui/Views/TreeView/TreeView.cs b/Terminal.Gui/Views/TreeView/TreeView.cs index f2039d9c6..5fbde234c 100644 --- a/Terminal.Gui/Views/TreeView/TreeView.cs +++ b/Terminal.Gui/Views/TreeView/TreeView.cs @@ -352,7 +352,7 @@ public class TreeView : View, ITreeView where T : class { if (objectActivationKey != value) { - KeyBindings.Replace (ObjectActivationKey, value); + KeyBindings.ReplaceKey (ObjectActivationKey, value); objectActivationKey = value; } } diff --git a/UICatalog/Scenarios/KeyBindings.cs b/UICatalog/Scenarios/KeyBindings.cs index 8814a1991..d62444543 100644 --- a/UICatalog/Scenarios/KeyBindings.cs +++ b/UICatalog/Scenarios/KeyBindings.cs @@ -80,13 +80,10 @@ public sealed class KeyBindings : Scenario }; appWindow.Add (appBindingsListView); - foreach (var appBinding in Application.GetKeyBindings ()) + foreach (var appBinding in Application.KeyBindings.Bindings) { - foreach (var view in appBinding.Value) - { - var commands = view.KeyBindings.GetCommands (appBinding.Key); - appBindings.Add ($"{appBinding.Key} -> {view.GetType ().Name} - {commands [0]}"); - } + var commands = Application.KeyBindings.GetCommands (appBinding.Key); + appBindings.Add ($"{appBinding.Key} -> {appBinding.Value.BoundView?.GetType ().Name} - {commands [0]}"); } ObservableCollection hotkeyBindings = new (); @@ -153,10 +150,10 @@ public sealed class KeyBindings : Scenario private void AppWindow_Leave (object sender, FocusEventArgs e) { - //foreach (var binding in Application.Top.MostFocused.KeyBindings.Bindings.Where (b => b.Value.Scope == KeyBindingScope.Focused)) - //{ - // _focusedBindings.Add ($"{binding.Key} -> {binding.Value.Commands [0]}"); - //} + foreach (var binding in Application.Top.MostFocused.KeyBindings.Bindings.Where (b => b.Value.Scope == KeyBindingScope.Focused)) + { + _focusedBindings.Add ($"{binding.Key} -> {binding.Value.Commands [0]}"); + } } } @@ -166,28 +163,34 @@ public class KeyBindingsDemo : View { CanFocus = true; + + AddCommand (Command.Save, ctx => + { + MessageBox.Query ($"{ctx.KeyBinding?.Scope}", $"Key: {ctx.Key}\nCommand: {ctx.Command}", buttons: "Ok"); + return true; + }); AddCommand (Command.New, ctx => { - MessageBox.Query ("Hi", $"Key: {ctx.Key}\nCommand: {ctx.Command}", buttons: "Ok"); - + MessageBox.Query ($"{ctx.KeyBinding?.Scope}", $"Key: {ctx.Key}\nCommand: {ctx.Command}", buttons: "Ok"); return true; }); AddCommand (Command.HotKey, ctx => { - MessageBox.Query ("Hi", $"Key: {ctx.Key}\nCommand: {ctx.Command}", buttons: "Ok"); + MessageBox.Query ($"{ctx.KeyBinding?.Scope}", $"Key: {ctx.Key}\nCommand: {ctx.Command}", buttons: "Ok"); SetFocus (); return true; }); - KeyBindings.Add (Key.F3, KeyBindingScope.Focused, Command.New); - KeyBindings.Add (Key.F4, KeyBindingScope.Application, Command.New); - + KeyBindings.Add (Key.F2, KeyBindingScope.Focused, Command.Save); + KeyBindings.Add (Key.F3, Command.New); // same as specifying KeyBindingScope.Focused + Application.KeyBindings.Add (Key.F4, this, Command.New); AddCommand (Command.QuitToplevel, ctx => { + MessageBox.Query ($"{ctx.KeyBinding?.Scope}", $"Key: {ctx.Key}\nCommand: {ctx.Command}", buttons: "Ok"); Application.RequestStop (); return true; }); - KeyBindings.Add (Key.Q.WithCtrl, KeyBindingScope.Application, Command.QuitToplevel); + Application.KeyBindings.Add (Key.Q.WithAlt, this, Command.QuitToplevel); } } diff --git a/UICatalog/Scenarios/ListColumns.cs b/UICatalog/Scenarios/ListColumns.cs index 0f45b1e5d..12c9619cf 100644 --- a/UICatalog/Scenarios/ListColumns.cs +++ b/UICatalog/Scenarios/ListColumns.cs @@ -254,7 +254,7 @@ public class ListColumns : Scenario // if user clicks the mouse in TableView _listColView.MouseClick += (s, e) => { _listColView.ScreenToCell (e.MouseEvent.Position, out int? clickedCol); }; - _listColView.KeyBindings.Add (Key.Space, Command.Accept); + _listColView.KeyBindings.ReplaceCommands (Key.Space, Command.Accept); top.Add (appWindow); diff --git a/UICatalog/Scenarios/TableEditor.cs b/UICatalog/Scenarios/TableEditor.cs index df86aceaf..ca36671ce 100644 --- a/UICatalog/Scenarios/TableEditor.cs +++ b/UICatalog/Scenarios/TableEditor.cs @@ -769,7 +769,7 @@ public class TableEditor : Scenario } }; - _tableView.KeyBindings.Add (Key.Space, Command.Accept); + _tableView.KeyBindings.ReplaceCommands (Key.Space, Command.Accept); // Run - Start the application. Application.Run (appWindow); diff --git a/UICatalog/UICatalog.cs b/UICatalog/UICatalog.cs index f9ac4a503..dd18f0bd1 100644 --- a/UICatalog/UICatalog.cs +++ b/UICatalog/UICatalog.cs @@ -605,7 +605,9 @@ internal class UICatalogApp ScenarioList.CellActivated += ScenarioView_OpenSelectedItem; // TableView typically is a grid where nav keys are biased for moving left/right. + ScenarioList.KeyBindings.Remove (Key.Home); ScenarioList.KeyBindings.Add (Key.Home, Command.TopHome); + ScenarioList.KeyBindings.Remove (Key.End); ScenarioList.KeyBindings.Add (Key.End, Command.BottomEnd); // Ideally, TableView.MultiSelect = false would turn off any keybindings for diff --git a/UnitTests/Application/ApplicationTests.cs b/UnitTests/Application/ApplicationTests.cs index 6363233a9..f8f1f8029 100644 --- a/UnitTests/Application/ApplicationTests.cs +++ b/UnitTests/Application/ApplicationTests.cs @@ -199,7 +199,7 @@ public class ApplicationTests Assert.Null (Application._mouseEnteredView); // Keyboard - Assert.Empty (Application.GetViewsWithKeyBindings ()); + Assert.Empty (Application.GetViewKeyBindings ()); // Events - Can't check //Assert.Null (Application.NotifyNewRunState); @@ -233,7 +233,7 @@ public class ApplicationTests Application.AlternateBackwardKey = Key.A; Application.AlternateForwardKey = Key.B; Application.QuitKey = Key.C; - Application.AddKeyBinding (Key.A, new View ()); + Application.KeyBindings.Add (Key.A, KeyBindingScope.Application, Command.Cancel); //Application.OverlappedChildren = new List (); //Application.OverlappedTop = diff --git a/UnitTests/Application/KeyboardTests.cs b/UnitTests/Application/KeyboardTests.cs index 3483b8396..4dfb5b11a 100644 --- a/UnitTests/Application/KeyboardTests.cs +++ b/UnitTests/Application/KeyboardTests.cs @@ -71,7 +71,7 @@ public class KeyboardTests // After Init Assert.Equal (Key.Esc, Application.QuitKey); - Application.Shutdown(); + Application.Shutdown (); } private object _timeoutLock; @@ -200,62 +200,62 @@ public class KeyboardTests Assert.True (v1.HasFocus); // Using default keys. - top.NewKeyDownEvent (Key.Tab.WithCtrl); + Application.OnKeyDown (Key.Tab.WithCtrl); Assert.True (v2.HasFocus); - top.NewKeyDownEvent (Key.Tab.WithCtrl); + Application.OnKeyDown (Key.Tab.WithCtrl); Assert.True (v3.HasFocus); - top.NewKeyDownEvent (Key.Tab.WithCtrl); + Application.OnKeyDown (Key.Tab.WithCtrl); Assert.True (v4.HasFocus); - top.NewKeyDownEvent (Key.Tab.WithCtrl); + Application.OnKeyDown (Key.Tab.WithCtrl); Assert.True (v1.HasFocus); - top.NewKeyDownEvent (Key.Tab.WithShift.WithCtrl); + Application.OnKeyDown (Key.Tab.WithShift.WithCtrl); Assert.True (v4.HasFocus); - top.NewKeyDownEvent (Key.Tab.WithShift.WithCtrl); + Application.OnKeyDown (Key.Tab.WithShift.WithCtrl); Assert.True (v3.HasFocus); - top.NewKeyDownEvent (Key.Tab.WithShift.WithCtrl); + Application.OnKeyDown (Key.Tab.WithShift.WithCtrl); Assert.True (v2.HasFocus); - top.NewKeyDownEvent (Key.Tab.WithShift.WithCtrl); + Application.OnKeyDown (Key.Tab.WithShift.WithCtrl); Assert.True (v1.HasFocus); - top.NewKeyDownEvent (Key.PageDown.WithCtrl); + Application.OnKeyDown (Key.PageDown.WithCtrl); Assert.True (v2.HasFocus); - top.NewKeyDownEvent (Key.PageDown.WithCtrl); + Application.OnKeyDown (Key.PageDown.WithCtrl); Assert.True (v3.HasFocus); - top.NewKeyDownEvent (Key.PageDown.WithCtrl); + Application.OnKeyDown (Key.PageDown.WithCtrl); Assert.True (v4.HasFocus); - top.NewKeyDownEvent (Key.PageDown.WithCtrl); + Application.OnKeyDown (Key.PageDown.WithCtrl); Assert.True (v1.HasFocus); - top.NewKeyDownEvent (Key.PageUp.WithCtrl); + Application.OnKeyDown (Key.PageUp.WithCtrl); Assert.True (v4.HasFocus); - top.NewKeyDownEvent (Key.PageUp.WithCtrl); + Application.OnKeyDown (Key.PageUp.WithCtrl); Assert.True (v3.HasFocus); - top.NewKeyDownEvent (Key.PageUp.WithCtrl); + Application.OnKeyDown (Key.PageUp.WithCtrl); Assert.True (v2.HasFocus); - top.NewKeyDownEvent (Key.PageUp.WithCtrl); + Application.OnKeyDown (Key.PageUp.WithCtrl); Assert.True (v1.HasFocus); // Using another's alternate keys. Application.AlternateForwardKey = Key.F7; Application.AlternateBackwardKey = Key.F6; - top.NewKeyDownEvent (Key.F7); + Application.OnKeyDown (Key.F7); Assert.True (v2.HasFocus); - top.NewKeyDownEvent (Key.F7); + Application.OnKeyDown (Key.F7); Assert.True (v3.HasFocus); - top.NewKeyDownEvent (Key.F7); + Application.OnKeyDown (Key.F7); Assert.True (v4.HasFocus); - top.NewKeyDownEvent (Key.F7); + Application.OnKeyDown (Key.F7); Assert.True (v1.HasFocus); - top.NewKeyDownEvent (Key.F6); + Application.OnKeyDown (Key.F6); Assert.True (v4.HasFocus); - top.NewKeyDownEvent (Key.F6); + Application.OnKeyDown (Key.F6); Assert.True (v3.HasFocus); - top.NewKeyDownEvent (Key.F6); + Application.OnKeyDown (Key.F6); Assert.True (v2.HasFocus); - top.NewKeyDownEvent (Key.F6); + Application.OnKeyDown (Key.F6); Assert.True (v1.HasFocus); Application.RequestStop (); @@ -321,14 +321,14 @@ public class KeyboardTests Assert.True (win2.HasFocus); Assert.Equal ("win2", ((Window)top.Subviews [^1]).Title); - top.NewKeyDownEvent (Key.Tab.WithCtrl); + Application.OnKeyDown (Key.Tab.WithCtrl); Assert.True (win2.CanFocus); Assert.False (win.HasFocus); Assert.True (win2.CanFocus); Assert.True (win2.HasFocus); Assert.Equal ("win2", ((Window)top.Subviews [^1]).Title); - top.NewKeyDownEvent (Key.Tab.WithCtrl); + Application.OnKeyDown (Key.Tab.WithCtrl); Assert.False (win.CanFocus); Assert.False (win.HasFocus); Assert.True (win2.CanFocus); @@ -374,14 +374,14 @@ public class KeyboardTests Assert.False (win2.HasFocus); Assert.Equal ("win", ((Window)top.Subviews [^1]).Title); - top.NewKeyDownEvent (Key.Tab.WithCtrl); + Application.OnKeyDown (Key.Tab.WithCtrl); Assert.True (win.CanFocus); Assert.False (win.HasFocus); Assert.True (win2.CanFocus); Assert.True (win2.HasFocus); Assert.Equal ("win2", ((Window)top.Subviews [^1]).Title); - top.NewKeyDownEvent (Key.Tab.WithCtrl); + Application.OnKeyDown (Key.Tab.WithCtrl); Assert.True (win.CanFocus); Assert.True (win.HasFocus); Assert.True (win2.CanFocus); @@ -496,21 +496,21 @@ public class KeyboardTests Application.Begin (top); Application.OnKeyDown (Key.A); - Assert.True (invoked); + Assert.False (invoked); Assert.True (view.ApplicationCommand); invoked = false; view.ApplicationCommand = false; - view.KeyBindings.Remove (KeyCode.A); + Application.KeyBindings.Remove (KeyCode.A); Application.OnKeyDown (Key.A); // old Assert.False (invoked); Assert.False (view.ApplicationCommand); - view.KeyBindings.Add (Key.A.WithCtrl, KeyBindingScope.Application, Command.Save); + Application.KeyBindings.Add (Key.A.WithCtrl, view, Command.Save); Application.OnKeyDown (Key.A); // old Assert.False (invoked); Assert.False (view.ApplicationCommand); Application.OnKeyDown (Key.A.WithCtrl); // new - Assert.True (invoked); + Assert.False (invoked); Assert.True (view.ApplicationCommand); invoked = false; @@ -556,70 +556,60 @@ public class KeyboardTests top.Dispose (); } + [Fact] + [AutoInitShutdown] + public void KeyBinding_Application_KeyBindings_Add_Adds () + { + Application.KeyBindings.Add (Key.A, KeyBindingScope.Application, Command.Accept); + Application.KeyBindings.Add (Key.B, KeyBindingScope.Application, Command.Accept); + + Assert.True (Application.KeyBindings.TryGet (Key.A, out var binding)); + Assert.Null (binding.BoundView); + Assert.True (Application.KeyBindings.TryGet (Key.B, out binding)); + Assert.Null (binding.BoundView); + } [Fact] [AutoInitShutdown] - public void KeyBinding_AddKeyBinding_Adds () + public void KeyBinding_View_KeyBindings_Add_Adds () { View view1 = new (); - Application.AddKeyBinding (Key.A, view1); + Application.KeyBindings.Add (Key.A, view1, Command.Accept); View view2 = new (); - Application.AddKeyBinding (Key.A, view2); + Application.KeyBindings.Add (Key.B, view2, Command.Accept); - Assert.True (Application.TryGetKeyBindings (Key.A, out List views)); - Assert.Contains (view1, views); - Assert.Contains (view2, views); - - Assert.False (Application.TryGetKeyBindings (Key.B, out List _)); + Assert.True (Application.KeyBindings.TryGet (Key.A, out var binding)); + Assert.Equal (view1, binding.BoundView); + Assert.True (Application.KeyBindings.TryGet (Key.B, out binding)); + Assert.Equal (view2, binding.BoundView); } [Fact] [AutoInitShutdown] - public void KeyBinding_ViewKeyBindings_Add_Adds () + public void KeyBinding_Application_RemoveKeyBinding_Removes () { + Application.KeyBindings.Add (Key.A, KeyBindingScope.Application, Command.Accept); + + Assert.True (Application.KeyBindings.TryGet (Key.A, out _)); + + Application.KeyBindings.Remove (Key.A); + Assert.False (Application.KeyBindings.TryGet (Key.A, out _)); + } + + [Fact] + [AutoInitShutdown] + public void KeyBinding_View_KeyBindings_RemoveKeyBinding_Removes () + { + View view1 = new (); - view1.KeyBindings.Add (Key.A, KeyBindingScope.Application, Command.Save); - view1.KeyBindings.Add (Key.B, KeyBindingScope.HotKey, Command.Left); - Assert.Single (Application.GetViewsWithKeyBindings ()); + Application.KeyBindings.Add (Key.A, view1, Command.Accept); View view2 = new (); - view2.KeyBindings.Add (Key.A, KeyBindingScope.Application, Command.Save); - view2.KeyBindings.Add (Key.B, KeyBindingScope.HotKey, Command.Left); + Application.KeyBindings.Add (Key.B, view1, Command.Accept); - Assert.True (Application.TryGetKeyBindings (Key.A, out List views)); - Assert.Contains (view1, views); - Assert.Contains (view2, views); - - Assert.False (Application.TryGetKeyBindings (Key.B, out List _)); - } - - [Fact] - [AutoInitShutdown] - public void KeyBinding_RemoveKeyBinding_Removes () - { - View view1 = new (); - Application.AddKeyBinding (Key.A, view1); - - Assert.True (Application.TryGetKeyBindings (Key.A, out List views)); - Assert.Contains (view1, views); - - Application.RemoveKeyBinding (Key.A, view1); - Assert.False (Application.TryGetKeyBindings (Key.A, out List _)); - } - - [Fact] - [AutoInitShutdown] - public void KeyBinding_ViewKeyBindings_RemoveKeyBinding_Removes () - { - View view1 = new (); - view1.KeyBindings.Add (Key.A, KeyBindingScope.Application, Command.Save); - - Assert.True (Application.TryGetKeyBindings (Key.A, out List views)); - Assert.Contains (view1, views); - - view1.KeyBindings.Remove (Key.A); - Assert.False (Application.TryGetKeyBindings (Key.A, out List _)); + Application.KeyBindings.Remove (Key.A, view1); + Assert.False (Application.KeyBindings.TryGet (Key.A, out _)); } // Test View for testing Application key Bindings @@ -631,9 +621,9 @@ public class KeyboardTests AddCommand (Command.HotKey, () => HotKeyCommand = true); AddCommand (Command.Left, () => FocusedCommand = true); - KeyBindings.Add (Key.A, KeyBindingScope.Application, Command.Save); + Application.KeyBindings.Add (Key.A, this, Command.Save); HotKey = KeyCode.H; - KeyBindings.Add (Key.F, KeyBindingScope.Focused, Command.Left); + KeyBindings.Add (Key.F, Command.Left); } public bool ApplicationCommand { get; set; } diff --git a/UnitTests/Input/KeyBindingTests.cs b/UnitTests/Input/KeyBindingTests.cs index 73a1c6b00..bf3b007fd 100644 --- a/UnitTests/Input/KeyBindingTests.cs +++ b/UnitTests/Input/KeyBindingTests.cs @@ -21,12 +21,12 @@ public class KeyBindingTests var keyBindings = new KeyBindings (); Command [] commands = { Command.Right, Command.Left }; - keyBindings.Add (Key.A, commands); + keyBindings.Add (Key.A, KeyBindingScope.Application, commands); Command [] resultCommands = keyBindings.GetCommands (Key.A); Assert.Contains (Command.Right, resultCommands); Assert.Contains (Command.Left, resultCommands); - keyBindings.Add (Key.B, commands); + keyBindings.Add (Key.B, KeyBindingScope.Application, commands); resultCommands = keyBindings.GetCommands (Key.B); Assert.Contains (Command.Right, resultCommands); Assert.Contains (Command.Left, resultCommands); @@ -36,11 +36,11 @@ public class KeyBindingTests public void Add_Single_Adds () { var keyBindings = new KeyBindings (); - keyBindings.Add (Key.A, Command.HotKey); + keyBindings.Add (Key.A, KeyBindingScope.Application, Command.HotKey); Command [] resultCommands = keyBindings.GetCommands (Key.A); Assert.Contains (Command.HotKey, resultCommands); - keyBindings.Add (Key.B, Command.HotKey); + keyBindings.Add (Key.B, KeyBindingScope.Application, Command.HotKey); resultCommands = keyBindings.GetCommands (Key.B); Assert.Contains (Command.HotKey, resultCommands); } @@ -50,7 +50,7 @@ public class KeyBindingTests public void Clear_Clears () { var keyBindings = new KeyBindings (); - keyBindings.Add (Key.B, Command.HotKey); + keyBindings.Add (Key.B, KeyBindingScope.Application, Command.HotKey); keyBindings.Clear (); Command [] resultCommands = keyBindings.GetCommands (Key.A); Assert.Empty (resultCommands); @@ -78,7 +78,7 @@ public class KeyBindingTests public void GetCommands_WithCommands_ReturnsCommands () { var keyBindings = new KeyBindings (); - keyBindings.Add (Key.A, Command.HotKey); + keyBindings.Add (Key.A, KeyBindingScope.Application, Command.HotKey); Command [] resultCommands = keyBindings.GetCommands (Key.A); Assert.Contains (Command.HotKey, resultCommands); } @@ -88,8 +88,8 @@ public class KeyBindingTests { var keyBindings = new KeyBindings (); Command [] commands = { Command.Right, Command.Left }; - keyBindings.Add (Key.A, commands); - keyBindings.Add (Key.B, commands); + keyBindings.Add (Key.A, KeyBindingScope.Application, commands); + keyBindings.Add (Key.B, KeyBindingScope.Application, commands); Command [] resultCommands = keyBindings.GetCommands (Key.A); Assert.Contains (Command.Right, resultCommands); Assert.Contains (Command.Left, resultCommands); @@ -103,7 +103,7 @@ public class KeyBindingTests { var keyBindings = new KeyBindings (); Command [] commands = { Command.Right, Command.Left }; - keyBindings.Add (Key.A, commands); + keyBindings.Add (Key.A, KeyBindingScope.Application, commands); Command [] resultCommands = keyBindings.GetCommands (Key.A); Assert.Contains (Command.Right, resultCommands); Assert.Contains (Command.Left, resultCommands); @@ -114,10 +114,10 @@ public class KeyBindingTests { var keyBindings = new KeyBindings (); Command [] commands1 = { Command.Right, Command.Left }; - keyBindings.Add (Key.A, commands1); + keyBindings.Add (Key.A, KeyBindingScope.Application, commands1); Command [] commands2 = { Command.LineUp, Command.LineDown }; - keyBindings.Add (Key.B, commands2); + keyBindings.Add (Key.B, KeyBindingScope.Application, commands2); Key key = keyBindings.GetKeyFromCommands (commands1); Assert.Equal (Key.A, key); @@ -133,7 +133,7 @@ public class KeyBindingTests public void GetKeyFromCommands_OneCommand () { var keyBindings = new KeyBindings (); - keyBindings.Add (Key.A, Command.Right); + keyBindings.Add (Key.A, KeyBindingScope.Application, Command.Right); Key key = keyBindings.GetKeyFromCommands (Command.Right); Assert.Equal (Key.A, key); @@ -154,66 +154,66 @@ public class KeyBindingTests public void GetKeyFromCommands_WithCommands_ReturnsKey () { var keyBindings = new KeyBindings (); - keyBindings.Add (Key.A, Command.HotKey); + keyBindings.Add (Key.A, KeyBindingScope.Application, Command.HotKey); Key resultKey = keyBindings.GetKeyFromCommands (Command.HotKey); Assert.Equal (Key.A, resultKey); } // Add should not allow duplicates [Fact] - public void Add_Replaces_If_Exists () + public void Add_Throws_If_Exists () { var keyBindings = new KeyBindings (); - keyBindings.Add (Key.A, Command.HotKey); - keyBindings.Add (Key.A, Command.Accept); + 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.DoesNotContain (Command.HotKey, resultCommands); + Assert.Contains (Command.HotKey, resultCommands); keyBindings = new (); keyBindings.Add (Key.A, KeyBindingScope.Focused, Command.HotKey); - keyBindings.Add (Key.A, KeyBindingScope.Focused, Command.Accept); + Assert.Throws (() => keyBindings.Add (Key.A, KeyBindingScope.Focused, Command.Accept)); resultCommands = keyBindings.GetCommands (Key.A); - Assert.DoesNotContain (Command.HotKey, resultCommands); + Assert.Contains (Command.HotKey, resultCommands); keyBindings = new (); keyBindings.Add (Key.A, KeyBindingScope.HotKey, Command.HotKey); - keyBindings.Add (Key.A, KeyBindingScope.Focused, Command.Accept); + Assert.Throws (() => keyBindings.Add (Key.A, KeyBindingScope.Focused, Command.Accept)); resultCommands = keyBindings.GetCommands (Key.A); - Assert.DoesNotContain (Command.HotKey, resultCommands); + Assert.Contains (Command.HotKey, resultCommands); keyBindings = new (); keyBindings.Add (Key.A, new KeyBinding (new [] { Command.HotKey }, KeyBindingScope.HotKey)); - keyBindings.Add (Key.A, new KeyBinding (new [] { Command.Accept }, KeyBindingScope.HotKey)); + Assert.Throws (() => keyBindings.Add (Key.A, new KeyBinding (new [] { Command.Accept }, KeyBindingScope.HotKey))); resultCommands = keyBindings.GetCommands (Key.A); - Assert.DoesNotContain (Command.HotKey, resultCommands); + Assert.Contains (Command.HotKey, resultCommands); } [Fact] public void Replace_Key () { var keyBindings = new KeyBindings (); - keyBindings.Add (Key.A, Command.HotKey); - keyBindings.Add (Key.B, Command.HotKey); - keyBindings.Add (Key.C, Command.HotKey); - keyBindings.Add (Key.D, Command.HotKey); + keyBindings.Add (Key.A, KeyBindingScope.Application, Command.HotKey); + keyBindings.Add (Key.B, KeyBindingScope.Application, Command.HotKey); + keyBindings.Add (Key.C, KeyBindingScope.Application, Command.HotKey); + keyBindings.Add (Key.D, KeyBindingScope.Application, Command.HotKey); - keyBindings.Replace (Key.A, Key.E); + keyBindings.ReplaceKey (Key.A, Key.E); Assert.Empty (keyBindings.GetCommands (Key.A)); Assert.Contains (Command.HotKey, keyBindings.GetCommands (Key.E)); - keyBindings.Replace (Key.B, Key.F); + keyBindings.ReplaceKey (Key.B, Key.F); Assert.Empty (keyBindings.GetCommands (Key.B)); Assert.Contains (Command.HotKey, keyBindings.GetCommands (Key.F)); - keyBindings.Replace (Key.C, Key.G); + keyBindings.ReplaceKey (Key.C, Key.G); Assert.Empty (keyBindings.GetCommands (Key.C)); Assert.Contains (Command.HotKey, keyBindings.GetCommands (Key.G)); - keyBindings.Replace (Key.D, Key.H); + keyBindings.ReplaceKey (Key.D, Key.H); Assert.Empty (keyBindings.GetCommands (Key.D)); Assert.Contains (Command.HotKey, keyBindings.GetCommands (Key.H)); } @@ -312,7 +312,7 @@ public class KeyBindingTests public void TryGet_WithCommands_ReturnsTrue () { var keyBindings = new KeyBindings (); - keyBindings.Add (Key.A, Command.HotKey); + keyBindings.Add (Key.A, KeyBindingScope.Application, Command.HotKey); bool result = keyBindings.TryGet (Key.A, out KeyBinding bindings); Assert.True (result); Assert.Contains (Command.HotKey, bindings.Commands); diff --git a/UnitTests/View/MouseTests.cs b/UnitTests/View/MouseTests.cs index a0bef94f5..56548aff4 100644 --- a/UnitTests/View/MouseTests.cs +++ b/UnitTests/View/MouseTests.cs @@ -92,193 +92,7 @@ public class MouseTests (ITestOutputHelper output) : TestsAllViews view.NewMouseEvent (new MouseEvent () { Flags = mouseFlags }); Assert.Equal (mouseFlagsFromEvent, expectedMouseFlagsFromEvent); } - - [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 () - { - X = 0, Y = 0, - Height = 1, - Width = 1, - CanFocus = true, - }; - - 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) - { - top.NewKeyDownEvent (Key.Tab.WithCtrl); - } - else if (view is DatePicker) - { - for (var i = 0; i < 4; i++) - { - top.NewKeyDownEvent (Key.Tab.WithCtrl); - } - } - else - { - top.NewKeyDownEvent (Key.Tab); - } - - Assert.Equal (1, nEnter); - Assert.Equal (1, nLeave); - - top.NewKeyDownEvent (Key.Tab); - - Assert.Equal (2, nEnter); - Assert.Equal (1, nLeave); - - top.Dispose (); - Application.Shutdown (); - } - - - [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) - { - top.NewKeyDownEvent (Key.Tab.WithCtrl); - } - else if (view is DatePicker) - { - for (var i = 0; i < 4; i++) - { - top.NewKeyDownEvent (Key.Tab.WithCtrl); - } - } - else - { - top.NewKeyDownEvent (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 NewMouseEvent_Invokes_MouseEvent_Properly () { diff --git a/UnitTests/View/NavigationTests.cs b/UnitTests/View/NavigationTests.cs index 5a7019c19..1933c1662 100644 --- a/UnitTests/View/NavigationTests.cs +++ b/UnitTests/View/NavigationTests.cs @@ -2,7 +2,7 @@ namespace Terminal.Gui.ViewTests; -public class NavigationTests (ITestOutputHelper output) +public class NavigationTests (ITestOutputHelper output) : TestsAllViews { [Fact] public void BringSubviewForward_Subviews_vs_TabIndexes () @@ -324,13 +324,13 @@ public class NavigationTests (ITestOutputHelper output) Assert.True (view2.CanFocus); Assert.False (view2.HasFocus); // Only one of the most focused toplevels view can have focus - Assert.True (top.NewKeyDownEvent (Key.Tab)); + Assert.True (Application.OnKeyDown (Key.Tab)); Assert.True (view1.CanFocus); Assert.False (view1.HasFocus); // Only one of the most focused toplevels view can have focus Assert.True (view2.CanFocus); Assert.True (view2.HasFocus); - Assert.True (top.NewKeyDownEvent (Key.Tab)); + Assert.True (Application.OnKeyDown (Key.Tab)); Assert.True (view1.CanFocus); Assert.True (view1.HasFocus); Assert.True (view2.CanFocus); @@ -365,13 +365,13 @@ public class NavigationTests (ITestOutputHelper output) Assert.True (view2.CanFocus); Assert.False (view2.HasFocus); // Only one of the most focused toplevels view can have focus - Assert.True (top.NewKeyDownEvent (Key.Tab.WithCtrl)); + Assert.True (Application.OnKeyDown (Key.Tab.WithCtrl)); Assert.True (view1.CanFocus); Assert.False (view1.HasFocus); // Only one of the most focused toplevels view can have focus Assert.True (view2.CanFocus); Assert.True (view2.HasFocus); - Assert.True (top.NewKeyDownEvent (Key.Tab.WithCtrl)); + Assert.True (Application.OnKeyDown (Key.Tab.WithCtrl)); Assert.True (view1.CanFocus); Assert.True (view1.HasFocus); Assert.True (view2.CanFocus); @@ -417,14 +417,14 @@ public class NavigationTests (ITestOutputHelper output) Assert.True (view2.CanFocus); Assert.False (view2.HasFocus); // Only one of the most focused toplevels view can have focus - Assert.True (top.NewKeyDownEvent (Key.Tab.WithCtrl)); - Assert.True (top.NewKeyDownEvent (Key.Tab.WithCtrl)); + Assert.True (Application.OnKeyDown (Key.Tab.WithCtrl)); + Assert.True (Application.OnKeyDown (Key.Tab.WithCtrl)); Assert.True (view1.CanFocus); Assert.False (view1.HasFocus); // Only one of the most focused toplevels view can have focus Assert.True (view2.CanFocus); Assert.True (view2.HasFocus); - Assert.True (top.NewKeyDownEvent (Key.Tab.WithCtrl)); + Assert.True (Application.OnKeyDown (Key.Tab.WithCtrl)); Assert.True (view1.CanFocus); Assert.True (view1.HasFocus); Assert.True (view2.CanFocus); @@ -530,6 +530,7 @@ public class NavigationTests (ITestOutputHelper output) } [Fact] + [AutoInitShutdown] public void FocusNearestView_Ensure_Focus_Ordered () { var top = new Toplevel (); @@ -544,16 +545,17 @@ public class NavigationTests (ITestOutputHelper output) frm.Add (frmSubview); top.Add (frm); - top.NewKeyDownEvent (Key.Tab); - Assert.Equal ("WindowSubview", top.MostFocused.Text); - top.NewKeyDownEvent (Key.Tab); - Assert.Equal ("FrameSubview", top.MostFocused.Text); - top.NewKeyDownEvent (Key.Tab); + Application.Begin (top); Assert.Equal ("WindowSubview", top.MostFocused.Text); - top.NewKeyDownEvent (Key.Tab.WithShift); + Application.OnKeyDown (Key.Tab); Assert.Equal ("FrameSubview", top.MostFocused.Text); - top.NewKeyDownEvent (Key.Tab.WithShift); + Application.OnKeyDown (Key.Tab); + Assert.Equal ("WindowSubview", top.MostFocused.Text); + + Application.OnKeyDown (Key.Tab.WithShift); + Assert.Equal ("FrameSubview", top.MostFocused.Text); + Application.OnKeyDown (Key.Tab.WithShift); Assert.Equal ("WindowSubview", top.MostFocused.Text); top.Dispose (); } @@ -605,7 +607,7 @@ public class NavigationTests (ITestOutputHelper output) Assert.False (removed); Assert.Null (view3); - Assert.True (top1.NewKeyDownEvent (Key.Tab.WithCtrl)); + Assert.True (Application.OnKeyDown (Key.Tab.WithCtrl)); Assert.True (top1.HasFocus); Assert.False (view1.HasFocus); Assert.True (view2.HasFocus); @@ -613,7 +615,7 @@ public class NavigationTests (ITestOutputHelper output) Assert.NotNull (view3); Exception exception = - Record.Exception (() => top1.NewKeyDownEvent (Key.Tab.WithCtrl)); + Record.Exception (() => Application.OnKeyDown (Key.Tab.WithCtrl)); Assert.Null (exception); Assert.True (removed); Assert.Null (view3); @@ -1582,4 +1584,191 @@ public class NavigationTests (ITestOutputHelper output) 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 () + { + X = 0, Y = 0, + Height = 1, + Width = 1, + CanFocus = true, + }; + + 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.Tab.WithCtrl); + } + else if (view is DatePicker) + { + for (var i = 0; i < 4; i++) + { + Application.OnKeyDown (Key.Tab.WithCtrl); + } + } + else + { + Application.OnKeyDown (Key.Tab); + } + + Assert.Equal (1, nEnter); + Assert.Equal (1, nLeave); + + Application.OnKeyDown (Key.Tab); + + Assert.Equal (2, nEnter); + Assert.Equal (1, nLeave); + + top.Dispose (); + Application.Shutdown (); + } + + + [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.Tab.WithCtrl); + } + else if (view is DatePicker) + { + for (var i = 0; i < 4; i++) + { + Application.OnKeyDown (Key.Tab.WithCtrl); + } + } + 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 (); + } } diff --git a/UnitTests/View/ViewKeyBindingTests.cs b/UnitTests/View/ViewKeyBindingTests.cs index d10d8a0c0..2ac278a7b 100644 --- a/UnitTests/View/ViewKeyBindingTests.cs +++ b/UnitTests/View/ViewKeyBindingTests.cs @@ -19,7 +19,8 @@ public class ViewKeyBindingTests (ITestOutputHelper output) Application.Begin (top); Application.OnKeyDown (Key.A); - Assert.True (invoked); + Assert.False (invoked); + Assert.True (view.ApplicationCommand); invoked = false; Application.OnKeyDown (Key.H); @@ -134,7 +135,7 @@ public class ViewKeyBindingTests (ITestOutputHelper output) AddCommand (Command.HotKey, () => HotKeyCommand = true); AddCommand (Command.Left, () => FocusedCommand = true); - KeyBindings.Add (Key.A, KeyBindingScope.Application, Command.Save); + Application.KeyBindings.Add (Key.A, this, Command.Save); HotKey = KeyCode.H; KeyBindings.Add (Key.F, KeyBindingScope.Focused, Command.Left); } diff --git a/UnitTests/Views/AllViewsTests.cs b/UnitTests/Views/AllViewsTests.cs index 92232d551..7b6945e7b 100644 --- a/UnitTests/Views/AllViewsTests.cs +++ b/UnitTests/Views/AllViewsTests.cs @@ -108,21 +108,21 @@ public class AllViewsTests (ITestOutputHelper output) : TestsAllViews if (vType is TextView) { - top.NewKeyDownEvent (Key.Tab.WithCtrl); + Application.OnKeyDown (Key.Tab.WithCtrl); } else if (vType is DatePicker) { for (var i = 0; i < 4; i++) { - top.NewKeyDownEvent (Key.Tab.WithCtrl); + Application.OnKeyDown (Key.Tab.WithCtrl); } } else { - top.NewKeyDownEvent (Key.Tab); + Application.OnKeyDown (Key.Tab); } - top.NewKeyDownEvent (Key.Tab); + Application.OnKeyDown (Key.Tab); Assert.Equal (2, vTypeEnter); Assert.Equal (1, vTypeLeave); diff --git a/UnitTests/Views/OverlappedTests.cs b/UnitTests/Views/OverlappedTests.cs index 5ca0fc566..2cf4fa96d 100644 --- a/UnitTests/Views/OverlappedTests.cs +++ b/UnitTests/Views/OverlappedTests.cs @@ -1030,28 +1030,30 @@ public class OverlappedTests var win1 = new Window { Id = "win1", Width = Dim.Percent (50), Height = Dim.Fill () }; var lblTf1W1 = new Label { Text = "Enter text in TextField on Win1:" }; - var tf1W1 = new TextField { X = Pos.Right (lblTf1W1) + 1, Width = Dim.Fill (), Text = "Text1 on Win1" }; + var tf1W1 = new TextField { Id="tf1W1", X = Pos.Right (lblTf1W1) + 1, Width = Dim.Fill (), Text = "Text1 on Win1" }; var lblTvW1 = new Label { Y = Pos.Bottom (lblTf1W1) + 1, Text = "Enter text in TextView on Win1:" }; var tvW1 = new TextView { + Id = "tvW1", X = Pos.Left (tf1W1), Width = Dim.Fill (), Height = 2, Text = "First line Win1\nSecond line Win1" }; var lblTf2W1 = new Label { Y = Pos.Bottom (lblTvW1) + 1, Text = "Enter text in TextField on Win1:" }; - var tf2W1 = new TextField { X = Pos.Left (tf1W1), Width = Dim.Fill (), Text = "Text2 on Win1" }; + var tf2W1 = new TextField { Id = "tf2W1", X = Pos.Left (tf1W1), Width = Dim.Fill (), Text = "Text2 on Win1" }; win1.Add (lblTf1W1, tf1W1, lblTvW1, tvW1, lblTf2W1, tf2W1); var win2 = new Window { Id = "win2", Width = Dim.Percent (50), Height = Dim.Fill () }; var lblTf1W2 = new Label { Text = "Enter text in TextField on Win2:" }; - var tf1W2 = new TextField { X = Pos.Right (lblTf1W2) + 1, Width = Dim.Fill (), Text = "Text1 on Win2" }; + var tf1W2 = new TextField { Id = "tf1W2", X = Pos.Right (lblTf1W2) + 1, Width = Dim.Fill (), Text = "Text1 on Win2" }; var lblTvW2 = new Label { Y = Pos.Bottom (lblTf1W2) + 1, Text = "Enter text in TextView on Win2:" }; var tvW2 = new TextView { + Id = "tvW2", X = Pos.Left (tf1W2), Width = Dim.Fill (), Height = 2, Text = "First line Win1\nSecond line Win2" }; var lblTf2W2 = new Label { Y = Pos.Bottom (lblTvW2) + 1, Text = "Enter text in TextField on Win2:" }; - var tf2W2 = new TextField { X = Pos.Left (tf1W2), Width = Dim.Fill (), Text = "Text2 on Win2" }; + var tf2W2 = new TextField { Id = "tf2W2", X = Pos.Left (tf1W2), Width = Dim.Fill (), Text = "Text2 on Win2" }; win2.Add (lblTf1W2, tf1W2, lblTvW2, tvW2, lblTf2W2, tf2W2); win1.Closing += (s, e) => isRunning = false; @@ -1104,72 +1106,69 @@ public class OverlappedTests Assert.True (Application.OnKeyDown (Key.Tab)); Assert.Equal ($"\tFirst line Win1{Environment.NewLine}Second line Win1", tvW1.Text); - Assert.True ( - Application.OnKeyDown (Key.Tab.WithShift) - ); + Assert.True (Application.OnKeyDown (Key.Tab.WithShift)); Assert.Equal ($"First line Win1{Environment.NewLine}Second line Win1", tvW1.Text); - Assert.True ( - Application.OnKeyDown (Key.Tab.WithCtrl) - ); + Assert.True (Application.OnKeyDown (Key.Tab.WithCtrl)); // move to win2 + Assert.Equal (win2, Application.OverlappedChildren [0]); + + Assert.True (Application.OnKeyDown (Key.Tab.WithCtrl.WithShift)); // move back to win1 Assert.Equal (win1, Application.OverlappedChildren [0]); - Assert.Equal (tf2W1, win1.MostFocused); - Assert.True (Application.OnKeyDown (Key.Tab)); - Assert.Equal (win1, Application.OverlappedChildren [0]); - Assert.Equal (tf1W1, win1.MostFocused); - Assert.True (Application.OnKeyDown (Key.CursorRight)); - Assert.Equal (win1, Application.OverlappedChildren [0]); - Assert.Equal (tf1W1, win1.MostFocused); - Assert.True (Application.OnKeyDown (Key.CursorDown)); + + Assert.Equal (tvW1, win1.MostFocused); + Assert.True (Application.OnKeyDown (Key.Tab)); // text view eats tab Assert.Equal (win1, Application.OverlappedChildren [0]); Assert.Equal (tvW1, win1.MostFocused); + + tvW1.AllowsTab = false; + Assert.True (Application.OnKeyDown (Key.Tab)); // text view eats tab + Assert.Equal (win1, Application.OverlappedChildren [0]); + Assert.Equal (tf2W1, win1.MostFocused); + + Assert.True (Application.OnKeyDown (Key.CursorRight)); + Assert.Equal (win1, Application.OverlappedChildren [0]); + Assert.Equal (tf2W1, win1.MostFocused); + Assert.True (Application.OnKeyDown (Key.CursorDown)); + Assert.Equal (win1, Application.OverlappedChildren [0]); + Assert.Equal (tf1W1, win1.MostFocused); #if UNIX_KEY_BINDINGS Assert.True (Application.OverlappedChildren [0].ProcessKeyDown (new (Key.I.WithCtrl))); Assert.Equal (win1, Application.OverlappedChildren [0]); Assert.Equal (tf2W1, win1.MostFocused); #endif - Assert.True ( - Application.OverlappedChildren [0] - .NewKeyDownEvent (Key.Tab.WithShift) - ); - Assert.Equal (win1, Application.OverlappedChildren [0]); - Assert.Equal (tvW1, win1.MostFocused); - Assert.True (Application.OnKeyDown (Key.CursorLeft)); - Assert.Equal (win1, Application.OverlappedChildren [0]); - Assert.Equal (tf1W1, win1.MostFocused); - Assert.True (Application.OnKeyDown (Key.CursorUp)); - Assert.Equal (win1, Application.OverlappedChildren [0]); - Assert.Equal (tf2W1, win1.MostFocused); Assert.True (Application.OnKeyDown (Key.Tab)); Assert.Equal (win1, Application.OverlappedChildren [0]); - Assert.Equal (tf1W1, win1.MostFocused); + Assert.Equal (tvW1, win1.MostFocused); + Assert.True (Application.OnKeyDown (Key.CursorLeft)); // The view to the left of tvW1 is tf2W1, but tvW1 is still focused and eats cursor keys + Assert.Equal (win1, Application.OverlappedChildren [0]); + Assert.Equal (tvW1, win1.MostFocused); + Assert.True (Application.OnKeyDown (Key.CursorUp)); + Assert.Equal (win1, Application.OverlappedChildren [0]); + Assert.Equal (tvW1, win1.MostFocused); + Assert.True (Application.OnKeyDown (Key.Tab)); + Assert.Equal (win1, Application.OverlappedChildren [0]); + Assert.Equal (tf2W1, win1.MostFocused); - Assert.True ( - Application.OverlappedChildren [0] - .NewKeyDownEvent (Key.Tab.WithCtrl) - ); + Assert.True (Application.OnKeyDown (Key.Tab.WithCtrl)); // Move to win2 Assert.Equal (win2, Application.OverlappedChildren [0]); Assert.Equal (tf1W2, win2.MostFocused); tf2W2.SetFocus (); Assert.True (tf2W2.HasFocus); - Assert.True ( - Application.OverlappedChildren [0] - .NewKeyDownEvent (Key.Tab.WithCtrl.WithShift) - ); + Assert.True (Application.OnKeyDown (Key.Tab.WithCtrl.WithShift)); Assert.Equal (win1, Application.OverlappedChildren [0]); - Assert.Equal (tf1W1, win1.MostFocused); + Assert.Equal (tf2W1, win1.MostFocused); Assert.True (Application.OnKeyDown (Application.AlternateForwardKey)); Assert.Equal (win2, Application.OverlappedChildren [0]); Assert.Equal (tf2W2, win2.MostFocused); Assert.True (Application.OnKeyDown (Application.AlternateBackwardKey)); Assert.Equal (win1, Application.OverlappedChildren [0]); - Assert.Equal (tf1W1, win1.MostFocused); + Assert.Equal (tf2W1, win1.MostFocused); Assert.True (Application.OnKeyDown (Key.CursorDown)); Assert.Equal (win1, Application.OverlappedChildren [0]); - Assert.Equal (tvW1, win1.MostFocused); + Assert.Equal (tf1W1, win1.MostFocused); #if UNIX_KEY_BINDINGS - Assert.True (Application.OverlappedChildren [0].ProcessKeyDown (new (Key.B.WithCtrl))); + Assert.True (Application.OnKeyDown (new (Key.B.WithCtrl))); #else Assert.True (Application.OnKeyDown (Key.CursorLeft)); #endif @@ -1180,20 +1179,17 @@ public class OverlappedTests Assert.Equal (tvW1, win1.MostFocused); Assert.Equal (Point.Empty, tvW1.CursorPosition); - Assert.True ( - Application.OverlappedChildren [0] - .NewKeyDownEvent (Key.End.WithCtrl) - ); + Assert.True (Application.OnKeyDown (Key.End.WithCtrl)); Assert.Equal (win1, Application.OverlappedChildren [0]); Assert.Equal (tvW1, win1.MostFocused); Assert.Equal (new (16, 1), tvW1.CursorPosition); #if UNIX_KEY_BINDINGS - Assert.True (Application.OverlappedChildren [0].ProcessKeyDown (new (Key.F.WithCtrl))); + Assert.True (Application.OnKeyDown (new (Key.F.WithCtrl))); #else Assert.True (Application.OnKeyDown (Key.CursorRight)); #endif Assert.Equal (win1, Application.OverlappedChildren [0]); - Assert.Equal (tf2W1, win1.MostFocused); + Assert.Equal (tvW1, win1.MostFocused); #if UNIX_KEY_BINDINGS Assert.True (Application.OverlappedChildren [0].ProcessKeyDown (new (Key.L.WithCtrl))); diff --git a/UnitTests/Views/TableViewTests.cs b/UnitTests/Views/TableViewTests.cs index 6740120f6..a87bb533e 100644 --- a/UnitTests/Views/TableViewTests.cs +++ b/UnitTests/Views/TableViewTests.cs @@ -2993,7 +2993,7 @@ A B C dt.Rows.Add (1, 2, 3, 4, 5, 6); tableView.MultiSelect = true; - tableView.KeyBindings.Add (Key.Space, Command.Select); + tableView.KeyBindings.ReplaceCommands (Key.Space, Command.Select); Point selectedCell = tableView.GetAllSelectedCells ().Single (); Assert.Equal (0, selectedCell.X); @@ -3065,7 +3065,7 @@ A B C dt.Rows.Add (1, 2, 3, 4, 5, 6); tableView.FullRowSelect = true; tableView.MultiSelect = true; - tableView.KeyBindings.Add (Key.Space, Command.Select); + tableView.KeyBindings.ReplaceCommands (Key.Space, Command.Select); // Toggle Select Cell 0,0 tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.Space }); @@ -3101,7 +3101,7 @@ A B C dt.Rows.Add (1, 2, 3, 4, 5, 6); dt.Rows.Add (1, 2, 3, 4, 5, 6); tableView.MultiSelect = true; - tableView.KeyBindings.Add (Key.Space, Command.Select); + tableView.KeyBindings.ReplaceCommands (Key.Space, Command.Select); // Make a square selection tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.ShiftMask | KeyCode.CursorDown }); @@ -3142,7 +3142,7 @@ A B C dt.Rows.Add (1, 2, 3, 4, 5, 6); dt.Rows.Add (1, 2, 3, 4, 5, 6); tableView.MultiSelect = true; - tableView.KeyBindings.Add (Key.Space, Command.Select); + tableView.KeyBindings.ReplaceCommands (Key.Space, Command.Select); // Make first square selection (0,0 to 1,1) tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.ShiftMask | KeyCode.CursorDown }); diff --git a/UnitTests/Views/ToplevelTests.cs b/UnitTests/Views/ToplevelTests.cs index c19c72bb1..7a5f9700c 100644 --- a/UnitTests/Views/ToplevelTests.cs +++ b/UnitTests/Views/ToplevelTests.cs @@ -485,27 +485,31 @@ public partial class ToplevelTests (ITestOutputHelper output) Assert.Equal ($"First line Win1{Environment.NewLine}Second line Win1", tvW1.Text); Assert.True (Application.OnKeyDown (Key.Tab.WithCtrl)); Assert.Equal (win1, top.Focused); - Assert.Equal (tf2W1, top.MostFocused); + Assert.Equal (tf2W1, top.MostFocused); // tf2W1 is last subview in win1 - tabbing should take us to first subview of win2 Assert.True (Application.OnKeyDown (Key.Tab)); - Assert.Equal (win1, top.Focused); - Assert.Equal (tf1W1, top.MostFocused); - Assert.True (Application.OnKeyDown (Key.CursorRight)); - Assert.Equal (win1, top.Focused); - Assert.Equal (tf1W1, top.MostFocused); - Assert.True (Application.OnKeyDown (Key.CursorDown)); - Assert.Equal (win1, top.Focused); - Assert.Equal (tvW1, top.MostFocused); + Assert.Equal (win2, top.Focused); + Assert.Equal (tf1W2, top.MostFocused); + Assert.True (Application.OnKeyDown (Key.CursorRight)); // move char to right in tf1W2 + Assert.Equal (win2, top.Focused); + Assert.Equal (tf1W2, top.MostFocused); + Assert.True (Application.OnKeyDown (Key.CursorDown)); // move down to next view (tvW2) + Assert.Equal (win2, top.Focused); + Assert.Equal (tvW2, top.MostFocused); #if UNIX_KEY_BINDINGS Assert.True (Application.OnKeyDown (new (Key.I.WithCtrl))); Assert.Equal (win1, top.Focused); Assert.Equal (tf2W1, top.MostFocused); #endif - Assert.True (Application.OnKeyDown (Key.Tab.WithShift)); - Assert.Equal (win1, top.Focused); - Assert.Equal (tvW1, top.MostFocused); + Assert.True (Application.OnKeyDown (Key.Tab.WithShift)); // Ignored. TextView eats shift-tab by default + Assert.Equal (win2, top.Focused); + Assert.Equal (tvW2, top.MostFocused); + tvW2.AllowsTab = false; + Assert.True (Application.OnKeyDown (Key.Tab.WithShift)); + Assert.Equal (win2, top.Focused); + Assert.Equal (tf1W2, top.MostFocused); Assert.True (Application.OnKeyDown (Key.CursorLeft)); - Assert.Equal (win1, top.Focused); - Assert.Equal (tf1W1, top.MostFocused); + Assert.Equal (win2, top.Focused); + Assert.Equal (tf1W2, top.MostFocused); Assert.True (Application.OnKeyDown (Key.CursorUp)); Assert.Equal (win1, top.Focused); Assert.Equal (tf2W1, top.MostFocused); diff --git a/UnitTests/Views/TreeTableSourceTests.cs b/UnitTests/Views/TreeTableSourceTests.cs index 39e18327b..a1a319b1b 100644 --- a/UnitTests/Views/TreeTableSourceTests.cs +++ b/UnitTests/Views/TreeTableSourceTests.cs @@ -187,7 +187,7 @@ public class TreeTableSourceTests : IDisposable Assert.Equal (0, tv.SelectedRow); Assert.Equal (1, tv.SelectedColumn); - top.NewKeyDownEvent (Key.CursorRight); + Application.OnKeyDown (Key.CursorRight); tv.Draw (); From 22dcbc1a782539e5ee423ef66e0bd9ee5ab6cf62 Mon Sep 17 00:00:00 2001 From: Tig Date: Wed, 24 Jul 2024 12:36:45 -0600 Subject: [PATCH 14/33] removed un needed key handling code from TextView --- .../Application/Application.Keyboard.cs | 12 ++-- Terminal.Gui/Views/TextView.cs | 58 +------------------ 2 files changed, 11 insertions(+), 59 deletions(-) diff --git a/Terminal.Gui/Application/Application.Keyboard.cs b/Terminal.Gui/Application/Application.Keyboard.cs index c725b13bd..84fade3cb 100644 --- a/Terminal.Gui/Application/Application.Keyboard.cs +++ b/Terminal.Gui/Application/Application.Keyboard.cs @@ -358,9 +358,10 @@ public static partial class Application // Keyboard handling ); AddCommand ( - Command.NextView, // TODO: Figure out how to move this to the View that is at the root of the view hierarchy (currently Application.Top) + Command.NextView, () => { + // TODO: Move this method to Application.Navigation.cs Current.MoveNextView (); return true; @@ -368,9 +369,10 @@ public static partial class Application // Keyboard handling ); AddCommand ( - Command.PreviousView,// TODO: Figure out how to move this to the View that is at the root of the view hierarchy (currently Application.Top) + Command.PreviousView, () => { + // TODO: Move this method to Application.Navigation.cs Current.MovePreviousView (); return true; @@ -378,9 +380,10 @@ public static partial class Application // Keyboard handling ); AddCommand ( - Command.NextViewOrTop,// TODO: Figure out how to move this to the View that is at the root of the view hierarchy (currently Application.Top) + Command.NextViewOrTop, () => { + // TODO: Move this method to Application.Navigation.cs Current.MoveNextViewOrTop (); return true; @@ -388,9 +391,10 @@ public static partial class Application // Keyboard handling ); AddCommand ( - Command.PreviousViewOrTop,// TODO: Figure out how to move this to the View that is at the root of the view hierarchy (currently Application.Top) + Command.PreviousViewOrTop, () => { + // TODO: Move this method to Application.Navigation.cs Current.MovePreviousViewOrTop (); return true; diff --git a/Terminal.Gui/Views/TextView.cs b/Terminal.Gui/Views/TextView.cs index 598f78961..db8627b47 100644 --- a/Terminal.Gui/Views/TextView.cs +++ b/Terminal.Gui/Views/TextView.cs @@ -2369,8 +2369,6 @@ public class TextView : View ); AddCommand (Command.Tab, () => ProcessTab ()); AddCommand (Command.BackTab, () => ProcessBackTab ()); - //AddCommand (Command.NextView, () => ProcessMoveNextView ()); - //AddCommand (Command.PreviousView, () => ProcessMovePreviousView ()); AddCommand ( Command.Undo, @@ -2503,12 +2501,6 @@ public class TextView : View KeyBindings.Add (Key.Tab, Command.Tab); KeyBindings.Add (Key.Tab.WithShift, Command.BackTab); - //KeyBindings.Add (Key.Tab.WithCtrl, Command.NextView); - //KeyBindings.Add (Application.AlternateForwardKey, Command.NextView); - - //KeyBindings.Add (Key.Tab.WithCtrl.WithShift, Command.PreviousView); - //KeyBindings.Add (Application.AlternateBackwardKey, Command.PreviousView); - KeyBindings.Add (Key.Z.WithCtrl, Command.Undo); KeyBindings.Add (Key.R.WithCtrl, Command.Redo); @@ -5365,16 +5357,6 @@ public class TextView : View DoNeededAction (); } - private bool MoveNextView () - { - if (Application.OverlappedTop is { }) - { - return SuperView?.FocusNext () == true; - } - - return false; - } - private void MovePageDown () { int nPageDnShift = Viewport.Height - 1; @@ -5431,16 +5413,6 @@ public class TextView : View DoNeededAction (); } - private bool MovePreviousView () - { - if (Application.OverlappedTop is { }) - { - return SuperView?.FocusPrev () == true; - } - - return false; - } - private void MoveRight () { List currentLine = GetCurrentLine (); @@ -5617,7 +5589,7 @@ public class TextView : View if (!AllowsTab || _isReadOnly) { - return ProcessMovePreviousView (); + return false; } if (CurrentColumn > 0) @@ -5889,21 +5861,7 @@ public class TextView : View StartSelecting (); MoveLeft (); } - - private bool ProcessMoveNextView () - { - ResetColumnTrack (); - - return MoveNextView (); - } - - private bool ProcessMovePreviousView () - { - ResetColumnTrack (); - - return MovePreviousView (); - } - + private bool ProcessMoveRight () { // if the user presses Right (without any control keys) @@ -6163,7 +6121,7 @@ public class TextView : View if (!AllowsTab || _isReadOnly) { - return ProcessMoveNextView (); + return false; } InsertText (new Key ((KeyCode)'\t')); @@ -6369,13 +6327,6 @@ public class TextView : View private void TextView_Initialized (object sender, EventArgs e) { Autocomplete.HostControl = this; - - if (Application.Top is { }) - { - Application.Top.AlternateForwardKeyChanged += Top_AlternateForwardKeyChanged!; - Application.Top.AlternateBackwardKeyChanged += Top_AlternateBackwardKeyChanged!; - } - OnContentsChanged (); } @@ -6393,9 +6344,6 @@ public class TextView : View _selectionStartRow = CurrentRow; } - private void Top_AlternateBackwardKeyChanged (object sender, KeyChangedEventArgs e) { KeyBindings.ReplaceKey (e.OldKey, e.NewKey); } - private void Top_AlternateForwardKeyChanged (object sender, KeyChangedEventArgs e) { KeyBindings.ReplaceKey (e.OldKey, e.NewKey); } - // Tries to snap the cursor to the tracking column private void TrackColumn () { From c088f2e98cfbad285a7291f9db5eec429ce87d84 Mon Sep 17 00:00:00 2001 From: Tig Date: Wed, 24 Jul 2024 12:38:59 -0600 Subject: [PATCH 15/33] removed unneeded key handling code from Toplevel --- Terminal.Gui/Views/Toplevel.cs | 125 --------------------------------- 1 file changed, 125 deletions(-) diff --git a/Terminal.Gui/Views/Toplevel.cs b/Terminal.Gui/Views/Toplevel.cs index fb31efb73..4c23b215f 100644 --- a/Terminal.Gui/Views/Toplevel.cs +++ b/Terminal.Gui/Views/Toplevel.cs @@ -31,15 +31,10 @@ public partial class Toplevel : View Arrangement = ViewArrangement.Fixed; Width = Dim.Fill (); Height = Dim.Fill (); - ColorScheme = Colors.ColorSchemes ["TopLevel"]; - - //ConfigureKeyBindings (); - MouseClick += Toplevel_MouseClick; } - #region Keyboard & Mouse // TODO: IRunnable: Re-implement - Modal means IRunnable, ViewArrangement.Overlapped where modalView.Z > allOtherViews.Max (v = v.Z), and exclusive key/mouse input. @@ -65,114 +60,6 @@ public partial class Toplevel : View /// public bool Modal { get; set; } - // TODO: Overlapped: Figure out how these keybindings should work. - private void ConfigureKeyBindings () - { - // Things this view knows how to do - AddCommand ( - Command.QuitToplevel, // TODO: IRunnable: Rename to Command.Quit to make more generic. - () => - { - QuitToplevel (); - - return true; - } - ); - - /// TODO: Overlapped: Add Command.ShowHide - - AddCommand ( - Command.Suspend, // TODO: Move to Application - () => - { - Driver.Suspend (); - ; - - return true; - } - ); - - AddCommand ( - Command.NextView, // TODO: Figure out how to move this to the View that is at the root of the view hierarchy (currently Application.Top) - () => - { - MoveNextView (); - - return true; - } - ); - - AddCommand ( - Command.PreviousView,// TODO: Figure out how to move this to the View that is at the root of the view hierarchy (currently Application.Top) - () => - { - MovePreviousView (); - - return true; - } - ); - - AddCommand ( - Command.NextViewOrTop,// TODO: Figure out how to move this to the View that is at the root of the view hierarchy (currently Application.Top) - () => - { - MoveNextViewOrTop (); - - return true; - } - ); - - AddCommand ( - Command.PreviousViewOrTop,// TODO: Figure out how to move this to the View that is at the root of the view hierarchy (currently Application.Top) - () => - { - MovePreviousViewOrTop (); - - return true; - } - ); - - AddCommand ( - Command.Refresh, - () => - { - Application.Refresh (); // TODO: Move to Application - - return true; - } - ); - - // Default keybindings for this view - KeyBindings.Add (Application.QuitKey, Command.QuitToplevel); - - KeyBindings.Add (Key.CursorRight, Command.NextView); - KeyBindings.Add (Key.CursorDown, Command.NextView); - KeyBindings.Add (Key.CursorLeft, Command.PreviousView); - KeyBindings.Add (Key.CursorUp, Command.PreviousView); - - KeyBindings.Add (Key.Tab, Command.NextView); - KeyBindings.Add (Key.Tab.WithShift, Command.PreviousView); - KeyBindings.Add (Key.Tab.WithCtrl, Command.NextViewOrTop); - KeyBindings.Add (Key.Tab.WithShift.WithCtrl, Command.PreviousViewOrTop); - - // TODO: Refresh Key should be configurable - KeyBindings.Add (Key.F5, KeyBindingScope.Application, Command.Refresh); - KeyBindings.Add (Application.AlternateForwardKey, Command.NextViewOrTop); // Needed on Unix - KeyBindings.Add (Application.AlternateBackwardKey, Command.PreviousViewOrTop); // Needed on Unix - - if (Environment.OSVersion.Platform == PlatformID.Unix) - { - KeyBindings.Add (Key.Z.WithCtrl, Command.Suspend); - } - -#if UNIX_KEY_BINDINGS - KeyBindings.Add (Key.L.WithCtrl, Command.Refresh); // Unix - KeyBindings.Add (Key.F.WithCtrl, Command.NextView); // Unix - KeyBindings.Add (Key.I.WithCtrl, Command.NextView); // Unix - KeyBindings.Add (Key.B.WithCtrl, Command.PreviousView); // Unix -#endif - } - private void Toplevel_MouseClick (object sender, MouseEventEventArgs e) { e.Handled = InvokeCommand (Command.HotKey) == true; } // TODO: Deprecate - No need for this at View level; having at Application is sufficient. @@ -500,18 +387,6 @@ public partial class Toplevel : View Unloaded?.Invoke (this, EventArgs.Empty); } - private void QuitToplevel () - { - if (Application.OverlappedTop is { }) - { - RequestStop (this); - } - else - { - Application.RequestStop (); - } - } - #endregion #region Draw From 4a56b84324a67fb990aa781ac82212c1574597e2 Mon Sep 17 00:00:00 2001 From: Tig Date: Wed, 24 Jul 2024 12:41:28 -0600 Subject: [PATCH 16/33] removed unneeded AlternateBack/FormardKey code from Toplevel --- .../Application/Application.Keyboard.cs | 32 -------- Terminal.Gui/Views/Toplevel.cs | 37 --------- UnitTests/Views/ToplevelTests.cs | 76 +------------------ 3 files changed, 1 insertion(+), 144 deletions(-) diff --git a/Terminal.Gui/Application/Application.Keyboard.cs b/Terminal.Gui/Application/Application.Keyboard.cs index 84fade3cb..c05ba6c34 100644 --- a/Terminal.Gui/Application/Application.Keyboard.cs +++ b/Terminal.Gui/Application/Application.Keyboard.cs @@ -29,20 +29,10 @@ public static partial class Application // Keyboard handling { KeyBindings.ReplaceKey (oldKey, _alternateForwardKey); } - OnAlternateForwardKeyChanged (new (oldKey, value)); } } } - private static void OnAlternateForwardKeyChanged (KeyChangedEventArgs e) - { - // TODO: The fact Top has it's own AlternateForwardKey and events is needlessly complex. Remove it. - foreach (Toplevel top in _topLevels.ToArray ()) - { - top.OnAlternateForwardKeyChanged (e); - } - } - private static Key _alternateBackwardKey = Key.Empty; // Defined in config.json /// Alternative key to navigate backwards through views. Shift+Ctrl+Tab is the primary key. @@ -66,21 +56,10 @@ public static partial class Application // Keyboard handling { KeyBindings.ReplaceKey (oldKey, _alternateBackwardKey); } - - OnAlternateBackwardKeyChanged (new (oldKey, value)); } } } - private static void OnAlternateBackwardKeyChanged (KeyChangedEventArgs oldKey) - { - // TODO: The fact Top has it's own AlternateBackwardKey and events is needlessly complex. Remove it. - foreach (Toplevel top in _topLevels.ToArray ()) - { - top.OnAlternateBackwardKeyChanged (oldKey); - } - } - private static Key _quitKey = Key.Empty; // Defined in config.json /// Gets or sets the key to quit the application. @@ -103,21 +82,10 @@ public static partial class Application // Keyboard handling { KeyBindings.ReplaceKey (oldKey, _quitKey); } - OnQuitKeyChanged (new (oldKey, value)); } } } - private static void OnQuitKeyChanged (KeyChangedEventArgs e) - { - // TODO: The fact Top has it's own QuitKey and events is needlessly complex. Remove it. - // Duplicate the list so if it changes during enumeration we're safe - foreach (Toplevel top in _topLevels.ToArray ()) - { - top.OnQuitKeyChanged (e); - } - } - /// /// Event fired when the user presses a key. Fired by . /// diff --git a/Terminal.Gui/Views/Toplevel.cs b/Terminal.Gui/Views/Toplevel.cs index 4c23b215f..3a862bd69 100644 --- a/Terminal.Gui/Views/Toplevel.cs +++ b/Terminal.Gui/Views/Toplevel.cs @@ -62,43 +62,6 @@ public partial class Toplevel : View private void Toplevel_MouseClick (object sender, MouseEventEventArgs e) { e.Handled = InvokeCommand (Command.HotKey) == true; } - // TODO: Deprecate - No need for this at View level; having at Application is sufficient. - /// Invoked when the is changed. - public event EventHandler AlternateBackwardKeyChanged; - - // TODO: Deprecate - No need for this at View level; having at Application is sufficient. - /// Invoked when the is changed. - public event EventHandler AlternateForwardKeyChanged; - - // TODO: Deprecate - No need for this at View level; having at Application is sufficient. - /// Virtual method to invoke the event. - /// - public virtual void OnAlternateBackwardKeyChanged (KeyChangedEventArgs e) - { - KeyBindings.ReplaceKey (e.OldKey, e.NewKey); - AlternateBackwardKeyChanged?.Invoke (this, e); - } - - // TODO: Deprecate - No need for this at View level; having at Application is sufficient. - /// Virtual method to invoke the event. - /// - public virtual void OnAlternateForwardKeyChanged (KeyChangedEventArgs e) - { - KeyBindings.ReplaceKey (e.OldKey, e.NewKey); - AlternateForwardKeyChanged?.Invoke (this, e); - } - - /// Virtual method to invoke the event. - /// - public virtual void OnQuitKeyChanged (KeyChangedEventArgs e) - { - KeyBindings.ReplaceKey (e.OldKey, e.NewKey); - QuitKeyChanged?.Invoke (this, e); - } - - /// Invoked when the is changed. - public event EventHandler QuitKeyChanged; - #endregion #region Subviews diff --git a/UnitTests/Views/ToplevelTests.cs b/UnitTests/Views/ToplevelTests.cs index 7a5f9700c..2d4a0a323 100644 --- a/UnitTests/Views/ToplevelTests.cs +++ b/UnitTests/Views/ToplevelTests.cs @@ -573,23 +573,6 @@ public partial class ToplevelTests (ITestOutputHelper output) void View_Added (object sender, SuperViewChangedEventArgs e) { - Assert.Throws ( - () => - Application.Top.AlternateForwardKeyChanged += - (s, e) => alternateForwardKey = (KeyCode)e.OldKey - ); - - Assert.Throws ( - () => - Application.Top.AlternateBackwardKeyChanged += - (s, e) => alternateBackwardKey = (KeyCode)e.OldKey - ); - - Assert.Throws ( - () => - Application.Top.QuitKeyChanged += (s, e) => - quitKey = (KeyCode)e.OldKey - ); Assert.False (wasAdded); wasAdded = true; view.Added -= View_Added; @@ -605,64 +588,7 @@ public partial class ToplevelTests (ITestOutputHelper output) Application.Shutdown (); } - - [Fact] - [AutoInitShutdown] - public void AlternateForwardKeyChanged_AlternateBackwardKeyChanged_QuitKeyChanged_Events () - { - Key alternateForwardKey = KeyCode.Null; - Key alternateBackwardKey = KeyCode.Null; - Key quitKey = KeyCode.Null; - - Key previousQuitKey = Application.QuitKey; - - Toplevel top = new (); - var view = new View (); - view.Initialized += View_Initialized; - - void View_Initialized (object sender, EventArgs e) - { - top.AlternateForwardKeyChanged += (s, e) => alternateForwardKey = e.OldKey; - top.AlternateBackwardKeyChanged += (s, e) => alternateBackwardKey = e.OldKey; - top.QuitKeyChanged += (s, e) => quitKey = e.OldKey; - } - - var win = new Window (); - win.Add (view); - top.Add (win); - Application.Begin (top); - - Assert.Equal (KeyCode.Null, alternateForwardKey); - Assert.Equal (KeyCode.Null, alternateBackwardKey); - Assert.Equal (KeyCode.Null, quitKey); - - Assert.Equal (KeyCode.PageDown | KeyCode.CtrlMask, Application.AlternateForwardKey); - Assert.Equal (KeyCode.PageUp | KeyCode.CtrlMask, Application.AlternateBackwardKey); - Assert.Equal (Key.Esc, Application.QuitKey); - - Application.AlternateForwardKey = KeyCode.A; - Application.AlternateBackwardKey = KeyCode.B; - Application.QuitKey = KeyCode.C; - - Assert.Equal (KeyCode.PageDown | KeyCode.CtrlMask, alternateForwardKey); - Assert.Equal (KeyCode.PageUp | KeyCode.CtrlMask, alternateBackwardKey); - Assert.Equal (previousQuitKey, quitKey); - - Assert.Equal (KeyCode.A, Application.AlternateForwardKey); - Assert.Equal (KeyCode.B, Application.AlternateBackwardKey); - Assert.Equal (KeyCode.C, Application.QuitKey); - - // Replacing the defaults keys to avoid errors on others unit tests that are using it. - Application.AlternateForwardKey = Key.PageDown.WithCtrl; - Application.AlternateBackwardKey = Key.PageUp.WithCtrl; - Application.QuitKey = previousQuitKey; - - Assert.Equal (KeyCode.PageDown | KeyCode.CtrlMask, Application.AlternateForwardKey); - Assert.Equal (KeyCode.PageUp | KeyCode.CtrlMask, Application.AlternateBackwardKey); - Assert.Equal (previousQuitKey, Application.QuitKey); - top.Dispose (); - } - + [Fact] [AutoInitShutdown] public void Mouse_Drag_On_Top_With_Superview_Null () From 0c56dfeb5a5d2e995291a717f4ee451552736be4 Mon Sep 17 00:00:00 2001 From: Tig Date: Wed, 24 Jul 2024 12:56:55 -0600 Subject: [PATCH 17/33] Moved view navigation out of Toplevel and into Application (via ViewNavigation static class). --- .../Application/Application.Keyboard.cs | 8 +- .../Application/Application.Overlapped.cs | 163 ++++++++++++++++++ Terminal.Gui/Input/KeyBindings.cs | 19 ++ Terminal.Gui/Views/Toplevel.cs | 160 +---------------- 4 files changed, 187 insertions(+), 163 deletions(-) diff --git a/Terminal.Gui/Application/Application.Keyboard.cs b/Terminal.Gui/Application/Application.Keyboard.cs index c05ba6c34..bc6507c86 100644 --- a/Terminal.Gui/Application/Application.Keyboard.cs +++ b/Terminal.Gui/Application/Application.Keyboard.cs @@ -330,7 +330,7 @@ public static partial class Application // Keyboard handling () => { // TODO: Move this method to Application.Navigation.cs - Current.MoveNextView (); + ViewNavigation.MoveNextView (); return true; } @@ -341,7 +341,7 @@ public static partial class Application // Keyboard handling () => { // TODO: Move this method to Application.Navigation.cs - Current.MovePreviousView (); + ViewNavigation.MovePreviousView (); return true; } @@ -352,7 +352,7 @@ public static partial class Application // Keyboard handling () => { // TODO: Move this method to Application.Navigation.cs - Current.MoveNextViewOrTop (); + ViewNavigation.MoveNextViewOrTop (); return true; } @@ -363,7 +363,7 @@ public static partial class Application // Keyboard handling () => { // TODO: Move this method to Application.Navigation.cs - Current.MovePreviousViewOrTop (); + ViewNavigation.MovePreviousViewOrTop (); return true; } diff --git a/Terminal.Gui/Application/Application.Overlapped.cs b/Terminal.Gui/Application/Application.Overlapped.cs index 58eccfa3a..1ac547fde 100644 --- a/Terminal.Gui/Application/Application.Overlapped.cs +++ b/Terminal.Gui/Application/Application.Overlapped.cs @@ -1,6 +1,169 @@ #nullable enable +using static Terminal.Gui.View; +using System.Reflection; + namespace Terminal.Gui; +internal static class ViewNavigation +{ + /// + /// Gets the deepest focused subview of the specified . + /// + /// + /// + internal static View GetDeepestFocusedSubview (View view) + { + if (view is null) + { + return null; + } + + foreach (View v in view.Subviews) + { + if (v.HasFocus) + { + return GetDeepestFocusedSubview (v); + } + } + + return view; + } + + /// + /// Sets the focus to the next view in the list. If the last view is focused, the first view is focused. + /// + /// + /// + internal static void FocusNearestView (IEnumerable? viewsInTabIndexes, NavigationDirection direction) + { + if (viewsInTabIndexes is null) + { + return; + } + + var found = false; + var focusProcessed = false; + var idx = 0; + + foreach (View v in viewsInTabIndexes) + { + if (v == Application.Current) + { + found = true; + } + + if (found && v != Application.Current) + { + if (direction == NavigationDirection.Forward) + { + Application.Current.SuperView?.FocusNext (); + } + else + { + Application.Current.SuperView?.FocusPrev (); + } + + focusProcessed = true; + + if (Application.Current.SuperView?.Focused is { } && Application.Current.SuperView.Focused != Application.Current) + { + return; + } + } + else if (found && !focusProcessed && idx == viewsInTabIndexes.Count () - 1) + { + viewsInTabIndexes.ToList () [0].SetFocus (); + } + + idx++; + } + } + /// + /// Moves the focus to + /// + internal static void MoveNextView () + { + View old = GetDeepestFocusedSubview (Application.Current.Focused); + + if (!Application.Current.FocusNext ()) + { + Application.Current.FocusNext (); + } + + if (old != Application.Current.Focused && old != Application.Current.Focused?.Focused) + { + old?.SetNeedsDisplay (); + Application.Current.Focused?.SetNeedsDisplay (); + } + else + { + FocusNearestView (Application.Current.SuperView?.TabIndexes, NavigationDirection.Forward); + } + } + + internal static void MoveNextViewOrTop () + { + if (Application.OverlappedTop is null) + { + Toplevel top = Application.Current.Modal ? Application.Current : Application.Top; + top.FocusNext (); + + if (top.Focused is null) + { + top.FocusNext (); + } + + top.SetNeedsDisplay (); + Application.BringOverlappedTopToFront (); + } + else + { + Application.OverlappedMoveNext (); + } + } + + internal static void MovePreviousView () + { + View old = GetDeepestFocusedSubview (Application.Current.Focused); + + if (!Application.Current.FocusPrev ()) + { + Application.Current.FocusPrev (); + } + + if (old != Application.Current.Focused && old != Application.Current.Focused?.Focused) + { + old?.SetNeedsDisplay (); + Application.Current.Focused?.SetNeedsDisplay (); + } + else + { + FocusNearestView (Application.Current.SuperView?.TabIndexes?.Reverse (), NavigationDirection.Backward); + } + } + + internal static void MovePreviousViewOrTop () + { + if (Application.OverlappedTop is null) + { + Toplevel top = Application.Current.Modal ? Application.Current : Application.Top; + top.FocusPrev (); + + if (top.Focused is null) + { + top.FocusPrev (); + } + + top.SetNeedsDisplay (); + Application.BringOverlappedTopToFront (); + } + else + { + Application.OverlappedMovePrevious (); + } + } +} + public static partial class Application // App-level View Navigation { diff --git a/Terminal.Gui/Input/KeyBindings.cs b/Terminal.Gui/Input/KeyBindings.cs index 8ca45568e..89ffde7ad 100644 --- a/Terminal.Gui/Input/KeyBindings.cs +++ b/Terminal.Gui/Input/KeyBindings.cs @@ -110,6 +110,25 @@ public class KeyBindings } } + + /// + /// Adds a new key combination that will trigger the commands in . + /// + /// If the key is already bound to a different array of s it will be rebound + /// . + /// + /// + /// + /// Commands are only ever applied to the current (i.e. this feature cannot be used to switch + /// focus to another view and perform multiple commands there). + /// + /// The key to check. + /// The scope for the command. + /// + /// The command to invoked on the when is pressed. When + /// 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) { if (BoundView is { } && scope.FastHasFlags (KeyBindingScope.Application)) diff --git a/Terminal.Gui/Views/Toplevel.cs b/Terminal.Gui/Views/Toplevel.cs index 3a862bd69..11d216210 100644 --- a/Terminal.Gui/Views/Toplevel.cs +++ b/Terminal.Gui/Views/Toplevel.cs @@ -411,165 +411,7 @@ public partial class Toplevel : View /// public override bool OnLeave (View view) { return MostFocused?.OnLeave (view) ?? base.OnLeave (view); } - - /// - /// Sets the focus to the next view in the list. If the last view is focused, the first view is focused. - /// - /// - /// - private void FocusNearestView (IEnumerable viewsInTabIndexes, NavigationDirection direction) - { - if (viewsInTabIndexes is null) - { - return; - } - - var found = false; - var focusProcessed = false; - var idx = 0; - - foreach (View v in viewsInTabIndexes) - { - if (v == this) - { - found = true; - } - - if (found && v != this) - { - if (direction == NavigationDirection.Forward) - { - SuperView?.FocusNext (); - } - else - { - SuperView?.FocusPrev (); - } - - focusProcessed = true; - - if (SuperView.Focused is { } && SuperView.Focused != this) - { - return; - } - } - else if (found && !focusProcessed && idx == viewsInTabIndexes.Count () - 1) - { - viewsInTabIndexes.ToList () [0].SetFocus (); - } - - idx++; - } - } - - /// - /// Gets the deepest focused subview of the specified . - /// - /// - /// - private View GetDeepestFocusedSubview (View view) - { - if (view is null) - { - return null; - } - - foreach (View v in view.Subviews) - { - if (v.HasFocus) - { - return GetDeepestFocusedSubview (v); - } - } - - return view; - } - - /// - /// Moves the focus to - /// - internal void MoveNextView () - { - View old = GetDeepestFocusedSubview (Focused); - - if (!FocusNext ()) - { - FocusNext (); - } - - if (old != Focused && old != Focused?.Focused) - { - old?.SetNeedsDisplay (); - Focused?.SetNeedsDisplay (); - } - else - { - FocusNearestView (SuperView?.TabIndexes, NavigationDirection.Forward); - } - } - - internal void MoveNextViewOrTop () - { - if (Application.OverlappedTop is null) - { - Toplevel top = Modal ? this : Application.Top; - top.FocusNext (); - - if (top.Focused is null) - { - top.FocusNext (); - } - - top.SetNeedsDisplay (); - Application.BringOverlappedTopToFront (); - } - else - { - Application.OverlappedMoveNext (); - } - } - - internal void MovePreviousView () - { - View old = GetDeepestFocusedSubview (Focused); - - if (!FocusPrev ()) - { - FocusPrev (); - } - - if (old != Focused && old != Focused?.Focused) - { - old?.SetNeedsDisplay (); - Focused?.SetNeedsDisplay (); - } - else - { - FocusNearestView (SuperView?.TabIndexes?.Reverse (), NavigationDirection.Backward); - } - } - - internal void MovePreviousViewOrTop () - { - if (Application.OverlappedTop is null) - { - Toplevel top = Modal ? this : Application.Top; - top.FocusPrev (); - - if (top.Focused is null) - { - top.FocusPrev (); - } - - top.SetNeedsDisplay (); - Application.BringOverlappedTopToFront (); - } - else - { - Application.OverlappedMovePrevious (); - } - } - + #endregion #region Size / Position Management From 73a9dc37c48587e4d042ada19ece2296b17b0fe5 Mon Sep 17 00:00:00 2001 From: Tig Date: Wed, 24 Jul 2024 14:15:32 -0600 Subject: [PATCH 18/33] Fixed nullable warnings 2 --- .../Application/Application.Initialization.cs | 32 ++-- .../Application/Application.Keyboard.cs | 6 +- Terminal.Gui/Application/Application.Mouse.cs | 14 +- .../Application/Application.Overlapped.cs | 20 +-- Terminal.Gui/Application/Application.Run.cs | 74 +++++---- .../Application/Application.Toplevel.cs | 6 +- Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs | 5 +- Terminal.Gui/View/Adornment/Margin.cs | 4 +- Terminal.Gui/View/Adornment/ShadowView.cs | 2 +- Terminal.Gui/View/Layout/Dim.cs | 2 +- Terminal.Gui/View/Layout/DimView.cs | 2 +- Terminal.Gui/View/Layout/Pos.cs | 4 +- Terminal.Gui/View/Layout/PosView.cs | 2 +- Terminal.Gui/Views/Menu/MenuBar.cs | 2 +- UICatalog/UICatalog.cs | 2 +- UnitTests/Views/MenuBarTests.cs | 142 ++++++++---------- UnitTests/Views/ToplevelTests.cs | 9 +- 17 files changed, 154 insertions(+), 174 deletions(-) diff --git a/Terminal.Gui/Application/Application.Initialization.cs b/Terminal.Gui/Application/Application.Initialization.cs index 2e184b285..0d9b2caf5 100644 --- a/Terminal.Gui/Application/Application.Initialization.cs +++ b/Terminal.Gui/Application/Application.Initialization.cs @@ -36,7 +36,7 @@ public static partial class Application // Initialization (Init/Shutdown) /// [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] - public static void Init (ConsoleDriver driver = null, string driverName = null) { InternalInit (driver, driverName); } + public static void Init (ConsoleDriver? driver = null, string? driverName = null) { InternalInit (driver, driverName); } internal static bool _initialized; internal static int _mainThreadId = -1; @@ -53,8 +53,8 @@ public static partial class Application // Initialization (Init/Shutdown) [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] internal static void InternalInit ( - ConsoleDriver driver = null, - string driverName = null, + ConsoleDriver? driver = null, + string? driverName = null, bool calledViaRunT = false ) { @@ -114,17 +114,17 @@ public static partial class Application // Initialization (Init/Shutdown) } else { - List drivers = GetDriverTypes (); - Type driverType = drivers.FirstOrDefault (t => t.Name.Equals (ForceDriver, StringComparison.InvariantCultureIgnoreCase)); + List drivers = GetDriverTypes (); + Type? driverType = drivers.FirstOrDefault (t => t!.Name.Equals (ForceDriver, StringComparison.InvariantCultureIgnoreCase)); if (driverType is { }) { - Driver = (ConsoleDriver)Activator.CreateInstance (driverType); + Driver = (ConsoleDriver)Activator.CreateInstance (driverType)!; } else { throw new ArgumentException ( - $"Invalid driver name: {ForceDriver}. Valid names are {string.Join (", ", drivers.Select (t => t.Name))}" + $"Invalid driver name: {ForceDriver}. Valid names are {string.Join (", ", drivers.Select (t => t!.Name))}" ); } } @@ -132,7 +132,7 @@ public static partial class Application // Initialization (Init/Shutdown) try { - MainLoop = Driver.Init (); + MainLoop = Driver!.Init (); } catch (InvalidOperationException ex) { @@ -159,22 +159,22 @@ public static partial class Application // Initialization (Init/Shutdown) InitializedChanged?.Invoke (null, new (in _initialized)); } - private static void Driver_SizeChanged (object sender, SizeChangedEventArgs e) { OnSizeChanging (e); } - private static void Driver_KeyDown (object sender, Key e) { OnKeyDown (e); } - private static void Driver_KeyUp (object sender, Key e) { OnKeyUp (e); } - private static void Driver_MouseEvent (object sender, MouseEvent e) { OnMouseEvent (e); } + private static void Driver_SizeChanged (object? sender, SizeChangedEventArgs e) { OnSizeChanging (e); } + private static void Driver_KeyDown (object? sender, Key e) { OnKeyDown (e); } + private static void Driver_KeyUp (object? sender, Key e) { OnKeyUp (e); } + private static void Driver_MouseEvent (object? sender, MouseEvent e) { OnMouseEvent (e); } /// Gets of list of types that are available. /// [RequiresUnreferencedCode ("AOT")] - public static List GetDriverTypes () + public static List GetDriverTypes () { // use reflection to get the list of drivers - List driverTypes = new (); + List driverTypes = new (); foreach (Assembly asm in AppDomain.CurrentDomain.GetAssemblies ()) { - foreach (Type type in asm.GetTypes ()) + foreach (Type? type in asm.GetTypes ()) { if (type.IsSubclassOf (typeof (ConsoleDriver)) && !type.IsAbstract) { @@ -207,5 +207,5 @@ public static partial class Application // Initialization (Init/Shutdown) /// /// Intended to support unit tests that need to know when the application has been initialized. /// - public static event EventHandler> InitializedChanged; + public static event EventHandler>? InitializedChanged; } diff --git a/Terminal.Gui/Application/Application.Keyboard.cs b/Terminal.Gui/Application/Application.Keyboard.cs index bc6507c86..10419bf80 100644 --- a/Terminal.Gui/Application/Application.Keyboard.cs +++ b/Terminal.Gui/Application/Application.Keyboard.cs @@ -98,7 +98,7 @@ public static partial class Application // Keyboard handling /// and events. /// Fired after and before . /// - public static event EventHandler KeyDown; + public static event EventHandler? KeyDown; /// /// Called by the when the user presses a key. Fires the event @@ -199,7 +199,7 @@ public static partial class Application // Keyboard handling /// and events. /// Fired after . /// - public static event EventHandler KeyUp; + public static event EventHandler? KeyUp; /// /// Called by the when the user releases a key. Fires the event @@ -304,7 +304,7 @@ public static partial class Application // Keyboard handling { if (OverlappedTop is { }) { - RequestStop (Current); + RequestStop (Current!); } else { diff --git a/Terminal.Gui/Application/Application.Mouse.cs b/Terminal.Gui/Application/Application.Mouse.cs index 61fc6d63e..713e6375d 100644 --- a/Terminal.Gui/Application/Application.Mouse.cs +++ b/Terminal.Gui/Application/Application.Mouse.cs @@ -9,32 +9,32 @@ public static partial class Application // Mouse handling public static bool IsMouseDisabled { get; set; } /// The current object that wants continuous mouse button pressed events. - public static View WantContinuousButtonPressedView { get; private set; } + public static View? WantContinuousButtonPressedView { get; private set; } /// /// Gets the view that grabbed the mouse (e.g. for dragging). When this is set, all mouse events will be routed to /// this view until the view calls or the mouse is released. /// - public static View MouseGrabView { get; private set; } + public static View? MouseGrabView { get; private set; } /// Invoked when a view wants to grab the mouse; can be canceled. - public static event EventHandler GrabbingMouse; + public static event EventHandler? GrabbingMouse; /// Invoked when a view wants un-grab the mouse; can be canceled. - public static event EventHandler UnGrabbingMouse; + public static event EventHandler? UnGrabbingMouse; /// Invoked after a view has grabbed the mouse. - public static event EventHandler GrabbedMouse; + public static event EventHandler? GrabbedMouse; /// Invoked after a view has un-grabbed the mouse. - public static event EventHandler UnGrabbedMouse; + public static event EventHandler? UnGrabbedMouse; /// /// Grabs the mouse, forcing all mouse events to be routed to the specified view until /// is called. /// /// View that will receive all mouse events until is invoked. - public static void GrabMouse (View view) + public static void GrabMouse (View? view) { if (view is null) { diff --git a/Terminal.Gui/Application/Application.Overlapped.cs b/Terminal.Gui/Application/Application.Overlapped.cs index 1ac547fde..26e931c98 100644 --- a/Terminal.Gui/Application/Application.Overlapped.cs +++ b/Terminal.Gui/Application/Application.Overlapped.cs @@ -11,7 +11,7 @@ internal static class ViewNavigation /// /// /// - internal static View GetDeepestFocusedSubview (View view) + internal static View? GetDeepestFocusedSubview (View? view) { if (view is null) { @@ -30,7 +30,7 @@ internal static class ViewNavigation } /// - /// Sets the focus to the next view in the list. If the last view is focused, the first view is focused. + /// Sets the focus to the next view in the list. If the last view is focused, the first view is focused. /// /// /// @@ -56,11 +56,11 @@ internal static class ViewNavigation { if (direction == NavigationDirection.Forward) { - Application.Current.SuperView?.FocusNext (); + Application.Current!.SuperView?.FocusNext (); } else { - Application.Current.SuperView?.FocusPrev (); + Application.Current!.SuperView?.FocusPrev (); } focusProcessed = true; @@ -83,7 +83,7 @@ internal static class ViewNavigation /// internal static void MoveNextView () { - View old = GetDeepestFocusedSubview (Application.Current.Focused); + View? old = GetDeepestFocusedSubview (Application.Current!.Focused); if (!Application.Current.FocusNext ()) { @@ -105,8 +105,8 @@ internal static class ViewNavigation { if (Application.OverlappedTop is null) { - Toplevel top = Application.Current.Modal ? Application.Current : Application.Top; - top.FocusNext (); + Toplevel? top = Application.Current!.Modal ? Application.Current : Application.Top; + top!.FocusNext (); if (top.Focused is null) { @@ -124,7 +124,7 @@ internal static class ViewNavigation internal static void MovePreviousView () { - View old = GetDeepestFocusedSubview (Application.Current.Focused); + View? old = GetDeepestFocusedSubview (Application.Current!.Focused); if (!Application.Current.FocusPrev ()) { @@ -146,8 +146,8 @@ internal static class ViewNavigation { if (Application.OverlappedTop is null) { - Toplevel top = Application.Current.Modal ? Application.Current : Application.Top; - top.FocusPrev (); + Toplevel? top = Application.Current!.Modal ? Application.Current : Application.Top; + top!.FocusPrev (); if (top.Focused is null) { diff --git a/Terminal.Gui/Application/Application.Run.cs b/Terminal.Gui/Application/Application.Run.cs index 541ad7141..bbff9c1ba 100644 --- a/Terminal.Gui/Application/Application.Run.cs +++ b/Terminal.Gui/Application/Application.Run.cs @@ -8,7 +8,7 @@ public static partial class Application // Run (Begin, Run, End, Stop) { // When `End ()` is called, it is possible `RunState.Toplevel` is a different object than `Top`. // This variable is set in `End` in this case so that `Begin` correctly sets `Top`. - private static Toplevel _cachedRunStateToplevel; + private static Toplevel? _cachedRunStateToplevel; /// /// Notify that a new was created ( was called). The token is @@ -19,7 +19,7 @@ public static partial class Application // Run (Begin, Run, End, Stop) /// must also subscribe to and manually dispose of the token /// when the application is done. /// - public static event EventHandler NotifyNewRunState; + public static event EventHandler? NotifyNewRunState; /// Notify that an existent is stopping ( was called). /// @@ -27,7 +27,7 @@ public static partial class Application // Run (Begin, Run, End, Stop) /// must also subscribe to and manually dispose of the token /// when the application is done. /// - public static event EventHandler NotifyStopRunState; + public static event EventHandler? NotifyStopRunState; /// Building block API: Prepares the provided for execution. /// @@ -96,9 +96,9 @@ public static partial class Application // Run (Begin, Run, End, Stop) throw new ObjectDisposedException (Top.GetType ().FullName); } } - else if (OverlappedTop is { } && toplevel != Top && _topLevels.Contains (Top)) + else if (OverlappedTop is { } && toplevel != Top && _topLevels.Contains (Top!)) { - Top.OnLeave (toplevel); + Top!.OnLeave (toplevel); } // BUGBUG: We should not depend on `Id` internally. @@ -120,7 +120,7 @@ public static partial class Application // Run (Begin, Run, End, Stop) } else { - Toplevel dup = _topLevels.FirstOrDefault (x => x.Id == toplevel.Id); + Toplevel? dup = _topLevels.FirstOrDefault (x => x.Id == toplevel.Id); if (dup is null) { @@ -150,7 +150,7 @@ public static partial class Application // Run (Begin, Run, End, Stop) if (toplevel.Visible) { Current?.OnDeactivate (toplevel); - Toplevel previousCurrent = Current; + Toplevel previousCurrent = Current!; Current = toplevel; Current.OnActivate (previousCurrent); @@ -161,11 +161,10 @@ public static partial class Application // Run (Begin, Run, End, Stop) refreshDriver = false; } } - else if ((OverlappedTop != null - && toplevel != OverlappedTop + else if ((toplevel != OverlappedTop && Current?.Modal == true && !_topLevels.Peek ().Modal) - || (OverlappedTop is { } && toplevel != OverlappedTop && Current?.Running == false)) + || (toplevel != OverlappedTop && Current?.Running == false)) { refreshDriver = false; MoveCurrent (toplevel); @@ -173,10 +172,10 @@ public static partial class Application // Run (Begin, Run, End, Stop) else { refreshDriver = false; - MoveCurrent (Current); + MoveCurrent (Current!); } - toplevel.SetRelativeLayout (Driver.Screen.Size); + toplevel.SetRelativeLayout (Driver!.Screen.Size); toplevel.LayoutSubviews (); toplevel.PositionToplevels (); @@ -216,7 +215,7 @@ public static partial class Application // Run (Begin, Run, End, Stop) internal static bool PositionCursor (View view) { // Find the most focused view and position the cursor there. - View mostFocused = view?.MostFocused; + View? mostFocused = view?.MostFocused; if (mostFocused is null) { @@ -233,7 +232,7 @@ public static partial class Application // Run (Begin, Run, End, Stop) // If the view is not visible or enabled, don't position the cursor if (!mostFocused.Visible || !mostFocused.Enabled) { - Driver.GetCursorVisibility (out CursorVisibility current); + Driver!.GetCursorVisibility (out CursorVisibility current); if (current != CursorVisibility.Invisible) { @@ -245,7 +244,7 @@ public static partial class Application // Run (Begin, Run, End, Stop) // If the view is not visible within it's superview, don't position the cursor Rectangle mostFocusedViewport = mostFocused.ViewportToScreen (mostFocused.Viewport with { Location = Point.Empty }); - Rectangle superViewViewport = mostFocused.SuperView?.ViewportToScreen (mostFocused.SuperView.Viewport with { Location = Point.Empty }) ?? Driver.Screen; + Rectangle superViewViewport = mostFocused.SuperView?.ViewportToScreen (mostFocused.SuperView.Viewport with { Location = Point.Empty }) ?? Driver!.Screen; if (!superViewViewport.IntersectsWith (mostFocusedViewport)) { @@ -254,7 +253,7 @@ public static partial class Application // Run (Begin, Run, End, Stop) Point? cursor = mostFocused.PositionCursor (); - Driver.GetCursorVisibility (out CursorVisibility currentCursorVisibility); + Driver!.GetCursorVisibility (out CursorVisibility currentCursorVisibility); if (cursor is { }) { @@ -306,7 +305,7 @@ public static partial class Application // Run (Begin, Run, End, Stop) /// The created object. The caller is responsible for disposing this object. [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] - public static Toplevel Run (Func errorHandler = null, ConsoleDriver driver = null) { return Run (errorHandler, driver); } + public static Toplevel Run (Func? errorHandler = null, ConsoleDriver? driver = null) { return Run (errorHandler, driver); } /// /// Runs the application by creating a -derived object of type T and calling @@ -331,7 +330,7 @@ public static partial class Application // Run (Begin, Run, End, Stop) /// The created T object. The caller is responsible for disposing this object. [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] - public static T Run (Func errorHandler = null, ConsoleDriver driver = null) + public static T Run (Func? errorHandler = null, ConsoleDriver? driver = null) where T : Toplevel, new () { if (!_initialized) @@ -385,7 +384,7 @@ public static partial class Application // Run (Begin, Run, End, Stop) /// RELEASE builds only: Handler for any unhandled exceptions (resumes when returns true, /// rethrows when null). /// - public static void Run (Toplevel view, Func errorHandler = null) + public static void Run (Toplevel view, Func? errorHandler = null) { ArgumentNullException.ThrowIfNull (view); @@ -460,7 +459,7 @@ public static partial class Application // Run (Begin, Run, End, Stop) /// reset, repeating the invocation. If it returns false, the timeout will stop and be removed. The returned value is a /// token that can be used to stop the timeout by calling . /// - public static object AddTimeout (TimeSpan time, Func callback) { return MainLoop?.AddTimeout (time, callback); } + public static object AddTimeout (TimeSpan time, Func callback) { return MainLoop!.AddTimeout (time, callback); } /// Removes a previously scheduled timeout /// The token parameter is the value returned by . @@ -498,8 +497,7 @@ public static partial class Application // Run (Begin, Run, End, Stop) public static void Refresh () { // TODO: Figure out how to remove this call to ClearContents. Refresh should just repaint damaged areas, not clear - Driver.ClearContents (); - View last = null; + Driver!.ClearContents (); foreach (Toplevel v in _topLevels.Reverse ()) { @@ -509,8 +507,6 @@ public static partial class Application // Run (Begin, Run, End, Stop) v.SetSubViewNeedsDisplay (); v.Draw (); } - - last = v; } Driver.Refresh (); @@ -518,11 +514,11 @@ public static partial class Application // Run (Begin, Run, End, Stop) /// This event is raised on each iteration of the main loop. /// See also - public static event EventHandler Iteration; + public static event EventHandler? Iteration; /// The driver for the application /// The main loop. - internal static MainLoop MainLoop { get; private set; } + internal static MainLoop? MainLoop { get; private set; } /// /// Set to true to cause to be called after the first iteration. Set to false (the default) to @@ -661,17 +657,17 @@ public static partial class Application // Run (Begin, Run, End, Stop) /// property on the currently running to false. /// /// - public static void RequestStop (Toplevel top = null) + public static void RequestStop (Toplevel? top = null) { - if (OverlappedTop is null || top is null || (OverlappedTop is null && top is { })) + if (OverlappedTop is null || top is null) { top = Current; } if (OverlappedTop != null - && top.IsOverlappedContainer + && top!.IsOverlappedContainer && top?.Running == true - && (Current?.Modal == false || (Current?.Modal == true && Current?.Running == false))) + && (Current?.Modal == false || Current is { Modal: true, Running: false })) { OverlappedTop.RequestStop (); } @@ -679,7 +675,7 @@ public static partial class Application // Run (Begin, Run, End, Stop) && top != Current && Current?.Running == true && Current?.Modal == true - && top.Modal + && top!.Modal && top.Running) { var ev = new ToplevelClosingEventArgs (Current); @@ -708,13 +704,13 @@ public static partial class Application // Run (Begin, Run, End, Stop) && top != Current && Current?.Modal == false && Current?.Running == true - && !top.Running) + && !top!.Running) || (OverlappedTop != null && top != OverlappedTop && top != Current && Current?.Modal == false && Current?.Running == false - && !top.Running + && !top!.Running && _topLevels.ToArray () [1].Running)) { MoveCurrent (top); @@ -722,7 +718,7 @@ public static partial class Application // Run (Begin, Run, End, Stop) else if (OverlappedTop != null && Current != top && Current?.Running == true - && !top.Running + && !top!.Running && Current?.Modal == true && top.Modal) { @@ -734,9 +730,9 @@ public static partial class Application // Run (Begin, Run, End, Stop) && Current == top && OverlappedTop?.Running == true && Current?.Running == true - && top.Running + && top!.Running && Current?.Modal == true - && top.Modal) + && top!.Modal) { // The OverlappedTop was requested to stop inside a modal Toplevel which is the Current and top, // both are the same, so needed to set the Current.Running to false too. @@ -747,13 +743,13 @@ public static partial class Application // Run (Begin, Run, End, Stop) { Toplevel currentTop; - if (top == Current || (Current?.Modal == true && !top.Modal)) + if (top == Current || (Current?.Modal == true && !top!.Modal)) { - currentTop = Current; + currentTop = Current!; } else { - currentTop = top; + currentTop = top!; } if (!currentTop.Running) diff --git a/Terminal.Gui/Application/Application.Toplevel.cs b/Terminal.Gui/Application/Application.Toplevel.cs index d8996a383..e272ea7aa 100644 --- a/Terminal.Gui/Application/Application.Toplevel.cs +++ b/Terminal.Gui/Application/Application.Toplevel.cs @@ -10,7 +10,7 @@ public static partial class Application // Toplevel handling /// The object used for the application on startup () /// The top. - public static Toplevel Top { get; private set; } + public static Toplevel? Top { get; private set; } // TODO: Determine why this can't just return _topLevels.Peek()? /// @@ -22,7 +22,7 @@ public static partial class Application // Toplevel handling /// This will only be distinct from in scenarios where is . /// /// The current. - public static Toplevel Current { get; private set; } + public static Toplevel? Current { get; private set; } /// /// If is not already Current and visible, finds the last Modal Toplevel in the stack and makes it Current. @@ -195,7 +195,7 @@ public static partial class Application // Toplevel handling /// Event handlers can set to to prevent /// from changing it's size to match the new terminal size. /// - public static event EventHandler SizeChanging; + public static event EventHandler? SizeChanging; /// /// Called when the application's size changes. Sets the size of all s and fires the diff --git a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs index cad728b72..8e6a08d59 100644 --- a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs @@ -1,3 +1,4 @@ +#nullable enable // // ConsoleDriver.cs: Base class for Terminal.Gui ConsoleDriver implementations. // @@ -16,7 +17,7 @@ public abstract class ConsoleDriver { // As performance is a concern, we keep track of the dirty lines and only refresh those. // This is in addition to the dirty flag on each cell. - internal bool [] _dirtyLines; + internal bool []? _dirtyLines; // QUESTION: When non-full screen apps are supported, will this represent the app size, or will that be in Application? /// Gets the location and size of the terminal screen. @@ -443,7 +444,7 @@ public abstract class ConsoleDriver public abstract bool SetCursorVisibility (CursorVisibility visibility); /// The event fired when the terminal is resized. - public event EventHandler SizeChanged; + public event EventHandler? SizeChanged; /// Suspends the application (e.g. on Linux via SIGTSTP) and upon resume, resets the console driver. /// This is only implemented in . diff --git a/Terminal.Gui/View/Adornment/Margin.cs b/Terminal.Gui/View/Adornment/Margin.cs index 046965e32..8ef63fe04 100644 --- a/Terminal.Gui/View/Adornment/Margin.cs +++ b/Terminal.Gui/View/Adornment/Margin.cs @@ -222,10 +222,10 @@ public class Margin : Adornment // Adjust the shadow such that it is drawn aligned with the Border if (ShadowStyle != ShadowStyle.None && _rightShadow is { } && _bottomShadow is { }) { - _rightShadow.Y = Parent.Border.Thickness.Top > 0 + _rightShadow.Y = Parent is { } && Parent.Border.Thickness.Top > 0 ? Parent.Border.Thickness.Top - (Parent.Border.Thickness.Top > 2 && Parent.Border.Settings.FastHasFlags (BorderSettings.Title) ? 1 : 0) : 1; - _bottomShadow.X = Parent.Border.Thickness.Left > 0 ? Parent.Border.Thickness.Left : 1; + _bottomShadow.X = Parent is { } && Parent.Border.Thickness.Left > 0 ? Parent.Border.Thickness.Left : 1; } } } diff --git a/Terminal.Gui/View/Adornment/ShadowView.cs b/Terminal.Gui/View/Adornment/ShadowView.cs index c5e7a428a..ad06dc754 100644 --- a/Terminal.Gui/View/Adornment/ShadowView.cs +++ b/Terminal.Gui/View/Adornment/ShadowView.cs @@ -15,7 +15,7 @@ internal class ShadowView : View { var attr = Attribute.Default; - if (adornment.Parent.SuperView is { }) + if (adornment.Parent?.SuperView is { }) { attr = adornment.Parent.SuperView.GetNormalColor (); } diff --git a/Terminal.Gui/View/Layout/Dim.cs b/Terminal.Gui/View/Layout/Dim.cs index 7dfc6eb2e..3102d5d3c 100644 --- a/Terminal.Gui/View/Layout/Dim.cs +++ b/Terminal.Gui/View/Layout/Dim.cs @@ -232,7 +232,7 @@ public abstract class Dim } var newDim = new DimCombine (AddOrSubtract.Add, left, right); - (left as DimView)?.Target.SetNeedsLayout (); + (left as DimView)?.Target?.SetNeedsLayout (); return newDim; } diff --git a/Terminal.Gui/View/Layout/DimView.cs b/Terminal.Gui/View/Layout/DimView.cs index 09ea96800..e95efd4fb 100644 --- a/Terminal.Gui/View/Layout/DimView.cs +++ b/Terminal.Gui/View/Layout/DimView.cs @@ -30,7 +30,7 @@ public class DimView : Dim public override bool Equals (object? other) { return other is DimView abs && abs.Target == Target && abs.Dimension == Dimension; } /// - public override int GetHashCode () { return Target.GetHashCode (); } + public override int GetHashCode () { return Target!.GetHashCode (); } /// /// Gets the View the dimension is anchored to. diff --git a/Terminal.Gui/View/Layout/Pos.cs b/Terminal.Gui/View/Layout/Pos.cs index 853bfa0ab..63e14b67f 100644 --- a/Terminal.Gui/View/Layout/Pos.cs +++ b/Terminal.Gui/View/Layout/Pos.cs @@ -379,7 +379,7 @@ public abstract class Pos if (left is PosView view) { - view.Target.SetNeedsLayout (); + view.Target?.SetNeedsLayout (); } return newPos; @@ -408,7 +408,7 @@ public abstract class Pos if (left is PosView view) { - view.Target.SetNeedsLayout (); + view.Target?.SetNeedsLayout (); } return newPos; diff --git a/Terminal.Gui/View/Layout/PosView.cs b/Terminal.Gui/View/Layout/PosView.cs index fdf5bf784..8ceba980f 100644 --- a/Terminal.Gui/View/Layout/PosView.cs +++ b/Terminal.Gui/View/Layout/PosView.cs @@ -28,7 +28,7 @@ public class PosView (View? view, Side side) : Pos public override bool Equals (object? other) { return other is PosView abs && abs.Target == Target && abs.Side == Side; } /// - public override int GetHashCode () { return Target.GetHashCode (); } + public override int GetHashCode () { return Target!.GetHashCode (); } /// public override string ToString () diff --git a/Terminal.Gui/Views/Menu/MenuBar.cs b/Terminal.Gui/Views/Menu/MenuBar.cs index 0b4e574de..3428b2c93 100644 --- a/Terminal.Gui/Views/Menu/MenuBar.cs +++ b/Terminal.Gui/Views/Menu/MenuBar.cs @@ -1594,7 +1594,7 @@ public class MenuBar : View, IDesignable /// - public bool EnableForDesign (in TContext context) where TContext : notnull + public bool EnableForDesign (ref readonly TContext context) where TContext : notnull { if (context is not Func actionFn) { diff --git a/UICatalog/UICatalog.cs b/UICatalog/UICatalog.cs index dd18f0bd1..a2cc9a474 100644 --- a/UICatalog/UICatalog.cs +++ b/UICatalog/UICatalog.cs @@ -715,7 +715,7 @@ internal class UICatalogApp } ColorScheme = Colors.ColorSchemes [_topLevelColorScheme]; - Application.Top.SetNeedsDisplay (); + Application.Top!.SetNeedsDisplay (); }; schemeMenuItems.Add (item); } diff --git a/UnitTests/Views/MenuBarTests.cs b/UnitTests/Views/MenuBarTests.cs index 35d08beff..6f9de4683 100644 --- a/UnitTests/Views/MenuBarTests.cs +++ b/UnitTests/Views/MenuBarTests.cs @@ -1,6 +1,4 @@ -using UICatalog.Scenarios; -using Xunit.Abstractions; - +using Xunit.Abstractions; namespace Terminal.Gui.ViewsTests; @@ -34,13 +32,13 @@ public class MenuBarTests (ITestOutputHelper output) Assert.True ( menu.NewMouseEvent ( - new() { Position = new (0, 0), Flags = MouseFlags.Button1Pressed, View = menu } + new () { Position = new (0, 0), Flags = MouseFlags.Button1Pressed, View = menu } ) ); Assert.True ( menu._openMenu.NewMouseEvent ( - new() { Position = new (0, 0), Flags = MouseFlags.Button1Clicked, View = menu._openMenu } + new () { Position = new (0, 0), Flags = MouseFlags.Button1Clicked, View = menu._openMenu } ) ); Application.MainLoop.RunIteration (); @@ -54,7 +52,7 @@ public class MenuBarTests (ITestOutputHelper output) Assert.True ( menu.NewMouseEvent ( - new() { Position = new (0, 0), Flags = MouseFlags.Button1Pressed, View = menu } + new () { Position = new (0, 0), Flags = MouseFlags.Button1Pressed, View = menu } ) ); Application.Refresh (); @@ -63,16 +61,14 @@ public class MenuBarTests (ITestOutputHelper output) @$" Nullable Checked ┌──────────────────────┐ -│ { - CM.Glyphs.CheckStateNone -} Check this out 你 │ +│ {CM.Glyphs.CheckStateNone} Check this out 你 │ └──────────────────────┘", output ); Assert.True ( menu._openMenu.NewMouseEvent ( - new() { Position = new (0, 0), Flags = MouseFlags.Button1Clicked, View = menu._openMenu } + new () { Position = new (0, 0), Flags = MouseFlags.Button1Clicked, View = menu._openMenu } ) ); Application.MainLoop.RunIteration (); @@ -84,13 +80,13 @@ public class MenuBarTests (ITestOutputHelper output) Assert.True ( menu.NewMouseEvent ( - new() { Position = new (0, 0), Flags = MouseFlags.Button1Pressed, View = menu } + new () { Position = new (0, 0), Flags = MouseFlags.Button1Pressed, View = menu } ) ); Assert.True ( menu._openMenu.NewMouseEvent ( - new() { Position = new (0, 0), Flags = MouseFlags.Button1Clicked, View = menu._openMenu } + new () { Position = new (0, 0), Flags = MouseFlags.Button1Clicked, View = menu._openMenu } ) ); Application.MainLoop.RunIteration (); @@ -185,7 +181,7 @@ public class MenuBarTests (ITestOutputHelper output) Assert.True (menuBar.WantMousePositionReports); Assert.False (menuBar.IsMenuOpen); - menuBar = new() { Menus = [] }; + menuBar = new () { Menus = [] }; Assert.Equal (0, menuBar.X); Assert.Equal (0, menuBar.Y); Assert.IsType (menuBar.Width); @@ -296,7 +292,7 @@ public class MenuBarTests (ITestOutputHelper output) Assert.True ( menu.NewMouseEvent ( - new() { Position = new (0, 0), Flags = MouseFlags.Button1Pressed, View = menu } + new () { Position = new (0, 0), Flags = MouseFlags.Button1Pressed, View = menu } ) ); top.Draw (); @@ -317,7 +313,7 @@ public class MenuBarTests (ITestOutputHelper output) Assert.True ( top.Subviews [1] .NewMouseEvent ( - new() { Position = new (0, 2), Flags = MouseFlags.Button1Clicked, View = top.Subviews [1] } + new () { Position = new (0, 2), Flags = MouseFlags.Button1Clicked, View = top.Subviews [1] } ) ); top.Subviews [1].Draw (); @@ -338,7 +334,7 @@ public class MenuBarTests (ITestOutputHelper output) Assert.True ( top.Subviews [1] .NewMouseEvent ( - new() { Position = new (0, 2), Flags = MouseFlags.ReportMousePosition, View = top.Subviews [1] } + new () { Position = new (0, 2), Flags = MouseFlags.ReportMousePosition, View = top.Subviews [1] } ) ); top.Subviews [1].Draw (); @@ -516,7 +512,7 @@ public class MenuBarTests (ITestOutputHelper output) output ); - Application.OnMouseEvent (new() { Position = new (20, 5), Flags = MouseFlags.Button1Clicked }); + Application.OnMouseEvent (new () { Position = new (20, 5), Flags = MouseFlags.Button1Clicked }); firstIteration = false; @@ -549,7 +545,7 @@ public class MenuBarTests (ITestOutputHelper output) { menu.OpenMenu (); - Application.OnMouseEvent (new() { Position = new (20, 5 + i), Flags = MouseFlags.Button1Clicked }); + Application.OnMouseEvent (new () { Position = new (20, 5 + i), Flags = MouseFlags.Button1Clicked }); firstIteration = false; Application.RunIteration (ref rsDialog, ref firstIteration); @@ -705,7 +701,7 @@ public class MenuBarTests (ITestOutputHelper output) output ); - Application.OnMouseEvent (new() { Position = new (20, 5), Flags = MouseFlags.Button1Clicked }); + Application.OnMouseEvent (new () { Position = new (20, 5), Flags = MouseFlags.Button1Clicked }); firstIteration = false; @@ -727,7 +723,7 @@ public class MenuBarTests (ITestOutputHelper output) { menu.OpenMenu (); - Application.OnMouseEvent (new() { Position = new (20, 5 + i), Flags = MouseFlags.Button1Clicked }); + Application.OnMouseEvent (new () { Position = new (20, 5 + i), Flags = MouseFlags.Button1Clicked }); firstIteration = false; Application.RunIteration (ref rs, ref firstIteration); @@ -1253,15 +1249,15 @@ wo MenuItem mbiCurrent = null; MenuItem miCurrent = null; - MenuBar menu = new MenuBar (); - menu.EnableForDesign ( - new Func (s => - { - miAction = s as string; + var menu = new MenuBar (); - return true; - }) - ); + Func fn = s => + { + miAction = s as string; + + return true; + }; + menu.EnableForDesign (ref fn); menu.Key = KeyCode.F9; menu.MenuOpening += (s, e) => mbiCurrent = e.CurrentMenu; @@ -1303,15 +1299,17 @@ wo MenuItem mbiCurrent = null; MenuItem miCurrent = null; - MenuBar menu = new MenuBar (); - menu.EnableForDesign ( - new Func (s => - { - miAction = s as string; + var menu = new MenuBar (); - return true; - }) - ); + menu.EnableForDesign ( + new Func ( + s => + { + miAction = s as string; + + return true; + }) + ); menu.Key = KeyCode.F9; menu.MenuOpening += (s, e) => mbiCurrent = e.CurrentMenu; @@ -1478,13 +1476,13 @@ wo top.Add (menu); Application.Begin (top); - Assert.True (menu.NewMouseEvent (new() { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu })); + Assert.True (menu.NewMouseEvent (new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu })); Assert.True (menu.IsMenuOpen); top.Draw (); TestHelpers.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); - Assert.True (menu.NewMouseEvent (new() { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu })); + Assert.True (menu.NewMouseEvent (new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu })); Assert.False (menu.IsMenuOpen); top.Draw (); TestHelpers.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); @@ -1985,7 +1983,7 @@ wo top.Remove (menu); // Now test WITH HotKeys - menu = new() + menu = new () { Menus = [ @@ -2114,9 +2112,9 @@ wo { Menus = [ - new() { Title = "Test 1", Action = () => { } }, + new () { Title = "Test 1", Action = () => { } }, - new() { Title = "Test 2", Action = () => { } } + new () { Title = "Test 2", Action = () => { } } ] }; @@ -2204,7 +2202,7 @@ wo // open the menu Assert.True ( menu.NewMouseEvent ( - new() { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu } + new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu } ) ); Assert.True (menu.IsMenuOpen); @@ -2213,7 +2211,7 @@ wo Assert.True ( mCurrent.NewMouseEvent ( - new() { Position = new (1, 1), Flags = MouseFlags.ReportMousePosition, View = mCurrent } + new () { Position = new (1, 1), Flags = MouseFlags.ReportMousePosition, View = mCurrent } ) ); Assert.True (menu.IsMenuOpen); @@ -2222,7 +2220,7 @@ wo Assert.True ( mCurrent.NewMouseEvent ( - new() { Position = new (1, 1), Flags = MouseFlags.ReportMousePosition, View = mCurrent } + new () { Position = new (1, 1), Flags = MouseFlags.ReportMousePosition, View = mCurrent } ) ); Assert.True (menu.IsMenuOpen); @@ -2231,7 +2229,7 @@ wo Assert.True ( mCurrent.NewMouseEvent ( - new() { Position = new (1, 2), Flags = MouseFlags.ReportMousePosition, View = mCurrent } + new () { Position = new (1, 2), Flags = MouseFlags.ReportMousePosition, View = mCurrent } ) ); Assert.True (menu.IsMenuOpen); @@ -2241,7 +2239,7 @@ wo // close the menu Assert.True ( menu.NewMouseEvent ( - new() { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu } + new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu } ) ); Assert.False (menu.IsMenuOpen); @@ -2411,7 +2409,7 @@ Edit // Click on Edit Assert.True ( menu.NewMouseEvent ( - new() { Position = new (10, 0), Flags = MouseFlags.Button1Pressed, View = menu } + new () { Position = new (10, 0), Flags = MouseFlags.Button1Pressed, View = menu } ) ); Assert.True (menu.IsMenuOpen); @@ -2421,7 +2419,7 @@ Edit // Click on Paste Assert.True ( mCurrent.NewMouseEvent ( - new() { Position = new (10, 2), Flags = MouseFlags.ReportMousePosition, View = mCurrent } + new () { Position = new (10, 2), Flags = MouseFlags.ReportMousePosition, View = mCurrent } ) ); Assert.True (menu.IsMenuOpen); @@ -2435,7 +2433,7 @@ Edit // Edit menu is open. Click on the menu at Y = -1, which is outside the menu. Assert.False ( mCurrent.NewMouseEvent ( - new() { Position = new (10, i), Flags = MouseFlags.ReportMousePosition, View = menu } + new () { Position = new (10, i), Flags = MouseFlags.ReportMousePosition, View = menu } ) ); } @@ -2444,7 +2442,7 @@ Edit // Edit menu is open. Click on the menu at Y = i. Assert.True ( mCurrent.NewMouseEvent ( - new() { Position = new (10, i), Flags = MouseFlags.ReportMousePosition, View = mCurrent } + new () { Position = new (10, i), Flags = MouseFlags.ReportMousePosition, View = mCurrent } ) ); } @@ -2609,7 +2607,7 @@ Edit Application.Begin (top); Assert.True (tf.HasFocus); - Assert.True (menu.NewMouseEvent (new() { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu })); + Assert.True (menu.NewMouseEvent (new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu })); Assert.True (menu.IsMenuOpen); Assert.False (tf.HasFocus); top.Draw (); @@ -2617,7 +2615,7 @@ Edit Assert.True ( menu.NewMouseEvent ( - new() { Position = new (8, 0), Flags = MouseFlags.ReportMousePosition, View = menu } + new () { Position = new (8, 0), Flags = MouseFlags.ReportMousePosition, View = menu } ) ); Assert.True (menu.IsMenuOpen); @@ -2627,7 +2625,7 @@ Edit Assert.True ( menu.NewMouseEvent ( - new() { Position = new (15, 0), Flags = MouseFlags.ReportMousePosition, View = menu } + new () { Position = new (15, 0), Flags = MouseFlags.ReportMousePosition, View = menu } ) ); Assert.True (menu.IsMenuOpen); @@ -2637,7 +2635,7 @@ Edit Assert.True ( menu.NewMouseEvent ( - new() { Position = new (8, 0), Flags = MouseFlags.ReportMousePosition, View = menu } + new () { Position = new (8, 0), Flags = MouseFlags.ReportMousePosition, View = menu } ) ); Assert.True (menu.IsMenuOpen); @@ -2647,7 +2645,7 @@ Edit Assert.True ( menu.NewMouseEvent ( - new() { Position = new (1, 0), Flags = MouseFlags.ReportMousePosition, View = menu } + new () { Position = new (1, 0), Flags = MouseFlags.ReportMousePosition, View = menu } ) ); Assert.True (menu.IsMenuOpen); @@ -2655,7 +2653,7 @@ Edit top.Draw (); TestHelpers.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); - Assert.True (menu.NewMouseEvent (new() { Position = new (8, 0), Flags = MouseFlags.Button1Pressed, View = menu })); + Assert.True (menu.NewMouseEvent (new () { Position = new (8, 0), Flags = MouseFlags.Button1Pressed, View = menu })); Assert.False (menu.IsMenuOpen); Assert.True (tf.HasFocus); top.Draw (); @@ -2991,7 +2989,7 @@ Edit Assert.True ( menu.NewMouseEvent ( - new() { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu } + new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu } ) ); top.Draw (); @@ -3010,7 +3008,7 @@ Edit Assert.False ( menu.NewMouseEvent ( - new() + new () { Position = new (1, 2), Flags = MouseFlags.ReportMousePosition, View = Application.Top.Subviews [1] } @@ -3033,7 +3031,7 @@ Edit Assert.False ( menu.NewMouseEvent ( - new() + new () { Position = new (1, 1), Flags = MouseFlags.ReportMousePosition, View = Application.Top.Subviews [1] } @@ -3055,7 +3053,7 @@ Edit Assert.False ( menu.NewMouseEvent ( - new() { Position = new (70, 2), Flags = MouseFlags.Button1Clicked, View = Application.Top } + new () { Position = new (70, 2), Flags = MouseFlags.Button1Clicked, View = Application.Top } ) ); top.Draw (); @@ -3488,7 +3486,7 @@ Edit Assert.True ( menu.NewMouseEvent ( - new() { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu } + new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu } ) ); top.Draw (); @@ -3505,7 +3503,7 @@ Edit Assert.False ( menu.NewMouseEvent ( - new() { Position = new (1, 2), Flags = MouseFlags.Button1Clicked, View = Application.Top.Subviews [1] } + new () { Position = new (1, 2), Flags = MouseFlags.Button1Clicked, View = Application.Top.Subviews [1] } ) ); top.Draw (); @@ -3523,7 +3521,7 @@ Edit Assert.False ( menu.NewMouseEvent ( - new() { Position = new (1, 1), Flags = MouseFlags.Button1Clicked, View = Application.Top.Subviews [2] } + new () { Position = new (1, 1), Flags = MouseFlags.Button1Clicked, View = Application.Top.Subviews [2] } ) ); top.Draw (); @@ -3540,7 +3538,7 @@ Edit Assert.False ( menu.NewMouseEvent ( - new() { Position = new (70, 2), Flags = MouseFlags.Button1Clicked, View = Application.Top } + new () { Position = new (70, 2), Flags = MouseFlags.Button1Clicked, View = Application.Top } ) ); top.Draw (); @@ -3619,13 +3617,7 @@ Edit public string ExpectedBottomRow (int i) { - return $"{ - CM.Glyphs.LLCorner - }{ - new (CM.Glyphs.HLine.ToString () [0], Menus [i].Children [0].TitleLength + 3) - }{ - CM.Glyphs.LRCorner - } \n"; + return $"{CM.Glyphs.LLCorner}{new (CM.Glyphs.HLine.ToString () [0], Menus [i].Children [0].TitleLength + 3)}{CM.Glyphs.LRCorner} \n"; } // The 3 spaces at end are a result of Menu.cs line 1062 where `pos` is calculated (` + spacesAfterTitle`) @@ -3654,13 +3646,7 @@ Edit // 1 space before the Title and 2 spaces after the Title/Check/Help public string ExpectedTopRow (int i) { - return $"{ - CM.Glyphs.ULCorner - }{ - new (CM.Glyphs.HLine.ToString () [0], Menus [i].Children [0].TitleLength + 3) - }{ - CM.Glyphs.URCorner - } \n"; + return $"{CM.Glyphs.ULCorner}{new (CM.Glyphs.HLine.ToString () [0], Menus [i].Children [0].TitleLength + 3)}{CM.Glyphs.URCorner} \n"; } // Padding for the X of the sub menu Frame diff --git a/UnitTests/Views/ToplevelTests.cs b/UnitTests/Views/ToplevelTests.cs index 2d4a0a323..624da69ac 100644 --- a/UnitTests/Views/ToplevelTests.cs +++ b/UnitTests/Views/ToplevelTests.cs @@ -491,7 +491,7 @@ public partial class ToplevelTests (ITestOutputHelper output) Assert.Equal (tf1W2, top.MostFocused); Assert.True (Application.OnKeyDown (Key.CursorRight)); // move char to right in tf1W2 Assert.Equal (win2, top.Focused); - Assert.Equal (tf1W2, top.MostFocused); + Assert.Equal (tf1W2, top.MostFocused); Assert.True (Application.OnKeyDown (Key.CursorDown)); // move down to next view (tvW2) Assert.Equal (win2, top.Focused); Assert.Equal (tvW2, top.MostFocused); @@ -504,7 +504,7 @@ public partial class ToplevelTests (ITestOutputHelper output) Assert.Equal (win2, top.Focused); Assert.Equal (tvW2, top.MostFocused); tvW2.AllowsTab = false; - Assert.True (Application.OnKeyDown (Key.Tab.WithShift)); + Assert.True (Application.OnKeyDown (Key.Tab.WithShift)); Assert.Equal (win2, top.Focused); Assert.Equal (tf1W2, top.MostFocused); Assert.True (Application.OnKeyDown (Key.CursorLeft)); @@ -563,9 +563,6 @@ public partial class ToplevelTests (ITestOutputHelper output) [Fact] public void Added_Event_Should_Not_Be_Used_To_Initialize_Toplevel_Events () { - Key alternateForwardKey = default; - Key alternateBackwardKey = default; - Key quitKey = default; var wasAdded = false; var view = new View (); @@ -588,7 +585,7 @@ public partial class ToplevelTests (ITestOutputHelper output) Application.Shutdown (); } - + [Fact] [AutoInitShutdown] public void Mouse_Drag_On_Top_With_Superview_Null () From 04dbe68dbf72725dbcbdbcc0700fa142b7606503 Mon Sep 17 00:00:00 2001 From: Tig Date: Wed, 24 Jul 2024 14:20:08 -0600 Subject: [PATCH 19/33] Fixed nullable warnings 3 --- Terminal.Gui/Application/Application.Run.cs | 37 ++++++++++----------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/Terminal.Gui/Application/Application.Run.cs b/Terminal.Gui/Application/Application.Run.cs index bbff9c1ba..9a79af3d4 100644 --- a/Terminal.Gui/Application/Application.Run.cs +++ b/Terminal.Gui/Application/Application.Run.cs @@ -537,7 +537,7 @@ public static partial class Application // Run (Begin, Run, End, Stop) for (state.Toplevel.Running = true; state.Toplevel?.Running == true;) { - MainLoop.Running = true; + MainLoop!.Running = true; if (EndAfterFirstIteration && !firstIteration) { @@ -547,7 +547,7 @@ public static partial class Application // Run (Begin, Run, End, Stop) RunIteration (ref state, ref firstIteration); } - MainLoop.Running = false; + MainLoop!.Running = false; // Run one last iteration to consume any outstanding input events from Driver // This is important for remaining OnKeyUp events. @@ -562,7 +562,7 @@ public static partial class Application // Run (Begin, Run, End, Stop) /// public static void RunIteration (ref RunState state, ref bool firstIteration) { - if (MainLoop.Running && MainLoop.EventsPending ()) + if (MainLoop!.Running && MainLoop.EventsPending ()) { // Notify Toplevel it's ready if (firstIteration) @@ -580,7 +580,7 @@ public static partial class Application // Run (Begin, Run, End, Stop) OverlappedTop?.OnDeactivate (state.Toplevel); state.Toplevel = Current; OverlappedTop?.OnActivate (state.Toplevel); - Top.SetSubViewNeedsDisplay (); + Top!.SetSubViewNeedsDisplay (); Refresh (); } } @@ -592,9 +592,9 @@ public static partial class Application // Run (Begin, Run, End, Stop) return; } - if (state.Toplevel != Top && (Top.NeedsDisplay || Top.SubViewNeedsDisplay || Top.LayoutNeeded)) + if (state.Toplevel != Top && (Top!.NeedsDisplay || Top.SubViewNeedsDisplay || Top.LayoutNeeded)) { - state.Toplevel.SetNeedsDisplay (state.Toplevel.Frame); + state.Toplevel!.SetNeedsDisplay (state.Toplevel.Frame); Top.Draw (); foreach (Toplevel top in _topLevels.Reverse ()) @@ -610,8 +610,8 @@ public static partial class Application // Run (Begin, Run, End, Stop) if (_topLevels.Count == 1 && state.Toplevel == Top - && (Driver.Cols != state.Toplevel.Frame.Width - || Driver.Rows != state.Toplevel.Frame.Height) + && (Driver!.Cols != state.Toplevel!.Frame.Width + || Driver!.Rows != state.Toplevel.Frame.Height) && (state.Toplevel.NeedsDisplay || state.Toplevel.SubViewNeedsDisplay || state.Toplevel.LayoutNeeded)) @@ -619,18 +619,18 @@ public static partial class Application // Run (Begin, Run, End, Stop) Driver.ClearContents (); } - if (state.Toplevel.NeedsDisplay || state.Toplevel.SubViewNeedsDisplay || state.Toplevel.LayoutNeeded || OverlappedChildNeedsDisplay ()) + if (state.Toplevel!.NeedsDisplay || state.Toplevel.SubViewNeedsDisplay || state.Toplevel.LayoutNeeded || OverlappedChildNeedsDisplay ()) { state.Toplevel.SetNeedsDisplay (); state.Toplevel.Draw (); - Driver.UpdateScreen (); + Driver!.UpdateScreen (); //Driver.UpdateCursor (); } if (PositionCursor (state.Toplevel)) { - Driver.UpdateCursor (); + Driver!.UpdateCursor (); } // else @@ -642,7 +642,7 @@ public static partial class Application // Run (Begin, Run, End, Stop) //Driver.UpdateCursor (); } - if (state.Toplevel != Top && !state.Toplevel.Modal && (Top.NeedsDisplay || Top.SubViewNeedsDisplay || Top.LayoutNeeded)) + if (state.Toplevel != Top && !state.Toplevel.Modal && (Top!.NeedsDisplay || Top.SubViewNeedsDisplay || Top.LayoutNeeded)) { Top.Draw (); } @@ -673,8 +673,7 @@ public static partial class Application // Run (Begin, Run, End, Stop) } else if (OverlappedTop != null && top != Current - && Current?.Running == true - && Current?.Modal == true + && Current is { Running: true, Modal: true } && top!.Modal && top.Running) { @@ -702,14 +701,12 @@ public static partial class Application // Run (Begin, Run, End, Stop) else if ((OverlappedTop != null && top != OverlappedTop && top != Current - && Current?.Modal == false - && Current?.Running == true + && Current is { Modal: false, Running: true } && !top!.Running) || (OverlappedTop != null && top != OverlappedTop && top != Current - && Current?.Modal == false - && Current?.Running == false + && Current is { Modal: false, Running: false } && !top!.Running && _topLevels.ToArray () [1].Running)) { @@ -815,7 +812,7 @@ public static partial class Application // Run (Begin, Run, End, Stop) // If there is a OverlappedTop that is not the RunState.Toplevel then RunState.Toplevel // is a child of MidTop, and we should notify the OverlappedTop that it is closing - if (OverlappedTop is { } && !runState.Toplevel.Modal && runState.Toplevel != OverlappedTop) + if (OverlappedTop is { } && !runState.Toplevel!.Modal && runState.Toplevel != OverlappedTop) { OverlappedTop.OnChildClosed (runState.Toplevel); } @@ -841,7 +838,7 @@ public static partial class Application // Run (Begin, Run, End, Stop) else { SetCurrentOverlappedAsTop (); - runState.Toplevel.OnLeave (Current); + runState.Toplevel!.OnLeave (Current); Current.OnEnter (runState.Toplevel); } From 3b351891067d6677fa501df33993842d3a098b7d Mon Sep 17 00:00:00 2001 From: Tig Date: Wed, 24 Jul 2024 14:36:20 -0600 Subject: [PATCH 20/33] Fixed nullable warnings 4 --- .../Application/Application.Toplevel.cs | 15 +- Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs | 201 +++++++++--------- 2 files changed, 116 insertions(+), 100 deletions(-) diff --git a/Terminal.Gui/Application/Application.Toplevel.cs b/Terminal.Gui/Application/Application.Toplevel.cs index e272ea7aa..d45360d64 100644 --- a/Terminal.Gui/Application/Application.Toplevel.cs +++ b/Terminal.Gui/Application/Application.Toplevel.cs @@ -59,7 +59,7 @@ public static partial class Application // Toplevel handling /// /// /// - private static Toplevel FindDeepestTop (Toplevel start, in Point location) + private static Toplevel? FindDeepestTop (Toplevel start, in Point location) { if (!start.Frame.Contains (location)) { @@ -91,15 +91,20 @@ public static partial class Application // Toplevel handling /// /// Given , returns the first Superview up the chain that is . /// - private static View FindTopFromView (View view) + private static View? FindTopFromView (View? view) { - View top = view?.SuperView is { } && view?.SuperView != Top + if (view is null) + { + return null; + } + + View top = view.SuperView is { } && view.SuperView != Top ? view.SuperView : view; while (top?.SuperView is { } && top?.SuperView != Top) { - top = top.SuperView; + top = top!.SuperView; } return top; @@ -221,7 +226,7 @@ public static partial class Application // Toplevel handling if (PositionCursor (t)) { - Driver.UpdateCursor (); + Driver?.UpdateCursor (); } } diff --git a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs index 8e6a08d59..5e9a7dad3 100644 --- a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs @@ -46,7 +46,7 @@ public abstract class ConsoleDriver } /// Get the operating system clipboard. - public IClipboard Clipboard { get; internal set; } + public IClipboard? Clipboard { get; internal set; } /// /// Gets the column last set by . and are used by @@ -70,7 +70,7 @@ public abstract class ConsoleDriver /// is called. /// The format of the array is rows, columns. The first index is the row, the second index is the column. /// - public Cell [,] Contents { get; internal set; } + public Cell [,]? Contents { get; internal set; } /// The leftmost column in the terminal. public virtual int Left { get; internal set; } = 0; @@ -125,125 +125,133 @@ public abstract class ConsoleDriver int runeWidth = -1; bool validLocation = IsValidLocation (Col, Row); + if (Contents is null) + { + return; + } + if (validLocation) { rune = rune.MakePrintable (); runeWidth = rune.GetColumns (); - if (runeWidth == 0 && rune.IsCombiningMark ()) + lock (Contents) { - // AtlasEngine does not support NON-NORMALIZED combining marks in a way - // compatible with the driver architecture. Any CMs (except in the first col) - // are correctly combined with the base char, but are ALSO treated as 1 column - // width codepoints E.g. `echo "[e`u{0301}`u{0301}]"` will output `[é ]`. - // - // Until this is addressed (see Issue #), we do our best by - // a) Attempting to normalize any CM with the base char to it's left - // b) Ignoring any CMs that don't normalize - if (Col > 0) + if (runeWidth == 0 && rune.IsCombiningMark ()) { - if (Contents [Row, Col - 1].CombiningMarks.Count > 0) + // AtlasEngine does not support NON-NORMALIZED combining marks in a way + // compatible with the driver architecture. Any CMs (except in the first col) + // are correctly combined with the base char, but are ALSO treated as 1 column + // width codepoints E.g. `echo "[e`u{0301}`u{0301}]"` will output `[é ]`. + // + // Until this is addressed (see Issue #), we do our best by + // a) Attempting to normalize any CM with the base char to it's left + // b) Ignoring any CMs that don't normalize + if (Col > 0) { - // Just add this mark to the list - Contents [Row, Col - 1].CombiningMarks.Add (rune); - - // Ignore. Don't move to next column (let the driver figure out what to do). - } - else - { - // Attempt to normalize the cell to our left combined with this mark - string combined = Contents [Row, Col - 1].Rune + rune.ToString (); - - // Normalize to Form C (Canonical Composition) - string normalized = combined.Normalize (NormalizationForm.FormC); - - if (normalized.Length == 1) + if (Contents [Row, Col - 1].CombiningMarks.Count > 0) { - // It normalized! We can just set the Cell to the left with the - // normalized codepoint - Contents [Row, Col - 1].Rune = (Rune)normalized [0]; - - // Ignore. Don't move to next column because we're already there - } - else - { - // It didn't normalize. Add it to the Cell to left's CM list + // Just add this mark to the list Contents [Row, Col - 1].CombiningMarks.Add (rune); // Ignore. Don't move to next column (let the driver figure out what to do). } - } + else + { + // Attempt to normalize the cell to our left combined with this mark + string combined = Contents [Row, Col - 1].Rune + rune.ToString (); - Contents [Row, Col - 1].Attribute = CurrentAttribute; - Contents [Row, Col - 1].IsDirty = true; + // Normalize to Form C (Canonical Composition) + string normalized = combined.Normalize (NormalizationForm.FormC); + + if (normalized.Length == 1) + { + // It normalized! We can just set the Cell to the left with the + // normalized codepoint + Contents [Row, Col - 1].Rune = (Rune)normalized [0]; + + // Ignore. Don't move to next column because we're already there + } + else + { + // It didn't normalize. Add it to the Cell to left's CM list + Contents [Row, Col - 1].CombiningMarks.Add (rune); + + // Ignore. Don't move to next column (let the driver figure out what to do). + } + } + + Contents [Row, Col - 1].Attribute = CurrentAttribute; + Contents [Row, Col - 1].IsDirty = true; + } + else + { + // Most drivers will render a combining mark at col 0 as the mark + Contents [Row, Col].Rune = rune; + Contents [Row, Col].Attribute = CurrentAttribute; + Contents [Row, Col].IsDirty = true; + Col++; + } } else { - // Most drivers will render a combining mark at col 0 as the mark - Contents [Row, Col].Rune = rune; Contents [Row, Col].Attribute = CurrentAttribute; Contents [Row, Col].IsDirty = true; - Col++; - } - } - else - { - Contents [Row, Col].Attribute = CurrentAttribute; - Contents [Row, Col].IsDirty = true; - if (Col > 0) - { - // Check if cell to left has a wide glyph - if (Contents [Row, Col - 1].Rune.GetColumns () > 1) + if (Col > 0) { - // Invalidate cell to left - Contents [Row, Col - 1].Rune = Rune.ReplacementChar; - Contents [Row, Col - 1].IsDirty = true; + // Check if cell to left has a wide glyph + if (Contents [Row, Col - 1].Rune.GetColumns () > 1) + { + // Invalidate cell to left + Contents [Row, Col - 1].Rune = Rune.ReplacementChar; + Contents [Row, Col - 1].IsDirty = true; + } } - } - if (runeWidth < 1) - { - Contents [Row, Col].Rune = Rune.ReplacementChar; - } - else if (runeWidth == 1) - { - Contents [Row, Col].Rune = rune; - - if (Col < Clip.Right - 1) + if (runeWidth < 1) { - Contents [Row, Col + 1].IsDirty = true; - } - } - else if (runeWidth == 2) - { - if (Col == Clip.Right - 1) - { - // We're at the right edge of the clip, so we can't display a wide character. - // TODO: Figure out if it is better to show a replacement character or ' ' Contents [Row, Col].Rune = Rune.ReplacementChar; } - else + else if (runeWidth == 1) { Contents [Row, Col].Rune = rune; if (Col < Clip.Right - 1) { - // Invalidate cell to right so that it doesn't get drawn - // TODO: Figure out if it is better to show a replacement character or ' ' - Contents [Row, Col + 1].Rune = Rune.ReplacementChar; Contents [Row, Col + 1].IsDirty = true; } } - } - else - { - // This is a non-spacing character, so we don't need to do anything - Contents [Row, Col].Rune = (Rune)' '; - Contents [Row, Col].IsDirty = false; - } + else if (runeWidth == 2) + { + if (Col == Clip.Right - 1) + { + // We're at the right edge of the clip, so we can't display a wide character. + // TODO: Figure out if it is better to show a replacement character or ' ' + Contents [Row, Col].Rune = Rune.ReplacementChar; + } + else + { + Contents [Row, Col].Rune = rune; - _dirtyLines [Row] = true; + if (Col < Clip.Right - 1) + { + // Invalidate cell to right so that it doesn't get drawn + // TODO: Figure out if it is better to show a replacement character or ' ' + Contents [Row, Col + 1].Rune = Rune.ReplacementChar; + Contents [Row, Col + 1].IsDirty = true; + } + } + } + else + { + // This is a non-spacing character, so we don't need to do anything + Contents [Row, Col].Rune = (Rune)' '; + Contents [Row, Col].IsDirty = false; + } + + _dirtyLines! [Row] = true; + } } } @@ -258,14 +266,17 @@ public abstract class ConsoleDriver if (validLocation && Col < Clip.Right) { - // This is a double-width character, and we are not at the end of the line. - // Col now points to the second column of the character. Ensure it doesn't - // Get rendered. - Contents [Row, Col].IsDirty = false; - Contents [Row, Col].Attribute = CurrentAttribute; + lock (Contents!) + { + // This is a double-width character, and we are not at the end of the line. + // Col now points to the second column of the character. Ensure it doesn't + // Get rendered. + Contents [Row, Col].IsDirty = false; + Contents [Row, Col].Attribute = CurrentAttribute; - // TODO: Determine if we should wipe this out (for now now) - //Contents [Row, Col].Rune = (Rune)' '; + // TODO: Determine if we should wipe this out (for now now) + //Contents [Row, Col].Rune = (Rune)' '; + } } Col++; @@ -332,7 +343,7 @@ public abstract class ConsoleDriver /// public void SetContentsAsDirty () { - lock (Contents) + lock (Contents!) { for (var row = 0; row < Rows; row++) { @@ -358,7 +369,7 @@ public abstract class ConsoleDriver public void FillRect (Rectangle rect, Rune rune = default) { rect = Rectangle.Intersect (rect, Clip); - lock (Contents) + lock (Contents!) { for (int r = rect.Y; r < rect.Y + rect.Height; r++) { From 689c0cd93f6cc2892fe3cacaf56ed7a7be09c416 Mon Sep 17 00:00:00 2001 From: Tig Date: Wed, 24 Jul 2024 14:37:23 -0600 Subject: [PATCH 21/33] Fixed nullable warnings 5 --- Terminal.Gui/ConsoleDrivers/WindowsDriver.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs index 8ff127a4b..180745d9d 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs @@ -1266,18 +1266,18 @@ internal class WindowsDriver : ConsoleDriver return WinConsole?.WriteANSI (sb.ToString ()) ?? false; } - if (!(Col >= 0 && Row >= 0 && Col < Cols && Row < Rows)) - { - GetCursorVisibility (out CursorVisibility cursorVisibility); - _cachedCursorVisibility = cursorVisibility; - SetCursorVisibility (CursorVisibility.Invisible); + //if (!(Col >= 0 && Row >= 0 && Col < Cols && Row < Rows)) + //{ + // GetCursorVisibility (out CursorVisibility cursorVisibility); + // _cachedCursorVisibility = cursorVisibility; + // SetCursorVisibility (CursorVisibility.Invisible); - return false; - } + // return false; + //} - SetCursorVisibility (_cachedCursorVisibility ?? CursorVisibility.Default); + //SetCursorVisibility (_cachedCursorVisibility ?? CursorVisibility.Default); - return _cachedCursorVisibility == CursorVisibility.Default; + //return _cachedCursorVisibility == CursorVisibility.Default; } #endregion Cursor Handling From ff47aa29b9fe29e28c7bd52ed1aa718f0f87aa6a Mon Sep 17 00:00:00 2001 From: Tig Date: Wed, 24 Jul 2024 14:39:34 -0600 Subject: [PATCH 22/33] Fixed nullable warnings 6 --- UnitTests/Views/ToplevelTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/UnitTests/Views/ToplevelTests.cs b/UnitTests/Views/ToplevelTests.cs index 624da69ac..8688c5d4a 100644 --- a/UnitTests/Views/ToplevelTests.cs +++ b/UnitTests/Views/ToplevelTests.cs @@ -1049,12 +1049,12 @@ public partial class ToplevelTests (ITestOutputHelper output) Assert.False (subTop.IsLoaded); Assert.Equal (new (0, 0, 20, 10), view.Frame); - view.LayoutStarted += view_LayoutStarted; + view.LayoutStarted += ViewLayoutStarted; - void view_LayoutStarted (object sender, LayoutEventArgs e) + void ViewLayoutStarted (object sender, LayoutEventArgs e) { Assert.Equal (new (0, 0, 20, 10), view._needsDisplayRect); - view.LayoutStarted -= view_LayoutStarted; + view.LayoutStarted -= ViewLayoutStarted; } Application.Begin (top); From 022050db730f7fc53d2dfb8e171e6da2ca82d137 Mon Sep 17 00:00:00 2001 From: Tig Date: Wed, 24 Jul 2024 15:09:48 -0600 Subject: [PATCH 23/33] Fixed nullable warnings 7 --- CommunityToolkitExample/Program.cs | 2 +- .../Application/Application.Initialization.cs | 17 +- .../Application/Application.Keyboard.cs | 4 +- Terminal.Gui/Application/Application.Mouse.cs | 22 +- .../Application/Application.Navigation.cs | 197 ++++++++++++++++-- Terminal.Gui/Application/Application.Run.cs | 6 +- .../Application/Application.Toplevel.cs | 142 ------------- Terminal.Gui/Application/Application.cs | 6 +- .../Application/MainLoopSyncContext.cs | 2 +- Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs | 10 +- Terminal.Gui/View/Adornment/ShadowView.cs | 4 +- Terminal.Gui/View/EventArgs.cs | 2 +- Terminal.Gui/View/Layout/Dim.cs | 2 +- Terminal.Gui/View/Layout/DimView.cs | 4 +- Terminal.Gui/View/Layout/PosView.cs | 8 +- Terminal.Gui/View/View.cs | 2 +- Terminal.Gui/Views/TextValidateField.cs | 8 +- Terminal.Gui/Views/Tile.cs | 2 +- UICatalog/Scenarios/Buttons.cs | 4 +- UICatalog/Scenarios/ListViewWithSelection.cs | 2 +- UICatalog/Scenarios/MenuBarScenario.cs | 8 +- UICatalog/UICatalog.cs | 2 +- UnitTests/Application/ApplicationTests.cs | 20 +- UnitTests/Application/KeyboardTests.cs | 2 +- UnitTests/UICatalog/ScenarioTests.cs | 2 +- UnitTests/View/DrawTests.cs | 8 +- UnitTests/Views/MenuBarTests.cs | 19 +- 27 files changed, 263 insertions(+), 244 deletions(-) diff --git a/CommunityToolkitExample/Program.cs b/CommunityToolkitExample/Program.cs index 0d4f21c30..a9ababfec 100644 --- a/CommunityToolkitExample/Program.cs +++ b/CommunityToolkitExample/Program.cs @@ -12,7 +12,7 @@ public static class Program Services = ConfigureServices (); Application.Init (); Application.Run (Services.GetRequiredService ()); - Application.Top.Dispose(); + Application.Top?.Dispose(); Application.Shutdown (); } diff --git a/Terminal.Gui/Application/Application.Initialization.cs b/Terminal.Gui/Application/Application.Initialization.cs index 0d9b2caf5..37b34a358 100644 --- a/Terminal.Gui/Application/Application.Initialization.cs +++ b/Terminal.Gui/Application/Application.Initialization.cs @@ -38,8 +38,8 @@ public static partial class Application // Initialization (Init/Shutdown) [RequiresDynamicCode ("AOT")] public static void Init (ConsoleDriver? driver = null, string? driverName = null) { InternalInit (driver, driverName); } - internal static bool _initialized; - internal static int _mainThreadId = -1; + internal static bool IsInitialized { get; set; } + internal static int MainThreadId { get; set; } = -1; // INTERNAL function for initializing an app with a Toplevel factory object, driver, and mainloop. // @@ -58,12 +58,12 @@ public static partial class Application // Initialization (Init/Shutdown) bool calledViaRunT = false ) { - if (_initialized && driver is null) + if (IsInitialized && driver is null) { return; } - if (_initialized) + if (IsInitialized) { throw new InvalidOperationException ("Init has already been called and must be bracketed by Shutdown."); } @@ -154,9 +154,9 @@ public static partial class Application // Initialization (Init/Shutdown) SynchronizationContext.SetSynchronizationContext (new MainLoopSyncContext ()); SupportedCultures = GetSupportedCultures (); - _mainThreadId = Thread.CurrentThread.ManagedThreadId; - _initialized = true; - InitializedChanged?.Invoke (null, new (in _initialized)); + MainThreadId = Thread.CurrentThread.ManagedThreadId; + bool init = IsInitialized = true; + InitializedChanged?.Invoke (null, new (init)); } private static void Driver_SizeChanged (object? sender, SizeChangedEventArgs e) { OnSizeChanging (e); } @@ -198,7 +198,8 @@ public static partial class Application // Initialization (Init/Shutdown) // TODO: Throw an exception if Init hasn't been called. ResetState (); PrintJsonErrors (); - InitializedChanged?.Invoke (null, new (in _initialized)); + bool init = IsInitialized; + InitializedChanged?.Invoke (null, new (in init)); } /// diff --git a/Terminal.Gui/Application/Application.Keyboard.cs b/Terminal.Gui/Application/Application.Keyboard.cs index 10419bf80..59c78a83e 100644 --- a/Terminal.Gui/Application/Application.Keyboard.cs +++ b/Terminal.Gui/Application/Application.Keyboard.cs @@ -110,7 +110,7 @@ public static partial class Application // Keyboard handling /// if the key was handled. public static bool OnKeyDown (Key keyEvent) { - if (!_initialized) + if (!IsInitialized) { return true; } @@ -210,7 +210,7 @@ public static partial class Application // Keyboard handling /// if the key was handled. public static bool OnKeyUp (Key a) { - if (!_initialized) + if (!IsInitialized) { return true; } diff --git a/Terminal.Gui/Application/Application.Mouse.cs b/Terminal.Gui/Application/Application.Mouse.cs index 713e6375d..be7c38df6 100644 --- a/Terminal.Gui/Application/Application.Mouse.cs +++ b/Terminal.Gui/Application/Application.Mouse.cs @@ -64,7 +64,7 @@ public static partial class Application // Mouse handling } } - private static bool OnGrabbingMouse (View view) + private static bool OnGrabbingMouse (View? view) { if (view is null) { @@ -77,7 +77,7 @@ public static partial class Application // Mouse handling return evArgs.Cancel; } - private static bool OnUnGrabbingMouse (View view) + private static bool OnUnGrabbingMouse (View? view) { if (view is null) { @@ -90,7 +90,7 @@ public static partial class Application // Mouse handling return evArgs.Cancel; } - private static void OnGrabbedMouse (View view) + private static void OnGrabbedMouse (View? view) { if (view is null) { @@ -100,7 +100,7 @@ public static partial class Application // Mouse handling GrabbedMouse?.Invoke (view, new (view)); } - private static void OnUnGrabbedMouse (View view) + private static void OnUnGrabbedMouse (View? view) { if (view is null) { @@ -113,7 +113,7 @@ public static partial class Application // Mouse handling #nullable enable // Used by OnMouseEvent to track the last view that was clicked on. - internal static View? _mouseEnteredView; + internal static View? MouseEnteredView { get; set; } /// Event fired when a mouse move or click occurs. Coordinates are screen relative. /// @@ -166,7 +166,7 @@ public static partial class Application // Mouse handling if ((MouseGrabView.Viewport with { Location = Point.Empty }).Contains (viewRelativeMouseEvent.Position) is false) { // The mouse has moved outside the bounds of the view that grabbed the mouse - _mouseEnteredView?.NewMouseLeaveEvent (mouseEvent); + MouseEnteredView?.NewMouseLeaveEvent (mouseEvent); } //System.Diagnostics.Debug.WriteLine ($"{nme.Flags};{nme.X};{nme.Y};{mouseGrabView}"); @@ -242,16 +242,16 @@ public static partial class Application // Mouse handling return; } - if (_mouseEnteredView is null) + if (MouseEnteredView is null) { - _mouseEnteredView = view; + MouseEnteredView = view; view.NewMouseEnterEvent (me); } - else if (_mouseEnteredView != view) + else if (MouseEnteredView != view) { - _mouseEnteredView.NewMouseLeaveEvent (me); + MouseEnteredView.NewMouseLeaveEvent (me); view.NewMouseEnterEvent (me); - _mouseEnteredView = view; + MouseEnteredView = view; } if (!view.WantMousePositionReports && mouseEvent.Flags == MouseFlags.ReportMousePosition) diff --git a/Terminal.Gui/Application/Application.Navigation.cs b/Terminal.Gui/Application/Application.Navigation.cs index 0bbce67e6..d3f42fd64 100644 --- a/Terminal.Gui/Application/Application.Navigation.cs +++ b/Terminal.Gui/Application/Application.Navigation.cs @@ -1,3 +1,4 @@ +#nullable enable namespace Terminal.Gui; public static partial class Application @@ -6,30 +7,32 @@ public static partial class Application /// Gets the list of the Overlapped children which are not modal from the /// . /// - public static List OverlappedChildren + public static List? OverlappedChildren { get { if (OverlappedTop is { }) { - List _overlappedChildren = new (); + List overlappedChildren = new (); - foreach (Toplevel top in _topLevels) + lock (_topLevels) { - if (top != OverlappedTop && !top.Modal) + foreach (Toplevel top in _topLevels) { - _overlappedChildren.Add (top); + if (top != OverlappedTop && !top.Modal) + { + overlappedChildren.Add (top); + } } } - return _overlappedChildren; + return overlappedChildren; } return null; } } -#nullable enable /// /// The object used for the application on startup which /// is true. @@ -46,7 +49,6 @@ public static partial class Application return null; } } -#nullable restore /// Brings the superview of the most focused overlapped view is on front. public static void BringOverlappedTopToFront () @@ -56,9 +58,9 @@ public static partial class Application return; } - View top = FindTopFromView (Top?.MostFocused); + View? top = FindTopFromView (Top?.MostFocused); - if (top is Toplevel && Top.Subviews.Count > 1 && Top.Subviews [^1] != top) + if (top is Toplevel && Top?.Subviews.Count > 1 && Top.Subviews [^1] != top) { Top.BringSubviewToFront (top); } @@ -68,9 +70,9 @@ public static partial class Application /// The type. /// The strings to exclude. /// The matched view. - public static Toplevel GetTopOverlappedChild (Type type = null, string [] exclude = null) + public static Toplevel? GetTopOverlappedChild (Type? type = null, string []? exclude = null) { - if (OverlappedTop is null) + if (OverlappedChildren is null || OverlappedTop is null) { return null; } @@ -118,7 +120,7 @@ public static partial class Application /// Move to the next Overlapped child from the . public static void OverlappedMoveNext () { - if (OverlappedTop is { } && !Current.Modal) + if (OverlappedTop is { } && !Current!.Modal) { lock (_topLevels) { @@ -133,7 +135,7 @@ public static partial class Application } else if (isOverlapped && _topLevels.Peek () == OverlappedTop) { - MoveCurrent (Top); + MoveCurrent (Top!); break; } @@ -149,7 +151,7 @@ public static partial class Application /// Move to the previous Overlapped child from the . public static void OverlappedMovePrevious () { - if (OverlappedTop is { } && !Current.Modal) + if (OverlappedTop is { } && !Current!.Modal) { lock (_topLevels) { @@ -164,7 +166,7 @@ public static partial class Application } else if (isOverlapped && _topLevels.Peek () == OverlappedTop) { - MoveCurrent (Top); + MoveCurrent (Top!); break; } @@ -184,13 +186,16 @@ public static partial class Application return false; } - foreach (Toplevel top in _topLevels) + lock (_topLevels) { - if (top != Current && top.Visible && (top.NeedsDisplay || top.SubViewNeedsDisplay || top.LayoutNeeded)) + foreach (Toplevel top in _topLevels) { - OverlappedTop.SetSubViewNeedsDisplay (); + if (top != Current && top.Visible && (top.NeedsDisplay || top.SubViewNeedsDisplay || top.LayoutNeeded)) + { + OverlappedTop.SetSubViewNeedsDisplay (); - return true; + return true; + } } } @@ -208,4 +213,154 @@ public static partial class Application return false; } -} \ No newline at end of file + + /// + /// Finds the first Toplevel in the stack that is Visible and who's Frame contains the . + /// + /// + /// + /// + private static Toplevel? FindDeepestTop (Toplevel start, in Point location) + { + if (!start.Frame.Contains (location)) + { + return null; + } + + lock (_topLevels) + { + if (_topLevels is not { Count: > 0 }) + { + return start; + } + + int rx = location.X - start.Frame.X; + int ry = location.Y - start.Frame.Y; + + foreach (Toplevel t in _topLevels) + { + if (t == Current) + { + continue; + } + + if (t != start && t.Visible && t.Frame.Contains (rx, ry)) + { + start = t; + + break; + } + } + } + + return start; + } + + /// + /// Given , returns the first Superview up the chain that is . + /// + private static View? FindTopFromView (View? view) + { + if (view is null) + { + return null; + } + + View top = view.SuperView is { } && view.SuperView != Top + ? view.SuperView + : view; + + while (top?.SuperView is { } && top?.SuperView != Top) + { + top = top!.SuperView; + } + + return top; + } + + /// + /// If the is not the then is moved to the top of + /// the Toplevel stack and made Current. + /// + /// + /// + private static bool MoveCurrent (Toplevel top) + { + // The Current is modal and the top is not modal Toplevel then + // the Current must be moved above the first not modal Toplevel. + if (OverlappedTop is { } + && top != OverlappedTop + && top != Current + && Current?.Modal == true + && !_topLevels.Peek ().Modal) + { + lock (_topLevels) + { + _topLevels.MoveTo (Current, 0, new ToplevelEqualityComparer ()); + } + + var index = 0; + Toplevel [] savedToplevels = _topLevels.ToArray (); + + foreach (Toplevel t in savedToplevels) + { + if (!t!.Modal && t != Current && t != top && t != savedToplevels [index]) + { + lock (_topLevels) + { + _topLevels.MoveTo (top, index, new ToplevelEqualityComparer ()); + } + } + + index++; + } + + return false; + } + + // The Current and the top are both not running Toplevel then + // the top must be moved above the first not running Toplevel. + if (OverlappedTop is { } + && top != OverlappedTop + && top != Current + && Current?.Running == false + && top?.Running == false) + { + lock (_topLevels) + { + _topLevels.MoveTo (Current, 0, new ToplevelEqualityComparer ()); + } + + var index = 0; + + foreach (Toplevel t in _topLevels.ToArray ()) + { + if (!t.Running && t != Current && index > 0) + { + lock (_topLevels) + { + _topLevels.MoveTo (top, index - 1, new ToplevelEqualityComparer ()); + } + } + + index++; + } + + return false; + } + + if ((OverlappedTop is { } && top?.Modal == true && _topLevels.Peek () != top) + || (OverlappedTop is { } && Current != OverlappedTop && Current?.Modal == false && top == OverlappedTop) + || (OverlappedTop is { } && Current?.Modal == false && top != Current) + || (OverlappedTop is { } && Current?.Modal == true && top == OverlappedTop)) + { + lock (_topLevels) + { + _topLevels.MoveTo (top, 0, new ToplevelEqualityComparer ()); + Current = top; + } + } + + return true; + } +} diff --git a/Terminal.Gui/Application/Application.Run.cs b/Terminal.Gui/Application/Application.Run.cs index 9a79af3d4..87bfde4c0 100644 --- a/Terminal.Gui/Application/Application.Run.cs +++ b/Terminal.Gui/Application/Application.Run.cs @@ -333,7 +333,7 @@ public static partial class Application // Run (Begin, Run, End, Stop) public static T Run (Func? errorHandler = null, ConsoleDriver? driver = null) where T : Toplevel, new () { - if (!_initialized) + if (!IsInitialized) { // Init() has NOT been called. InternalInit (driver, null, true); @@ -388,7 +388,7 @@ public static partial class Application // Run (Begin, Run, End, Stop) { ArgumentNullException.ThrowIfNull (view); - if (_initialized) + if (IsInitialized) { if (Driver is null) { @@ -824,7 +824,7 @@ public static partial class Application // Run (Begin, Run, End, Stop) } else { - if (_topLevels.Count > 1 && _topLevels.Peek () == OverlappedTop && OverlappedChildren.Any (t => t.Visible) is { }) + if (_topLevels.Count > 1 && _topLevels.Peek () == OverlappedTop && OverlappedChildren?.Any (t => t.Visible) != null) { OverlappedMoveNext (); } diff --git a/Terminal.Gui/Application/Application.Toplevel.cs b/Terminal.Gui/Application/Application.Toplevel.cs index d45360d64..a050e28f1 100644 --- a/Terminal.Gui/Application/Application.Toplevel.cs +++ b/Terminal.Gui/Application/Application.Toplevel.cs @@ -53,148 +53,6 @@ public static partial class Application // Toplevel handling } } - /// - /// Finds the first Toplevel in the stack that is Visible and who's Frame contains the . - /// - /// - /// - /// - private static Toplevel? FindDeepestTop (Toplevel start, in Point location) - { - if (!start.Frame.Contains (location)) - { - return null; - } - - if (_topLevels is { Count: > 0 }) - { - int rx = location.X - start.Frame.X; - int ry = location.Y - start.Frame.Y; - - foreach (Toplevel t in _topLevels) - { - if (t != Current) - { - if (t != start && t.Visible && t.Frame.Contains (rx, ry)) - { - start = t; - - break; - } - } - } - } - - return start; - } - - /// - /// Given , returns the first Superview up the chain that is . - /// - private static View? FindTopFromView (View? view) - { - if (view is null) - { - return null; - } - - View top = view.SuperView is { } && view.SuperView != Top - ? view.SuperView - : view; - - while (top?.SuperView is { } && top?.SuperView != Top) - { - top = top!.SuperView; - } - - return top; - } - - /// - /// If the is not the then is moved to the top of the Toplevel stack and made Current. - /// - /// - /// - private static bool MoveCurrent (Toplevel top) - { - // The Current is modal and the top is not modal Toplevel then - // the Current must be moved above the first not modal Toplevel. - if (OverlappedTop is { } - && top != OverlappedTop - && top != Current - && Current?.Modal == true - && !_topLevels.Peek ().Modal) - { - lock (_topLevels) - { - _topLevels.MoveTo (Current, 0, new ToplevelEqualityComparer ()); - } - - var index = 0; - Toplevel [] savedToplevels = _topLevels.ToArray (); - - foreach (Toplevel t in savedToplevels) - { - if (!t!.Modal && t != Current && t != top && t != savedToplevels [index]) - { - lock (_topLevels) - { - _topLevels.MoveTo (top, index, new ToplevelEqualityComparer ()); - } - } - - index++; - } - - return false; - } - - // The Current and the top are both not running Toplevel then - // the top must be moved above the first not running Toplevel. - if (OverlappedTop is { } - && top != OverlappedTop - && top != Current - && Current?.Running == false - && top?.Running == false) - { - lock (_topLevels) - { - _topLevels.MoveTo (Current, 0, new ToplevelEqualityComparer ()); - } - - var index = 0; - - foreach (Toplevel t in _topLevels.ToArray ()) - { - if (!t.Running && t != Current && index > 0) - { - lock (_topLevels) - { - _topLevels.MoveTo (top, index - 1, new ToplevelEqualityComparer ()); - } - } - - index++; - } - - return false; - } - - if ((OverlappedTop is { } && top?.Modal == true && _topLevels.Peek () != top) - || (OverlappedTop is { } && Current != OverlappedTop && Current?.Modal == false && top == OverlappedTop) - || (OverlappedTop is { } && Current?.Modal == false && top != Current) - || (OverlappedTop is { } && Current?.Modal == true && top == OverlappedTop)) - { - lock (_topLevels) - { - _topLevels.MoveTo (top, 0, new ToplevelEqualityComparer ()); - Current = top; - } - } - - return true; - } - /// Invoked when the terminal's size changed. The new size of the terminal is provided. /// /// Event handlers can set to to prevent diff --git a/Terminal.Gui/Application/Application.cs b/Terminal.Gui/Application/Application.cs index 76c05922b..75fbd7191 100644 --- a/Terminal.Gui/Application/Application.cs +++ b/Terminal.Gui/Application/Application.cs @@ -84,7 +84,7 @@ public static partial class Application // MainLoop stuff MainLoop?.Dispose (); MainLoop = null; - _mainThreadId = -1; + MainThreadId = -1; Iteration = null; EndAfterFirstIteration = false; @@ -108,10 +108,10 @@ public static partial class Application NotifyNewRunState = null; NotifyStopRunState = null; MouseGrabView = null; - _initialized = false; + IsInitialized = false; // Mouse - _mouseEnteredView = null; + MouseEnteredView = null; WantContinuousButtonPressedView = null; MouseEvent = null; GrabbedMouse = null; diff --git a/Terminal.Gui/Application/MainLoopSyncContext.cs b/Terminal.Gui/Application/MainLoopSyncContext.cs index 5290a2076..749c76268 100644 --- a/Terminal.Gui/Application/MainLoopSyncContext.cs +++ b/Terminal.Gui/Application/MainLoopSyncContext.cs @@ -23,7 +23,7 @@ internal sealed class MainLoopSyncContext : SynchronizationContext //_mainLoop.Driver.Wakeup (); public override void Send (SendOrPostCallback d, object state) { - if (Thread.CurrentThread.ManagedThreadId == Application._mainThreadId) + if (Thread.CurrentThread.ManagedThreadId == Application.MainThreadId) { d (state); } diff --git a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs index 5e9a7dad3..e99521d1e 100644 --- a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs @@ -351,7 +351,7 @@ public abstract class ConsoleDriver { Contents [row, c].IsDirty = true; } - _dirtyLines [row] = true; + _dirtyLines! [row] = true; } } } @@ -380,7 +380,7 @@ public abstract class ConsoleDriver Rune = (rune != default ? rune : (Rune)' '), Attribute = CurrentAttribute, IsDirty = true }; - _dirtyLines [r] = true; + _dirtyLines! [r] = true; } } } @@ -561,7 +561,7 @@ public abstract class ConsoleDriver #region Mouse and Keyboard /// Event fired when a key is pressed down. This is a precursor to . - public event EventHandler KeyDown; + public event EventHandler? KeyDown; /// /// Called when a key is pressed down. Fires the event. This is a precursor to @@ -575,7 +575,7 @@ public abstract class ConsoleDriver /// Drivers that do not support key release events will fire this event after processing is /// complete. /// - public event EventHandler KeyUp; + public event EventHandler? KeyUp; /// Called when a key is released. Fires the event. /// @@ -586,7 +586,7 @@ public abstract class ConsoleDriver public void OnKeyUp (Key a) { KeyUp?.Invoke (this, a); } /// Event fired when a mouse event occurs. - public event EventHandler MouseEvent; + public event EventHandler? MouseEvent; /// Called when a mouse event occurs. Fires the event. /// diff --git a/Terminal.Gui/View/Adornment/ShadowView.cs b/Terminal.Gui/View/Adornment/ShadowView.cs index ad06dc754..e6009759a 100644 --- a/Terminal.Gui/View/Adornment/ShadowView.cs +++ b/Terminal.Gui/View/Adornment/ShadowView.cs @@ -109,7 +109,7 @@ internal class ShadowView : View for (int i = screen.X; i < screen.X + screen.Width - 1; i++) { Driver.Move (i, screen.Y); - Driver.AddRune (Driver.Contents [screen.Y, i].Rune); + Driver.AddRune (Driver.Contents! [screen.Y, i].Rune); } } @@ -133,7 +133,7 @@ internal class ShadowView : View for (int i = screen.Y; i < screen.Y + viewport.Height; i++) { Driver.Move (screen.X, i); - Driver.AddRune (Driver.Contents [i, screen.X].Rune); + Driver.AddRune (Driver.Contents! [i, screen.X].Rune); } } } diff --git a/Terminal.Gui/View/EventArgs.cs b/Terminal.Gui/View/EventArgs.cs index 03309a0f5..1de2347c6 100644 --- a/Terminal.Gui/View/EventArgs.cs +++ b/Terminal.Gui/View/EventArgs.cs @@ -11,7 +11,7 @@ public class EventArgs : EventArgs where T : notnull /// Initializes a new instance of the class. /// The current value of the property. /// The type of the value. - public EventArgs (ref readonly T currentValue) { CurrentValue = currentValue; } + public EventArgs (in T currentValue) { CurrentValue = currentValue; } /// The current value of the property. public T CurrentValue { get; } diff --git a/Terminal.Gui/View/Layout/Dim.cs b/Terminal.Gui/View/Layout/Dim.cs index 3102d5d3c..f6f50f798 100644 --- a/Terminal.Gui/View/Layout/Dim.cs +++ b/Terminal.Gui/View/Layout/Dim.cs @@ -257,7 +257,7 @@ public abstract class Dim } var newDim = new DimCombine (AddOrSubtract.Subtract, left, right); - (left as DimView)?.Target.SetNeedsLayout (); + (left as DimView)?.Target?.SetNeedsLayout (); return newDim; } diff --git a/Terminal.Gui/View/Layout/DimView.cs b/Terminal.Gui/View/Layout/DimView.cs index e95efd4fb..7a7568a95 100644 --- a/Terminal.Gui/View/Layout/DimView.cs +++ b/Terminal.Gui/View/Layout/DimView.cs @@ -52,8 +52,8 @@ public class DimView : Dim { return Dimension switch { - Dimension.Height => Target.Frame.Height, - Dimension.Width => Target.Frame.Width, + Dimension.Height => Target!.Frame.Height, + Dimension.Width => Target!.Frame.Width, _ => 0 }; } diff --git a/Terminal.Gui/View/Layout/PosView.cs b/Terminal.Gui/View/Layout/PosView.cs index 8ceba980f..a46f6898a 100644 --- a/Terminal.Gui/View/Layout/PosView.cs +++ b/Terminal.Gui/View/Layout/PosView.cs @@ -47,10 +47,10 @@ public class PosView (View? view, Side side) : Pos { return Side switch { - Side.Left => Target.Frame.X, - Side.Top => Target.Frame.Y, - Side.Right => Target.Frame.Right, - Side.Bottom => Target.Frame.Bottom, + Side.Left => Target!.Frame.X, + Side.Top => Target!.Frame.Y, + Side.Right => Target!.Frame.Right, + Side.Bottom => Target!.Frame.Bottom, _ => 0 }; } diff --git a/Terminal.Gui/View/View.cs b/Terminal.Gui/View/View.cs index 68eb1853f..d340a5aa4 100644 --- a/Terminal.Gui/View/View.cs +++ b/Terminal.Gui/View/View.cs @@ -490,7 +490,7 @@ public partial class View : Responder, ISupportInitializeNotification /// Called when the has been changed. Invokes the event. protected void OnTitleChanged () { - TitleChanged?.Invoke (this, new (ref _title)); + TitleChanged?.Invoke (this, new (in _title)); } /// diff --git a/Terminal.Gui/Views/TextValidateField.cs b/Terminal.Gui/Views/TextValidateField.cs index 045f6df3f..b0df130b3 100644 --- a/Terminal.Gui/Views/TextValidateField.cs +++ b/Terminal.Gui/Views/TextValidateField.cs @@ -206,7 +206,7 @@ namespace Terminal.Gui if (result) { - OnTextChanged (new EventArgs (ref oldValue)); + OnTextChanged (new EventArgs (in oldValue)); } return result; @@ -220,7 +220,7 @@ namespace Terminal.Gui if (result) { - OnTextChanged (new EventArgs (ref oldValue)); + OnTextChanged (new EventArgs (in oldValue)); } return result; @@ -333,7 +333,7 @@ namespace Terminal.Gui { string oldValue = Text; _text.RemoveAt (pos); - OnTextChanged (new EventArgs (ref oldValue)); + OnTextChanged (new EventArgs (in oldValue)); } return true; @@ -349,7 +349,7 @@ namespace Terminal.Gui { string oldValue = Text; _text.Insert (pos, (Rune)ch); - OnTextChanged (new EventArgs (ref oldValue)); + OnTextChanged (new EventArgs (in oldValue)); return true; } diff --git a/Terminal.Gui/Views/Tile.cs b/Terminal.Gui/Views/Tile.cs index ebc4b9f59..5224db8b4 100644 --- a/Terminal.Gui/Views/Tile.cs +++ b/Terminal.Gui/Views/Tile.cs @@ -62,7 +62,7 @@ public class Tile /// The new to be replaced. public virtual void OnTitleChanged (string oldTitle, string newTitle) { - var args = new EventArgs (ref newTitle); + var args = new EventArgs (in newTitle); TitleChanged?.Invoke (this, args); } diff --git a/UICatalog/Scenarios/Buttons.cs b/UICatalog/Scenarios/Buttons.cs index aab815a96..2ea67238e 100644 --- a/UICatalog/Scenarios/Buttons.cs +++ b/UICatalog/Scenarios/Buttons.cs @@ -527,8 +527,8 @@ public class Buttons : Scenario } _value = value; - _number.Text = _value.ToString (); - ValueChanged?.Invoke (this, new (ref _value)); + _number.Text = _value.ToString ()!; + ValueChanged?.Invoke (this, new (in _value)); } } diff --git a/UICatalog/Scenarios/ListViewWithSelection.cs b/UICatalog/Scenarios/ListViewWithSelection.cs index afcd8a542..27e1bf5d2 100644 --- a/UICatalog/Scenarios/ListViewWithSelection.cs +++ b/UICatalog/Scenarios/ListViewWithSelection.cs @@ -205,7 +205,7 @@ public class ListViewWithSelection : Scenario /// public event NotifyCollectionChangedEventHandler CollectionChanged; - public int Count => Scenarios != null ? Scenarios.Count : 0; + public int Count => Scenarios?.Count ?? 0; public int Length { get; private set; } public bool SuspendCollectionChangedEvent { get => throw new System.NotImplementedException (); set => throw new System.NotImplementedException (); } diff --git a/UICatalog/Scenarios/MenuBarScenario.cs b/UICatalog/Scenarios/MenuBarScenario.cs index 73c767f79..b9c6bee67 100644 --- a/UICatalog/Scenarios/MenuBarScenario.cs +++ b/UICatalog/Scenarios/MenuBarScenario.cs @@ -1,5 +1,6 @@ using System; using Terminal.Gui; +using static System.Runtime.InteropServices.JavaScript.JSType; namespace UICatalog.Scenarios; @@ -64,14 +65,17 @@ public class MenuBarScenario : Scenario menuBar.Key = KeyCode.F9; menuBar.Title = "TestMenuBar"; - bool fnAction (string s) + bool FnAction (string s) { _lastAction.Text = s; return true; } + + // Declare a variable for the function + Func fnActionVariable = FnAction; - menuBar.EnableForDesign ((Func)fnAction); + menuBar.EnableForDesign (ref fnActionVariable); menuBar.MenuOpening += (s, e) => { diff --git a/UICatalog/UICatalog.cs b/UICatalog/UICatalog.cs index a2cc9a474..a9014d017 100644 --- a/UICatalog/UICatalog.cs +++ b/UICatalog/UICatalog.cs @@ -104,7 +104,7 @@ internal class UICatalogApp // If no driver is provided, the default driver is used. Option driverOption = new Option ("--driver", "The ConsoleDriver to use.").FromAmong ( Application.GetDriverTypes () - .Select (d => d.Name) + .Select (d => d!.Name) .ToArray () ); diff --git a/UnitTests/Application/ApplicationTests.cs b/UnitTests/Application/ApplicationTests.cs index f8f1f8029..c600254a2 100644 --- a/UnitTests/Application/ApplicationTests.cs +++ b/UnitTests/Application/ApplicationTests.cs @@ -162,7 +162,7 @@ public class ApplicationTests // Set some values Application.Init (driverName: driverType.Name); - Application._initialized = true; + Application.IsInitialized = true; // Reset Application.ResetState (); @@ -191,12 +191,12 @@ public class ApplicationTests Assert.Null (Application.OverlappedTop); // Internal properties - Assert.False (Application._initialized); + Assert.False (Application.IsInitialized); Assert.Equal (Application.GetSupportedCultures (), Application.SupportedCultures); Assert.False (Application._forceFakeConsole); - Assert.Equal (-1, Application._mainThreadId); + Assert.Equal (-1, Application.MainThreadId); Assert.Empty (Application._topLevels); - Assert.Null (Application._mouseEnteredView); + Assert.Null (Application.MouseEnteredView); // Keyboard Assert.Empty (Application.GetViewKeyBindings ()); @@ -218,12 +218,12 @@ public class ApplicationTests CheckReset (); // Set the values that can be set - Application._initialized = true; + Application.IsInitialized = true; Application._forceFakeConsole = true; - Application._mainThreadId = 1; + Application.MainThreadId = 1; //Application._topLevels = new List (); - Application._mouseEnteredView = new (); + Application.MouseEnteredView = new (); //Application.SupportedCultures = new List (); Application.Force16Colors = true; @@ -237,7 +237,7 @@ public class ApplicationTests //Application.OverlappedChildren = new List (); //Application.OverlappedTop = - Application._mouseEnteredView = new (); + Application.MouseEnteredView = new (); //Application.WantContinuousButtonPressedView = new View (); @@ -413,7 +413,7 @@ public class ApplicationTests [AutoInitShutdown] public void Internal_Properties_Correct () { - Assert.True (Application._initialized); + Assert.True (Application.IsInitialized); Assert.Null (Application.Top); RunState rs = Application.Begin (new ()); Assert.Equal (Application.Top, rs.Toplevel); @@ -1206,7 +1206,7 @@ public class ApplicationTests Thread.Sleep ((int)timeoutTime / 10); // Worst case scenario - something went wrong - if (Application._initialized && iteration > 25) + if (Application.IsInitialized && iteration > 25) { _output.WriteLine ($"Too many iterations ({iteration}): Calling Application.RequestStop."); Application.RequestStop (); diff --git a/UnitTests/Application/KeyboardTests.cs b/UnitTests/Application/KeyboardTests.cs index 4dfb5b11a..a61519037 100644 --- a/UnitTests/Application/KeyboardTests.cs +++ b/UnitTests/Application/KeyboardTests.cs @@ -169,7 +169,7 @@ public class KeyboardTests _output.WriteLine ("Iteration: {0}", iteration); iteration++; Assert.True (iteration < 2, "Too many iterations, something is wrong."); - if (Application._initialized) + if (Application.IsInitialized) { _output.WriteLine (" Pressing QuitKey"); Application.OnKeyDown (Application.QuitKey); diff --git a/UnitTests/UICatalog/ScenarioTests.cs b/UnitTests/UICatalog/ScenarioTests.cs index 22a9e25c6..5e0dc2a5c 100644 --- a/UnitTests/UICatalog/ScenarioTests.cs +++ b/UnitTests/UICatalog/ScenarioTests.cs @@ -115,7 +115,7 @@ public class ScenarioTests : TestsAllViews void OnApplicationOnIteration (object s, IterationEventArgs a) { - if (Application._initialized) + if (Application.IsInitialized) { // Press QuitKey //_output.WriteLine ($"Forcing Quit with {Application.QuitKey}"); diff --git a/UnitTests/View/DrawTests.cs b/UnitTests/View/DrawTests.cs index 52b4659f8..19ed80383 100644 --- a/UnitTests/View/DrawTests.cs +++ b/UnitTests/View/DrawTests.cs @@ -48,16 +48,16 @@ public class DrawTests (ITestOutputHelper _output) view.Draw (); // Only valid location w/in Viewport is 0, 0 (view) - 2, 2 (screen) - Assert.Equal ((Rune)' ', Application.Driver?.Contents [2, 2].Rune); + Assert.Equal ((Rune)' ', Application.Driver?.Contents! [2, 2].Rune); view.AddRune (0, 0, Rune.ReplacementChar); - Assert.Equal (Rune.ReplacementChar, Application.Driver?.Contents [2, 2].Rune); + Assert.Equal (Rune.ReplacementChar, Application.Driver?.Contents! [2, 2].Rune); view.AddRune (-1, -1, Rune.ReplacementChar); - Assert.Equal ((Rune)'M', Application.Driver?.Contents [1, 1].Rune); + Assert.Equal ((Rune)'M', Application.Driver?.Contents! [1, 1].Rune); view.AddRune (1, 1, Rune.ReplacementChar); - Assert.Equal ((Rune)'M', Application.Driver?.Contents [3, 3].Rune); + Assert.Equal ((Rune)'M', Application.Driver?.Contents! [3, 3].Rune); View.Diagnostics = ViewDiagnosticFlags.Off; } diff --git a/UnitTests/Views/MenuBarTests.cs b/UnitTests/Views/MenuBarTests.cs index 6f9de4683..76598902c 100644 --- a/UnitTests/Views/MenuBarTests.cs +++ b/UnitTests/Views/MenuBarTests.cs @@ -1301,15 +1301,16 @@ wo var menu = new MenuBar (); - menu.EnableForDesign ( - new Func ( - s => - { - miAction = s as string; + bool FnAction (string s) + { + miAction = s; - return true; - }) - ); + return true; + } + // Declare a variable for the function + Func fnActionVariable = FnAction; + + menu.EnableForDesign (ref fnActionVariable); menu.Key = KeyCode.F9; menu.MenuOpening += (s, e) => mbiCurrent = e.CurrentMenu; @@ -1329,7 +1330,7 @@ wo foreach (KeyCode key in keys) { Assert.True (top.NewKeyDownEvent (new (key))); - Application.MainLoop.RunIteration (); + Application.MainLoop!.RunIteration (); } Assert.Equal (expectedAction, miAction); From f37ec5e04f314385859d8c00285283d24a4b1c96 Mon Sep 17 00:00:00 2001 From: Tig Date: Wed, 24 Jul 2024 15:42:04 -0600 Subject: [PATCH 24/33] Moved Overlapped stuff to ApplicationOverlap static class. Fixed nullable warnings. --- .../Application/Application .Screen.cs | 45 ++ .../Application/Application.Initialization.cs | 8 +- .../Application/Application.Keyboard.cs | 14 +- Terminal.Gui/Application/Application.Mouse.cs | 12 +- .../Application/Application.Navigation.cs | 450 +++++------------ .../Application/Application.Overlapped.cs | 465 +++++++++++++----- Terminal.Gui/Application/Application.Run.cs | 116 ++--- .../Application/Application.Toplevel.cs | 57 +-- Terminal.Gui/Application/Application.cs | 4 +- Terminal.Gui/View/Adornment/Border.cs | 2 +- Terminal.Gui/View/ViewSubViews.cs | 4 +- Terminal.Gui/Views/TileView.cs | 2 +- Terminal.Gui/Views/Toplevel.cs | 10 +- Terminal.Gui/Views/ToplevelOverlapped.cs | 2 +- .../Scenarios/BackgroundWorkerCollection.cs | 26 +- UnitTests/Application/ApplicationTests.cs | 24 +- UnitTests/Views/OverlappedTests.cs | 269 +++++----- 17 files changed, 758 insertions(+), 752 deletions(-) create mode 100644 Terminal.Gui/Application/Application .Screen.cs diff --git a/Terminal.Gui/Application/Application .Screen.cs b/Terminal.Gui/Application/Application .Screen.cs new file mode 100644 index 000000000..7770ae13c --- /dev/null +++ b/Terminal.Gui/Application/Application .Screen.cs @@ -0,0 +1,45 @@ +#nullable enable +namespace Terminal.Gui; + +public static partial class Application // Screen related stuff +{ + /// Invoked when the terminal's size changed. The new size of the terminal is provided. + /// + /// Event handlers can set to to prevent + /// from changing it's size to match the new terminal size. + /// + public static event EventHandler? SizeChanging; + + /// + /// Called when the application's size changes. Sets the size of all s and fires the + /// event. + /// + /// The new size. + /// if the size was changed. + public static bool OnSizeChanging (SizeChangedEventArgs args) + { + SizeChanging?.Invoke (null, args); + + if (args.Cancel || args.Size is null) + { + return false; + } + + foreach (Toplevel t in TopLevels) + { + t.SetRelativeLayout (args.Size.Value); + t.LayoutSubviews (); + t.PositionToplevels (); + t.OnSizeChanging (new (args.Size)); + + if (PositionCursor (t)) + { + Driver?.UpdateCursor (); + } + } + + Refresh (); + + return true; + } +} diff --git a/Terminal.Gui/Application/Application.Initialization.cs b/Terminal.Gui/Application/Application.Initialization.cs index 37b34a358..a2aacaab5 100644 --- a/Terminal.Gui/Application/Application.Initialization.cs +++ b/Terminal.Gui/Application/Application.Initialization.cs @@ -146,10 +146,10 @@ public static partial class Application // Initialization (Init/Shutdown) ); } - Driver.SizeChanged += (s, args) => OnSizeChanging (args); - Driver.KeyDown += (s, args) => OnKeyDown (args); - Driver.KeyUp += (s, args) => OnKeyUp (args); - Driver.MouseEvent += (s, args) => OnMouseEvent (args); + Driver.SizeChanged += Driver_SizeChanged; + Driver.KeyDown += Driver_KeyDown; + Driver.KeyUp += Driver_KeyUp; + Driver.MouseEvent += Driver_MouseEvent; SynchronizationContext.SetSynchronizationContext (new MainLoopSyncContext ()); diff --git a/Terminal.Gui/Application/Application.Keyboard.cs b/Terminal.Gui/Application/Application.Keyboard.cs index 59c78a83e..d26dcd432 100644 --- a/Terminal.Gui/Application/Application.Keyboard.cs +++ b/Terminal.Gui/Application/Application.Keyboard.cs @@ -122,7 +122,7 @@ public static partial class Application // Keyboard handling return true; } - foreach (Toplevel topLevel in _topLevels.ToList ()) + foreach (Toplevel topLevel in TopLevels.ToList ()) { if (topLevel.NewKeyDownEvent (keyEvent)) { @@ -222,7 +222,7 @@ public static partial class Application // Keyboard handling return true; } - foreach (Toplevel topLevel in _topLevels.ToList ()) + foreach (Toplevel topLevel in TopLevels.ToList ()) { if (topLevel.NewKeyUpEvent (a)) { @@ -302,7 +302,7 @@ public static partial class Application // Keyboard handling Command.QuitToplevel, // TODO: IRunnable: Rename to Command.Quit to make more generic. () => { - if (OverlappedTop is { }) + if (ApplicationOverlapped.OverlappedTop is { }) { RequestStop (Current!); } @@ -330,7 +330,7 @@ public static partial class Application // Keyboard handling () => { // TODO: Move this method to Application.Navigation.cs - ViewNavigation.MoveNextView (); + ApplicationNavigation.MoveNextView (); return true; } @@ -341,7 +341,7 @@ public static partial class Application // Keyboard handling () => { // TODO: Move this method to Application.Navigation.cs - ViewNavigation.MovePreviousView (); + ApplicationNavigation.MovePreviousView (); return true; } @@ -352,7 +352,7 @@ public static partial class Application // Keyboard handling () => { // TODO: Move this method to Application.Navigation.cs - ViewNavigation.MoveNextViewOrTop (); + ApplicationNavigation.MoveNextViewOrTop (); return true; } @@ -363,7 +363,7 @@ public static partial class Application // Keyboard handling () => { // TODO: Move this method to Application.Navigation.cs - ViewNavigation.MovePreviousViewOrTop (); + ApplicationNavigation.MovePreviousViewOrTop (); return true; } diff --git a/Terminal.Gui/Application/Application.Mouse.cs b/Terminal.Gui/Application/Application.Mouse.cs index be7c38df6..2c497b761 100644 --- a/Terminal.Gui/Application/Application.Mouse.cs +++ b/Terminal.Gui/Application/Application.Mouse.cs @@ -187,20 +187,20 @@ public static partial class Application // Mouse handling if (view is not Adornment) { - if ((view is null || view == OverlappedTop) + if ((view is null || view == ApplicationOverlapped.OverlappedTop) && Current is { Modal: false } - && OverlappedTop != null + && ApplicationOverlapped.OverlappedTop != null && mouseEvent.Flags != MouseFlags.ReportMousePosition && mouseEvent.Flags != 0) { // This occurs when there are multiple overlapped "tops" // E.g. "Mdi" - in the Background Worker Scenario - View? top = FindDeepestTop (Top!, mouseEvent.Position); + View? top = ApplicationOverlapped.FindDeepestTop (Top!, mouseEvent.Position); view = View.FindDeepestView (top, mouseEvent.Position); - if (view is { } && view != OverlappedTop && top != Current && top is { }) + if (view is { } && view != ApplicationOverlapped.OverlappedTop && top != Current && top is { }) { - MoveCurrent ((Toplevel)top); + ApplicationOverlapped.MoveCurrent ((Toplevel)top); } } } @@ -295,7 +295,7 @@ public static partial class Application // Mouse handling }; } - BringOverlappedTopToFront (); + ApplicationOverlapped.BringOverlappedTopToFront (); } #endregion Mouse handling diff --git a/Terminal.Gui/Application/Application.Navigation.cs b/Terminal.Gui/Application/Application.Navigation.cs index d3f42fd64..44bc6e99a 100644 --- a/Terminal.Gui/Application/Application.Navigation.cs +++ b/Terminal.Gui/Application/Application.Navigation.cs @@ -1,366 +1,162 @@ #nullable enable namespace Terminal.Gui; -public static partial class Application +internal static class ApplicationNavigation { /// - /// Gets the list of the Overlapped children which are not modal from the - /// . + /// Gets the deepest focused subview of the specified . /// - public static List? OverlappedChildren - { - get - { - if (OverlappedTop is { }) - { - List overlappedChildren = new (); - - lock (_topLevels) - { - foreach (Toplevel top in _topLevels) - { - if (top != OverlappedTop && !top.Modal) - { - overlappedChildren.Add (top); - } - } - } - - return overlappedChildren; - } - - return null; - } - } - - /// - /// The object used for the application on startup which - /// is true. - /// - public static Toplevel? OverlappedTop - { - get - { - if (Top is { IsOverlappedContainer: true }) - { - return Top; - } - - return null; - } - } - - /// Brings the superview of the most focused overlapped view is on front. - public static void BringOverlappedTopToFront () - { - if (OverlappedTop is { }) - { - return; - } - - View? top = FindTopFromView (Top?.MostFocused); - - if (top is Toplevel && Top?.Subviews.Count > 1 && Top.Subviews [^1] != top) - { - Top.BringSubviewToFront (top); - } - } - - /// Gets the current visible Toplevel overlapped child that matches the arguments pattern. - /// The type. - /// The strings to exclude. - /// The matched view. - public static Toplevel? GetTopOverlappedChild (Type? type = null, string []? exclude = null) - { - if (OverlappedChildren is null || OverlappedTop is null) - { - return null; - } - - foreach (Toplevel top in OverlappedChildren) - { - if (type is { } && top.GetType () == type && exclude?.Contains (top.Data.ToString ()) == false) - { - return top; - } - - if ((type is { } && top.GetType () != type) || exclude?.Contains (top.Data.ToString ()) == true) - { - continue; - } - - return top; - } - - return null; - } - - /// - /// Move to the next Overlapped child from the and set it as the if - /// it is not already. - /// - /// + /// /// - public static bool MoveToOverlappedChild (Toplevel top) - { - if (top.Visible && OverlappedTop is { } && Current?.Modal == false) - { - lock (_topLevels) - { - _topLevels.MoveTo (top, 0, new ToplevelEqualityComparer ()); - Current = top; - } - - return true; - } - - return false; - } - - /// Move to the next Overlapped child from the . - public static void OverlappedMoveNext () - { - if (OverlappedTop is { } && !Current!.Modal) - { - lock (_topLevels) - { - _topLevels.MoveNext (); - var isOverlapped = false; - - while (_topLevels.Peek () == OverlappedTop || !_topLevels.Peek ().Visible) - { - if (!isOverlapped && _topLevels.Peek () == OverlappedTop) - { - isOverlapped = true; - } - else if (isOverlapped && _topLevels.Peek () == OverlappedTop) - { - MoveCurrent (Top!); - - break; - } - - _topLevels.MoveNext (); - } - - Current = _topLevels.Peek (); - } - } - } - - /// Move to the previous Overlapped child from the . - public static void OverlappedMovePrevious () - { - if (OverlappedTop is { } && !Current!.Modal) - { - lock (_topLevels) - { - _topLevels.MovePrevious (); - var isOverlapped = false; - - while (_topLevels.Peek () == OverlappedTop || !_topLevels.Peek ().Visible) - { - if (!isOverlapped && _topLevels.Peek () == OverlappedTop) - { - isOverlapped = true; - } - else if (isOverlapped && _topLevels.Peek () == OverlappedTop) - { - MoveCurrent (Top!); - - break; - } - - _topLevels.MovePrevious (); - } - - Current = _topLevels.Peek (); - } - } - } - - private static bool OverlappedChildNeedsDisplay () - { - if (OverlappedTop is null) - { - return false; - } - - lock (_topLevels) - { - foreach (Toplevel top in _topLevels) - { - if (top != Current && top.Visible && (top.NeedsDisplay || top.SubViewNeedsDisplay || top.LayoutNeeded)) - { - OverlappedTop.SetSubViewNeedsDisplay (); - - return true; - } - } - } - - return false; - } - - private static bool SetCurrentOverlappedAsTop () - { - if (OverlappedTop is null && Current != Top && Current?.SuperView is null && Current?.Modal == false) - { - Top = Current; - - return true; - } - - return false; - } - - /// - /// Finds the first Toplevel in the stack that is Visible and who's Frame contains the . - /// - /// - /// - /// - private static Toplevel? FindDeepestTop (Toplevel start, in Point location) - { - if (!start.Frame.Contains (location)) - { - return null; - } - - lock (_topLevels) - { - if (_topLevels is not { Count: > 0 }) - { - return start; - } - - int rx = location.X - start.Frame.X; - int ry = location.Y - start.Frame.Y; - - foreach (Toplevel t in _topLevels) - { - if (t == Current) - { - continue; - } - - if (t != start && t.Visible && t.Frame.Contains (rx, ry)) - { - start = t; - - break; - } - } - } - - return start; - } - - /// - /// Given , returns the first Superview up the chain that is . - /// - private static View? FindTopFromView (View? view) + internal static View? GetDeepestFocusedSubview (View? view) { if (view is null) { return null; } - View top = view.SuperView is { } && view.SuperView != Top - ? view.SuperView - : view; - - while (top?.SuperView is { } && top?.SuperView != Top) + foreach (View v in view.Subviews) { - top = top!.SuperView; + if (v.HasFocus) + { + return GetDeepestFocusedSubview (v); + } } - return top; + return view; } /// - /// If the is not the then is moved to the top of - /// the Toplevel stack and made Current. + /// Sets the focus to the next view in the list. If the last view is focused, the first view is focused. /// - /// - /// - private static bool MoveCurrent (Toplevel top) + /// + /// + internal static void FocusNearestView (IEnumerable? viewsInTabIndexes, View.NavigationDirection direction) { - // The Current is modal and the top is not modal Toplevel then - // the Current must be moved above the first not modal Toplevel. - if (OverlappedTop is { } - && top != OverlappedTop - && top != Current - && Current?.Modal == true - && !_topLevels.Peek ().Modal) + if (viewsInTabIndexes is null) { - lock (_topLevels) + return; + } + + var found = false; + var focusProcessed = false; + var idx = 0; + + foreach (View v in viewsInTabIndexes) + { + if (v == Application.Current) { - _topLevels.MoveTo (Current, 0, new ToplevelEqualityComparer ()); + found = true; } - var index = 0; - Toplevel [] savedToplevels = _topLevels.ToArray (); - - foreach (Toplevel t in savedToplevels) + if (found && v != Application.Current) { - if (!t!.Modal && t != Current && t != top && t != savedToplevels [index]) + if (direction == View.NavigationDirection.Forward) { - lock (_topLevels) - { - _topLevels.MoveTo (top, index, new ToplevelEqualityComparer ()); - } + Application.Current!.SuperView?.FocusNext (); + } + else + { + Application.Current!.SuperView?.FocusPrev (); } - index++; - } + focusProcessed = true; - return false; - } - - // The Current and the top are both not running Toplevel then - // the top must be moved above the first not running Toplevel. - if (OverlappedTop is { } - && top != OverlappedTop - && top != Current - && Current?.Running == false - && top?.Running == false) - { - lock (_topLevels) - { - _topLevels.MoveTo (Current, 0, new ToplevelEqualityComparer ()); - } - - var index = 0; - - foreach (Toplevel t in _topLevels.ToArray ()) - { - if (!t.Running && t != Current && index > 0) + if (Application.Current.SuperView?.Focused is { } && Application.Current.SuperView.Focused != Application.Current) { - lock (_topLevels) - { - _topLevels.MoveTo (top, index - 1, new ToplevelEqualityComparer ()); - } + return; } - - index++; } - - return false; - } - - if ((OverlappedTop is { } && top?.Modal == true && _topLevels.Peek () != top) - || (OverlappedTop is { } && Current != OverlappedTop && Current?.Modal == false && top == OverlappedTop) - || (OverlappedTop is { } && Current?.Modal == false && top != Current) - || (OverlappedTop is { } && Current?.Modal == true && top == OverlappedTop)) - { - lock (_topLevels) + else if (found && !focusProcessed && idx == viewsInTabIndexes.Count () - 1) { - _topLevels.MoveTo (top, 0, new ToplevelEqualityComparer ()); - Current = top; + viewsInTabIndexes.ToList () [0].SetFocus (); } + + idx++; + } + } + /// + /// Moves the focus to + /// + internal static void MoveNextView () + { + View? old = GetDeepestFocusedSubview (Application.Current!.Focused); + + if (!Application.Current.FocusNext ()) + { + Application.Current.FocusNext (); } - return true; + if (old != Application.Current.Focused && old != Application.Current.Focused?.Focused) + { + old?.SetNeedsDisplay (); + Application.Current.Focused?.SetNeedsDisplay (); + } + else + { + FocusNearestView (Application.Current.SuperView?.TabIndexes, View.NavigationDirection.Forward); + } + } + + internal static void MoveNextViewOrTop () + { + if (ApplicationOverlapped.OverlappedTop is null) + { + Toplevel? top = Application.Current!.Modal ? Application.Current : Application.Top; + top!.FocusNext (); + + if (top.Focused is null) + { + top.FocusNext (); + } + + top.SetNeedsDisplay (); + ApplicationOverlapped.BringOverlappedTopToFront (); + } + else + { + ApplicationOverlapped.OverlappedMoveNext (); + } + } + + internal static void MovePreviousView () + { + View? old = GetDeepestFocusedSubview (Application.Current!.Focused); + + if (!Application.Current.FocusPrev ()) + { + Application.Current.FocusPrev (); + } + + if (old != Application.Current.Focused && old != Application.Current.Focused?.Focused) + { + old?.SetNeedsDisplay (); + Application.Current.Focused?.SetNeedsDisplay (); + } + else + { + FocusNearestView (Application.Current.SuperView?.TabIndexes?.Reverse (), View.NavigationDirection.Backward); + } + } + + internal static void MovePreviousViewOrTop () + { + if (ApplicationOverlapped.OverlappedTop is null) + { + Toplevel? top = Application.Current!.Modal ? Application.Current : Application.Top; + top!.FocusPrev (); + + if (top.Focused is null) + { + top.FocusPrev (); + } + + top.SetNeedsDisplay (); + ApplicationOverlapped.BringOverlappedTopToFront (); + } + else + { + ApplicationOverlapped.OverlappedMovePrevious (); + } } } diff --git a/Terminal.Gui/Application/Application.Overlapped.cs b/Terminal.Gui/Application/Application.Overlapped.cs index 26e931c98..ce88c1567 100644 --- a/Terminal.Gui/Application/Application.Overlapped.cs +++ b/Terminal.Gui/Application/Application.Overlapped.cs @@ -1,170 +1,373 @@ #nullable enable -using static Terminal.Gui.View; -using System.Reflection; - namespace Terminal.Gui; -internal static class ViewNavigation +/// +/// Helper class for managing overlapped views in the application. +/// +public static class ApplicationOverlapped { /// - /// Gets the deepest focused subview of the specified . + /// Gets the list of the Overlapped children which are not modal from the + /// . /// - /// + public static List? OverlappedChildren + { + get + { + if (OverlappedTop is { }) + { + List overlappedChildren = new (); + + lock (Application.TopLevels) + { + foreach (Toplevel top in Application.TopLevels) + { + if (top != OverlappedTop && !top.Modal) + { + overlappedChildren.Add (top); + } + } + } + + return overlappedChildren; + } + + return null; + } + } + + /// + /// The object used for the application on startup which + /// is true. + /// + public static Toplevel? OverlappedTop + { + get + { + if (Application.Top is { IsOverlappedContainer: true }) + { + return Application.Top; + } + + return null; + } + } + + /// Brings the superview of the most focused overlapped view is on front. + public static void BringOverlappedTopToFront () + { + if (OverlappedTop is { }) + { + return; + } + + View? top = FindTopFromView (Application.Top?.MostFocused); + + if (top is Toplevel && Application.Top?.Subviews.Count > 1 && Application.Top.Subviews [^1] != top) + { + Application.Top.BringSubviewToFront (top); + } + } + + /// Gets the current visible Toplevel overlapped child that matches the arguments pattern. + /// The type. + /// The strings to exclude. + /// The matched view. + public static Toplevel? GetTopOverlappedChild (Type? type = null, string []? exclude = null) + { + if (OverlappedChildren is null || OverlappedTop is null) + { + return null; + } + + foreach (Toplevel top in OverlappedChildren) + { + if (type is { } && top.GetType () == type && exclude?.Contains (top.Data.ToString ()) == false) + { + return top; + } + + if ((type is { } && top.GetType () != type) || exclude?.Contains (top.Data.ToString ()) == true) + { + continue; + } + + return top; + } + + return null; + } + + /// + /// Move to the next Overlapped child from the and set it as the if + /// it is not already. + /// + /// /// - internal static View? GetDeepestFocusedSubview (View? view) + public static bool MoveToOverlappedChild (Toplevel? top) + { + if (top is null) + { + return false; + } + if (top.Visible && OverlappedTop is { } && Application.Current?.Modal == false) + { + lock (Application.TopLevels) + { + Application.TopLevels.MoveTo (top, 0, new ToplevelEqualityComparer ()); + Application.Current = top; + } + + return true; + } + + return false; + } + + /// Move to the next Overlapped child from the . + public static void OverlappedMoveNext () + { + if (OverlappedTop is { } && !Application.Current!.Modal) + { + lock (Application.TopLevels) + { + Application.TopLevels.MoveNext (); + var isOverlapped = false; + + while (Application.TopLevels.Peek () == OverlappedTop || !Application.TopLevels.Peek ().Visible) + { + if (!isOverlapped && Application.TopLevels.Peek () == OverlappedTop) + { + isOverlapped = true; + } + else if (isOverlapped && Application.TopLevels.Peek () == OverlappedTop) + { + MoveCurrent (Application.Top!); + + break; + } + + Application.TopLevels.MoveNext (); + } + + Application.Current = Application.TopLevels.Peek (); + } + } + } + + /// Move to the previous Overlapped child from the . + public static void OverlappedMovePrevious () + { + if (OverlappedTop is { } && !Application.Current!.Modal) + { + lock (Application.TopLevels) + { + Application.TopLevels.MovePrevious (); + var isOverlapped = false; + + while (Application.TopLevels.Peek () == OverlappedTop || !Application.TopLevels.Peek ().Visible) + { + if (!isOverlapped && Application.TopLevels.Peek () == OverlappedTop) + { + isOverlapped = true; + } + else if (isOverlapped && Application.TopLevels.Peek () == OverlappedTop) + { + MoveCurrent (Application.Top!); + + break; + } + + Application.TopLevels.MovePrevious (); + } + + Application.Current = Application.TopLevels.Peek (); + } + } + } + + internal static bool OverlappedChildNeedsDisplay () + { + if (OverlappedTop is null) + { + return false; + } + + lock (Application.TopLevels) + { + foreach (Toplevel top in Application.TopLevels) + { + if (top != Application.Current && top.Visible && (top.NeedsDisplay || top.SubViewNeedsDisplay || top.LayoutNeeded)) + { + OverlappedTop.SetSubViewNeedsDisplay (); + + return true; + } + } + } + + return false; + } + + internal static bool SetCurrentOverlappedAsTop () + { + if (OverlappedTop is null && Application.Current != Application.Top && Application.Current?.SuperView is null && Application.Current?.Modal == false) + { + Application.Top = Application.Current; + + return true; + } + + return false; + } + + /// + /// Finds the first Toplevel in the stack that is Visible and who's Frame contains the . + /// + /// + /// + /// + internal static Toplevel? FindDeepestTop (Toplevel start, in Point location) + { + if (!start.Frame.Contains (location)) + { + return null; + } + + lock (Application.TopLevels) + { + if (Application.TopLevels is not { Count: > 0 }) + { + return start; + } + + int rx = location.X - start.Frame.X; + int ry = location.Y - start.Frame.Y; + + foreach (Toplevel t in Application.TopLevels) + { + if (t == Application.Current) + { + continue; + } + + if (t != start && t.Visible && t.Frame.Contains (rx, ry)) + { + start = t; + + break; + } + } + } + + return start; + } + + /// + /// Given , returns the first Superview up the chain that is . + /// + internal static View? FindTopFromView (View? view) { if (view is null) { return null; } - foreach (View v in view.Subviews) + View top = view.SuperView is { } && view.SuperView != Application.Top + ? view.SuperView + : view; + + while (top?.SuperView is { } && top?.SuperView != Application.Top) { - if (v.HasFocus) - { - return GetDeepestFocusedSubview (v); - } + top = top!.SuperView; } - return view; + return top; } /// - /// Sets the focus to the next view in the list. If the last view is focused, the first view is focused. + /// If the is not the then is moved to the top of + /// the Toplevel stack and made Current. /// - /// - /// - internal static void FocusNearestView (IEnumerable? viewsInTabIndexes, NavigationDirection direction) + /// + /// + internal static bool MoveCurrent (Toplevel top) { - if (viewsInTabIndexes is null) + // The Current is modal and the top is not modal Toplevel then + // the Current must be moved above the first not modal Toplevel. + if (OverlappedTop is { } + && top != OverlappedTop + && top != Application.Current + && Application.Current?.Modal == true + && !Application.TopLevels.Peek ().Modal) { - return; - } - - var found = false; - var focusProcessed = false; - var idx = 0; - - foreach (View v in viewsInTabIndexes) - { - if (v == Application.Current) + lock (Application.TopLevels) { - found = true; + Application.TopLevels.MoveTo (Application.Current, 0, new ToplevelEqualityComparer ()); } - if (found && v != Application.Current) + var index = 0; + Toplevel [] savedToplevels = Application.TopLevels.ToArray (); + + foreach (Toplevel t in savedToplevels) { - if (direction == NavigationDirection.Forward) + if (!t!.Modal && t != Application.Current && t != top && t != savedToplevels [index]) { - Application.Current!.SuperView?.FocusNext (); - } - else - { - Application.Current!.SuperView?.FocusPrev (); + lock (Application.TopLevels) + { + Application.TopLevels.MoveTo (top, index, new ToplevelEqualityComparer ()); + } } - focusProcessed = true; + index++; + } - if (Application.Current.SuperView?.Focused is { } && Application.Current.SuperView.Focused != Application.Current) + return false; + } + + // The Current and the top are both not running Toplevel then + // the top must be moved above the first not running Toplevel. + if (OverlappedTop is { } + && top != OverlappedTop + && top != Application.Current + && Application.Current?.Running == false + && top?.Running == false) + { + lock (Application.TopLevels) + { + Application.TopLevels.MoveTo (Application.Current, 0, new ToplevelEqualityComparer ()); + } + + var index = 0; + + foreach (Toplevel t in Application.TopLevels.ToArray ()) + { + if (!t.Running && t != Application.Current && index > 0) { - return; + lock (Application.TopLevels) + { + Application.TopLevels.MoveTo (top, index - 1, new ToplevelEqualityComparer ()); + } } + + index++; } - else if (found && !focusProcessed && idx == viewsInTabIndexes.Count () - 1) + + return false; + } + + if ((OverlappedTop is { } && top?.Modal == true && Application.TopLevels.Peek () != top) + || (OverlappedTop is { } && Application.Current != OverlappedTop && Application.Current?.Modal == false && top == OverlappedTop) + || (OverlappedTop is { } && Application.Current?.Modal == false && top != Application.Current) + || (OverlappedTop is { } && Application.Current?.Modal == true && top == OverlappedTop)) + { + lock (Application.TopLevels) { - viewsInTabIndexes.ToList () [0].SetFocus (); + Application.TopLevels.MoveTo (top, 0, new ToplevelEqualityComparer ()); + Application.Current = top; } - - idx++; - } - } - /// - /// Moves the focus to - /// - internal static void MoveNextView () - { - View? old = GetDeepestFocusedSubview (Application.Current!.Focused); - - if (!Application.Current.FocusNext ()) - { - Application.Current.FocusNext (); } - if (old != Application.Current.Focused && old != Application.Current.Focused?.Focused) - { - old?.SetNeedsDisplay (); - Application.Current.Focused?.SetNeedsDisplay (); - } - else - { - FocusNearestView (Application.Current.SuperView?.TabIndexes, NavigationDirection.Forward); - } - } - - internal static void MoveNextViewOrTop () - { - if (Application.OverlappedTop is null) - { - Toplevel? top = Application.Current!.Modal ? Application.Current : Application.Top; - top!.FocusNext (); - - if (top.Focused is null) - { - top.FocusNext (); - } - - top.SetNeedsDisplay (); - Application.BringOverlappedTopToFront (); - } - else - { - Application.OverlappedMoveNext (); - } - } - - internal static void MovePreviousView () - { - View? old = GetDeepestFocusedSubview (Application.Current!.Focused); - - if (!Application.Current.FocusPrev ()) - { - Application.Current.FocusPrev (); - } - - if (old != Application.Current.Focused && old != Application.Current.Focused?.Focused) - { - old?.SetNeedsDisplay (); - Application.Current.Focused?.SetNeedsDisplay (); - } - else - { - FocusNearestView (Application.Current.SuperView?.TabIndexes?.Reverse (), NavigationDirection.Backward); - } - } - - internal static void MovePreviousViewOrTop () - { - if (Application.OverlappedTop is null) - { - Toplevel? top = Application.Current!.Modal ? Application.Current : Application.Top; - top!.FocusPrev (); - - if (top.Focused is null) - { - top.FocusPrev (); - } - - top.SetNeedsDisplay (); - Application.BringOverlappedTopToFront (); - } - else - { - Application.OverlappedMovePrevious (); - } + return true; } } - -public static partial class Application // App-level View Navigation -{ - -} \ No newline at end of file diff --git a/Terminal.Gui/Application/Application.Run.cs b/Terminal.Gui/Application/Application.Run.cs index 87bfde4c0..3e088c348 100644 --- a/Terminal.Gui/Application/Application.Run.cs +++ b/Terminal.Gui/Application/Application.Run.cs @@ -54,7 +54,7 @@ public static partial class Application // Run (Begin, Run, End, Stop) } #endif - if (toplevel.IsOverlappedContainer && OverlappedTop != toplevel && OverlappedTop is { }) + if (toplevel.IsOverlappedContainer && ApplicationOverlapped.OverlappedTop != toplevel && ApplicationOverlapped.OverlappedTop is { }) { throw new InvalidOperationException ("Only one Overlapped Container is allowed."); } @@ -72,7 +72,7 @@ public static partial class Application // Run (Begin, Run, End, Stop) } #if DEBUG_IDISPOSABLE - if (Top is { } && toplevel != Top && !_topLevels.Contains (Top)) + if (Top is { } && toplevel != Top && !TopLevels.Contains (Top)) { // This assertion confirm if the Top was already disposed Debug.Assert (Top.WasDisposed); @@ -80,9 +80,9 @@ public static partial class Application // Run (Begin, Run, End, Stop) } #endif - lock (_topLevels) + lock (TopLevels) { - if (Top is { } && toplevel != Top && !_topLevels.Contains (Top)) + if (Top is { } && toplevel != Top && !TopLevels.Contains (Top)) { // If Top was already disposed and isn't on the Toplevels Stack, // clean it up here if is the same as _cachedRunStateToplevel @@ -96,7 +96,7 @@ public static partial class Application // Run (Begin, Run, End, Stop) throw new ObjectDisposedException (Top.GetType ().FullName); } } - else if (OverlappedTop is { } && toplevel != Top && _topLevels.Contains (Top!)) + else if (ApplicationOverlapped.OverlappedTop is { } && toplevel != Top && TopLevels.Contains (Top!)) { Top!.OnLeave (toplevel); } @@ -106,29 +106,29 @@ public static partial class Application // Run (Begin, Run, End, Stop) if (string.IsNullOrEmpty (toplevel.Id)) { var count = 1; - var id = (_topLevels.Count + count).ToString (); + var id = (TopLevels.Count + count).ToString (); - while (_topLevels.Count > 0 && _topLevels.FirstOrDefault (x => x.Id == id) is { }) + while (TopLevels.Count > 0 && TopLevels.FirstOrDefault (x => x.Id == id) is { }) { count++; - id = (_topLevels.Count + count).ToString (); + id = (TopLevels.Count + count).ToString (); } - toplevel.Id = (_topLevels.Count + count).ToString (); + toplevel.Id = (TopLevels.Count + count).ToString (); - _topLevels.Push (toplevel); + TopLevels.Push (toplevel); } else { - Toplevel? dup = _topLevels.FirstOrDefault (x => x.Id == toplevel.Id); + Toplevel? dup = TopLevels.FirstOrDefault (x => x.Id == toplevel.Id); if (dup is null) { - _topLevels.Push (toplevel); + TopLevels.Push (toplevel); } } - if (_topLevels.FindDuplicates (new ToplevelEqualityComparer ()).Count > 0) + if (TopLevels.FindDuplicates (new ToplevelEqualityComparer ()).Count > 0) { throw new ArgumentException ("There are duplicates Toplevel IDs"); } @@ -141,7 +141,7 @@ public static partial class Application // Run (Begin, Run, End, Stop) var refreshDriver = true; - if (OverlappedTop is null + if (ApplicationOverlapped.OverlappedTop is null || toplevel.IsOverlappedContainer || (Current?.Modal == false && toplevel.Modal) || (Current?.Modal == false && !toplevel.Modal) @@ -154,25 +154,25 @@ public static partial class Application // Run (Begin, Run, End, Stop) Current = toplevel; Current.OnActivate (previousCurrent); - SetCurrentOverlappedAsTop (); + ApplicationOverlapped.SetCurrentOverlappedAsTop (); } else { refreshDriver = false; } } - else if ((toplevel != OverlappedTop + else if ((toplevel != ApplicationOverlapped.OverlappedTop && Current?.Modal == true - && !_topLevels.Peek ().Modal) - || (toplevel != OverlappedTop && Current?.Running == false)) + && !TopLevels.Peek ().Modal) + || (toplevel != ApplicationOverlapped.OverlappedTop && Current?.Running == false)) { refreshDriver = false; - MoveCurrent (toplevel); + ApplicationOverlapped.MoveCurrent (toplevel); } else { refreshDriver = false; - MoveCurrent (Current!); + ApplicationOverlapped.MoveCurrent (Current!); } toplevel.SetRelativeLayout (Driver!.Screen.Size); @@ -180,11 +180,11 @@ public static partial class Application // Run (Begin, Run, End, Stop) toplevel.LayoutSubviews (); toplevel.PositionToplevels (); toplevel.FocusFirst (); - BringOverlappedTopToFront (); + ApplicationOverlapped.BringOverlappedTopToFront (); if (refreshDriver) { - OverlappedTop?.OnChildLoaded (toplevel); + ApplicationOverlapped.OverlappedTop?.OnChildLoaded (toplevel); toplevel.OnLoaded (); toplevel.SetNeedsDisplay (); toplevel.Draw (); @@ -427,7 +427,7 @@ public static partial class Application // Run (Begin, Run, End, Stop) if (runState.Toplevel is null) { #if DEBUG_IDISPOSABLE - Debug.Assert (_topLevels.Count == 0); + Debug.Assert (TopLevels.Count == 0); #endif runState.Dispose (); @@ -499,7 +499,7 @@ public static partial class Application // Run (Begin, Run, End, Stop) // TODO: Figure out how to remove this call to ClearContents. Refresh should just repaint damaged areas, not clear Driver!.ClearContents (); - foreach (Toplevel v in _topLevels.Reverse ()) + foreach (Toplevel v in TopLevels.Reverse ()) { if (v.Visible) { @@ -577,9 +577,9 @@ public static partial class Application // Run (Begin, Run, End, Stop) // TODO: Overlapped - Move elsewhere if (state.Toplevel != Current) { - OverlappedTop?.OnDeactivate (state.Toplevel); + ApplicationOverlapped.OverlappedTop?.OnDeactivate (state.Toplevel); state.Toplevel = Current; - OverlappedTop?.OnActivate (state.Toplevel); + ApplicationOverlapped.OverlappedTop?.OnActivate (state.Toplevel); Top!.SetSubViewNeedsDisplay (); Refresh (); } @@ -597,7 +597,7 @@ public static partial class Application // Run (Begin, Run, End, Stop) state.Toplevel!.SetNeedsDisplay (state.Toplevel.Frame); Top.Draw (); - foreach (Toplevel top in _topLevels.Reverse ()) + foreach (Toplevel top in TopLevels.Reverse ()) { if (top != Top && top != state.Toplevel) { @@ -608,7 +608,7 @@ public static partial class Application // Run (Begin, Run, End, Stop) } } - if (_topLevels.Count == 1 + if (TopLevels.Count == 1 && state.Toplevel == Top && (Driver!.Cols != state.Toplevel!.Frame.Width || Driver!.Rows != state.Toplevel.Frame.Height) @@ -619,7 +619,7 @@ public static partial class Application // Run (Begin, Run, End, Stop) Driver.ClearContents (); } - if (state.Toplevel!.NeedsDisplay || state.Toplevel.SubViewNeedsDisplay || state.Toplevel.LayoutNeeded || OverlappedChildNeedsDisplay ()) + if (state.Toplevel!.NeedsDisplay || state.Toplevel.SubViewNeedsDisplay || state.Toplevel.LayoutNeeded || ApplicationOverlapped.OverlappedChildNeedsDisplay ()) { state.Toplevel.SetNeedsDisplay (); state.Toplevel.Draw (); @@ -659,19 +659,19 @@ public static partial class Application // Run (Begin, Run, End, Stop) /// public static void RequestStop (Toplevel? top = null) { - if (OverlappedTop is null || top is null) + if (ApplicationOverlapped.OverlappedTop is null || top is null) { top = Current; } - if (OverlappedTop != null + if (ApplicationOverlapped.OverlappedTop != null && top!.IsOverlappedContainer && top?.Running == true && (Current?.Modal == false || Current is { Modal: true, Running: false })) { - OverlappedTop.RequestStop (); + ApplicationOverlapped.OverlappedTop.RequestStop (); } - else if (OverlappedTop != null + else if (ApplicationOverlapped.OverlappedTop != null && top != Current && Current is { Running: true, Modal: true } && top!.Modal @@ -698,21 +698,21 @@ public static partial class Application // Run (Begin, Run, End, Stop) top.Running = false; OnNotifyStopRunState (top); } - else if ((OverlappedTop != null - && top != OverlappedTop + else if ((ApplicationOverlapped.OverlappedTop != null + && top != ApplicationOverlapped.OverlappedTop && top != Current && Current is { Modal: false, Running: true } && !top!.Running) - || (OverlappedTop != null - && top != OverlappedTop + || (ApplicationOverlapped.OverlappedTop != null + && top != ApplicationOverlapped.OverlappedTop && top != Current && Current is { Modal: false, Running: false } && !top!.Running - && _topLevels.ToArray () [1].Running)) + && TopLevels.ToArray () [1].Running)) { - MoveCurrent (top); + ApplicationOverlapped.MoveCurrent (top); } - else if (OverlappedTop != null + else if (ApplicationOverlapped.OverlappedTop != null && Current != top && Current?.Running == true && !top!.Running @@ -723,9 +723,9 @@ public static partial class Application // Run (Begin, Run, End, Stop) Current.Running = false; OnNotifyStopRunState (Current); } - else if (OverlappedTop != null + else if (ApplicationOverlapped.OverlappedTop != null && Current == top - && OverlappedTop?.Running == true + && ApplicationOverlapped.OverlappedTop?.Running == true && Current?.Running == true && top!.Running && Current?.Modal == true @@ -784,9 +784,9 @@ public static partial class Application // Run (Begin, Run, End, Stop) { ArgumentNullException.ThrowIfNull (runState); - if (OverlappedTop is { }) + if (ApplicationOverlapped.OverlappedTop is { }) { - OverlappedTop.OnChildUnloaded (runState.Toplevel); + ApplicationOverlapped.OverlappedTop.OnChildUnloaded (runState.Toplevel); } else { @@ -795,16 +795,16 @@ public static partial class Application // Run (Begin, Run, End, Stop) // End the RunState.Toplevel // First, take it off the Toplevel Stack - if (_topLevels.Count > 0) + if (TopLevels.Count > 0) { - if (_topLevels.Peek () != runState.Toplevel) + if (TopLevels.Peek () != runState.Toplevel) { // If the top of the stack is not the RunState.Toplevel then // this call to End is not balanced with the call to Begin that started the RunState throw new ArgumentException ("End must be balanced with calls to Begin"); } - _topLevels.Pop (); + TopLevels.Pop (); } // Notify that it is closing @@ -812,32 +812,32 @@ public static partial class Application // Run (Begin, Run, End, Stop) // If there is a OverlappedTop that is not the RunState.Toplevel then RunState.Toplevel // is a child of MidTop, and we should notify the OverlappedTop that it is closing - if (OverlappedTop is { } && !runState.Toplevel!.Modal && runState.Toplevel != OverlappedTop) + if (ApplicationOverlapped.OverlappedTop is { } && !runState.Toplevel!.Modal && runState.Toplevel != ApplicationOverlapped.OverlappedTop) { - OverlappedTop.OnChildClosed (runState.Toplevel); + ApplicationOverlapped.OverlappedTop.OnChildClosed (runState.Toplevel); } // Set Current and Top to the next TopLevel on the stack - if (_topLevels.Count == 0) + if (TopLevels.Count == 0) { Current = null; } else { - if (_topLevels.Count > 1 && _topLevels.Peek () == OverlappedTop && OverlappedChildren?.Any (t => t.Visible) != null) + if (TopLevels.Count > 1 && TopLevels.Peek () == ApplicationOverlapped.OverlappedTop && ApplicationOverlapped.OverlappedChildren?.Any (t => t.Visible) != null) { - OverlappedMoveNext (); + ApplicationOverlapped.OverlappedMoveNext (); } - Current = _topLevels.Peek (); + Current = TopLevels.Peek (); - if (_topLevels.Count == 1 && Current == OverlappedTop) + if (TopLevels.Count == 1 && Current == ApplicationOverlapped.OverlappedTop) { - OverlappedTop.OnAllChildClosed (); + ApplicationOverlapped.OverlappedTop.OnAllChildClosed (); } else { - SetCurrentOverlappedAsTop (); + ApplicationOverlapped.SetCurrentOverlappedAsTop (); runState.Toplevel!.OnLeave (Current); Current.OnEnter (runState.Toplevel); } @@ -848,9 +848,9 @@ public static partial class Application // Run (Begin, Run, End, Stop) // Don't dispose runState.Toplevel. It's up to caller dispose it // If it's not the same as the current in the RunIteration, // it will be fixed later in the next RunIteration. - if (OverlappedTop is { } && !_topLevels.Contains (OverlappedTop)) + if (ApplicationOverlapped.OverlappedTop is { } && !TopLevels.Contains (ApplicationOverlapped.OverlappedTop)) { - _cachedRunStateToplevel = OverlappedTop; + _cachedRunStateToplevel = ApplicationOverlapped.OverlappedTop; } else { diff --git a/Terminal.Gui/Application/Application.Toplevel.cs b/Terminal.Gui/Application/Application.Toplevel.cs index a050e28f1..a1844b2d0 100644 --- a/Terminal.Gui/Application/Application.Toplevel.cs +++ b/Terminal.Gui/Application/Application.Toplevel.cs @@ -4,13 +4,13 @@ namespace Terminal.Gui; public static partial class Application // Toplevel handling { // BUGBUG: Technically, this is not the full lst of TopLevels. There be dragons here, e.g. see how Toplevel.Id is used. What + /// Holds the stack of TopLevel views. - // about TopLevels that are just a SubView of another View? - internal static readonly Stack _topLevels = new (); + internal static Stack TopLevels { get; } = new (); /// The object used for the application on startup () /// The top. - public static Toplevel? Top { get; private set; } + public static Toplevel? Top { get; internal set; } // TODO: Determine why this can't just return _topLevels.Peek()? /// @@ -22,7 +22,7 @@ public static partial class Application // Toplevel handling /// This will only be distinct from in scenarios where is . /// /// The current. - public static Toplevel? Current { get; private set; } + public static Toplevel? Current { get; internal set; } /// /// If is not already Current and visible, finds the last Modal Toplevel in the stack and makes it Current. @@ -31,17 +31,17 @@ public static partial class Application // Toplevel handling { if (!topLevel.Running || (topLevel == Current && topLevel.Visible) - || OverlappedTop == null - || _topLevels.Peek ().Modal) + || ApplicationOverlapped.OverlappedTop == null + || TopLevels.Peek ().Modal) { return; } - foreach (Toplevel top in _topLevels.Reverse ()) + foreach (Toplevel top in TopLevels.Reverse ()) { if (top.Modal && top != Current) { - MoveCurrent (top); + ApplicationOverlapped.MoveCurrent (top); return; } @@ -49,47 +49,8 @@ public static partial class Application // Toplevel handling if (!topLevel.Visible && topLevel == Current) { - OverlappedMoveNext (); + ApplicationOverlapped.OverlappedMoveNext (); } } - /// Invoked when the terminal's size changed. The new size of the terminal is provided. - /// - /// Event handlers can set to to prevent - /// from changing it's size to match the new terminal size. - /// - public static event EventHandler? SizeChanging; - - /// - /// Called when the application's size changes. Sets the size of all s and fires the - /// event. - /// - /// The new size. - /// if the size was changed. - public static bool OnSizeChanging (SizeChangedEventArgs args) - { - SizeChanging?.Invoke (null, args); - - if (args.Cancel || args.Size is null) - { - return false; - } - - foreach (Toplevel t in _topLevels) - { - t.SetRelativeLayout (args.Size.Value); - t.LayoutSubviews (); - t.PositionToplevels (); - t.OnSizeChanging (new (args.Size)); - - if (PositionCursor (t)) - { - Driver?.UpdateCursor (); - } - } - - Refresh (); - - return true; - } } diff --git a/Terminal.Gui/Application/Application.cs b/Terminal.Gui/Application/Application.cs index 75fbd7191..b4b4d1310 100644 --- a/Terminal.Gui/Application/Application.cs +++ b/Terminal.Gui/Application/Application.cs @@ -56,12 +56,12 @@ public static partial class Application // 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 (Toplevel? t in _topLevels) + foreach (Toplevel? t in TopLevels) { t!.Running = false; } - _topLevels.Clear (); + TopLevels.Clear (); Current = null; #if DEBUG_IDISPOSABLE diff --git a/Terminal.Gui/View/Adornment/Border.cs b/Terminal.Gui/View/Adornment/Border.cs index ef841e5b1..41fd02f9d 100644 --- a/Terminal.Gui/View/Adornment/Border.cs +++ b/Terminal.Gui/View/Adornment/Border.cs @@ -293,7 +293,7 @@ public class Border : Adornment if (!_dragPosition.HasValue && mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed)) { Parent.SetFocus (); - Application.BringOverlappedTopToFront (); + ApplicationOverlapped.BringOverlappedTopToFront (); // Only start grabbing if the user clicks in the Thickness area // Adornment.Contains takes Parent SuperView=relative coords. diff --git a/Terminal.Gui/View/ViewSubViews.cs b/Terminal.Gui/View/ViewSubViews.cs index 38863828a..5da8dd76a 100644 --- a/Terminal.Gui/View/ViewSubViews.cs +++ b/Terminal.Gui/View/ViewSubViews.cs @@ -456,7 +456,7 @@ public partial class View Application.Current.FocusNext (); } - Application.BringOverlappedTopToFront (); + ApplicationOverlapped.BringOverlappedTopToFront (); } } @@ -489,7 +489,7 @@ public partial class View if (this is Toplevel && Application.Current.Focused != this) { - Application.BringOverlappedTopToFront (); + ApplicationOverlapped.BringOverlappedTopToFront (); } } diff --git a/Terminal.Gui/Views/TileView.cs b/Terminal.Gui/Views/TileView.cs index 54ec8a4d2..6eb5fa3bf 100644 --- a/Terminal.Gui/Views/TileView.cs +++ b/Terminal.Gui/Views/TileView.cs @@ -908,7 +908,7 @@ public class TileView : View { // Start a Drag SetFocus (); - Application.BringOverlappedTopToFront (); + ApplicationOverlapped.BringOverlappedTopToFront (); if (mouseEvent.Flags == MouseFlags.Button1Pressed) { diff --git a/Terminal.Gui/Views/Toplevel.cs b/Terminal.Gui/Views/Toplevel.cs index 11d216210..d13ae0766 100644 --- a/Terminal.Gui/Views/Toplevel.cs +++ b/Terminal.Gui/Views/Toplevel.cs @@ -239,7 +239,7 @@ public partial class Toplevel : View || Application.Current?.Modal == false || (Application.Current?.Modal == true && Application.Current?.Running == false))) { - foreach (Toplevel child in Application.OverlappedChildren) + foreach (Toplevel child in ApplicationOverlapped.OverlappedChildren!) { var ev = new ToplevelClosingEventArgs (this); @@ -369,10 +369,10 @@ public partial class Toplevel : View //LayoutSubviews (); PositionToplevels (); - if (this == Application.OverlappedTop) + if (this == ApplicationOverlapped.OverlappedTop) { // This enables correct draw behavior when switching between overlapped subviews - foreach (Toplevel top in Application.OverlappedChildren.AsEnumerable ().Reverse ()) + foreach (Toplevel top in ApplicationOverlapped.OverlappedChildren!.AsEnumerable ().Reverse ()) { if (top.Frame.IntersectsWith (Viewport)) { @@ -437,7 +437,7 @@ public partial class Toplevel : View if (Focused is null) { // TODO: this is an Overlapped hack - foreach (Toplevel top in Application.OverlappedChildren) + foreach (Toplevel top in ApplicationOverlapped.OverlappedChildren!) { if (top != this && top.Visible) { @@ -607,7 +607,7 @@ public class ToplevelEqualityComparer : IEqualityComparer /// /// Implements the to sort the from the -/// if needed. +/// if needed. /// public sealed class ToplevelComparer : IComparer { diff --git a/Terminal.Gui/Views/ToplevelOverlapped.cs b/Terminal.Gui/Views/ToplevelOverlapped.cs index 3541ea091..06dac3694 100644 --- a/Terminal.Gui/Views/ToplevelOverlapped.cs +++ b/Terminal.Gui/Views/ToplevelOverlapped.cs @@ -3,7 +3,7 @@ public partial class Toplevel { /// Gets or sets if this Toplevel is in overlapped mode within a Toplevel container. - public bool IsOverlapped => Application.OverlappedTop is { } && Application.OverlappedTop != this && !Modal; + public bool IsOverlapped => ApplicationOverlapped.OverlappedTop is { } && ApplicationOverlapped.OverlappedTop != this && !Modal; /// Gets or sets if this Toplevel is a container for overlapped children. public bool IsOverlappedContainer { get; set; } diff --git a/UICatalog/Scenarios/BackgroundWorkerCollection.cs b/UICatalog/Scenarios/BackgroundWorkerCollection.cs index b2f6cdfa9..d1ce6b825 100644 --- a/UICatalog/Scenarios/BackgroundWorkerCollection.cs +++ b/UICatalog/Scenarios/BackgroundWorkerCollection.cs @@ -20,10 +20,10 @@ public class BackgroundWorkerCollection : Scenario Application.Run ().Dispose (); #if DEBUG_IDISPOSABLE - if (Application.OverlappedChildren is { }) + if (ApplicationOverlapped.OverlappedChildren is { }) { - Debug.Assert (Application.OverlappedChildren?.Count == 0); - Debug.Assert (Application.Top == Application.OverlappedTop); + Debug.Assert (ApplicationOverlapped.OverlappedChildren?.Count == 0); + Debug.Assert (Application.Top == ApplicationOverlapped.OverlappedTop); } #endif @@ -134,7 +134,7 @@ public class BackgroundWorkerCollection : Scenario { var index = 1; List menuItems = new (); - List sortedChildren = Application.OverlappedChildren; + List sortedChildren = ApplicationOverlapped.OverlappedChildren; sortedChildren.Sort (new ToplevelComparer ()); foreach (Toplevel top in sortedChildren) @@ -151,7 +151,7 @@ public class BackgroundWorkerCollection : Scenario string topTitle = top is Window ? ((Window)top).Title : top.Data.ToString (); string itemTitle = item.Title.Substring (index.ToString ().Length + 1); - if (top == Application.GetTopOverlappedChild () && topTitle == itemTitle) + if (top == ApplicationOverlapped.GetTopOverlappedChild () && topTitle == itemTitle) { item.Checked = true; } @@ -160,7 +160,7 @@ public class BackgroundWorkerCollection : Scenario item.Checked = false; } - item.Action += () => { Application.MoveToOverlappedChild (top); }; + item.Action += () => { ApplicationOverlapped.MoveToOverlappedChild (top); }; menuItems.Add (item); } @@ -188,7 +188,7 @@ public class BackgroundWorkerCollection : Scenario { List menuItems = new (); var item = new MenuItem { Title = "WorkerApp", CheckType = MenuItemCheckStyle.Checked }; - Toplevel top = Application.OverlappedChildren?.Find (x => x.Data.ToString () == "WorkerApp"); + Toplevel top = ApplicationOverlapped.OverlappedChildren?.Find (x => x.Data.ToString () == "WorkerApp"); if (top != null) { @@ -197,16 +197,16 @@ public class BackgroundWorkerCollection : Scenario item.Action += () => { - Toplevel top = Application.OverlappedChildren.Find (x => x.Data.ToString () == "WorkerApp"); + Toplevel top = ApplicationOverlapped.OverlappedChildren.Find (x => x.Data.ToString () == "WorkerApp"); item.Checked = top.Visible = (bool)!item.Checked; if (top.Visible) { - Application.MoveToOverlappedChild (top); + ApplicationOverlapped.MoveToOverlappedChild (top); } else { - Application.OverlappedTop.SetNeedsDisplay (); + ApplicationOverlapped.OverlappedTop!.SetNeedsDisplay (); } }; menuItems.Add (item); @@ -373,14 +373,14 @@ public class BackgroundWorkerCollection : Scenario } private void WorkerApp_Closing (object sender, ToplevelClosingEventArgs e) { - Toplevel top = Application.OverlappedChildren.Find (x => x.Data.ToString () == "WorkerApp"); + Toplevel top = ApplicationOverlapped.OverlappedChildren!.Find (x => x.Data.ToString () == "WorkerApp"); if (Visible && top == this) { Visible = false; e.Cancel = true; - Application.OverlappedMoveNext (); + ApplicationOverlapped.OverlappedMoveNext (); } } @@ -481,7 +481,7 @@ public class BackgroundWorkerCollection : Scenario _stagingsUi.Add (stagingUI); _stagingWorkers.Remove (staging); #if DEBUG_IDISPOSABLE - if (Application.OverlappedTop is null) + if (ApplicationOverlapped.OverlappedTop is null) { stagingUI.Dispose (); return; diff --git a/UnitTests/Application/ApplicationTests.cs b/UnitTests/Application/ApplicationTests.cs index c600254a2..004f07e2e 100644 --- a/UnitTests/Application/ApplicationTests.cs +++ b/UnitTests/Application/ApplicationTests.cs @@ -89,12 +89,12 @@ public class ApplicationTests RunState runstate = null; - EventHandler NewRunStateFn = (s, e) => + EventHandler newRunStateFn = (s, e) => { Assert.NotNull (e.State); runstate = e.State; }; - Application.NotifyNewRunState += NewRunStateFn; + Application.NotifyNewRunState += newRunStateFn; var topLevel = new Toplevel (); RunState rs = Application.Begin (topLevel); @@ -105,7 +105,7 @@ public class ApplicationTests Assert.Equal (topLevel, Application.Top); Assert.Equal (topLevel, Application.Current); - Application.NotifyNewRunState -= NewRunStateFn; + Application.NotifyNewRunState -= newRunStateFn; Application.End (runstate); Assert.Null (Application.Current); @@ -187,15 +187,15 @@ public class ApplicationTests 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); + Assert.Null (ApplicationOverlapped.OverlappedChildren); + Assert.Null (ApplicationOverlapped.OverlappedTop); // Internal properties Assert.False (Application.IsInitialized); Assert.Equal (Application.GetSupportedCultures (), Application.SupportedCultures); Assert.False (Application._forceFakeConsole); Assert.Equal (-1, Application.MainThreadId); - Assert.Empty (Application._topLevels); + Assert.Empty (Application.TopLevels); Assert.Null (Application.MouseEnteredView); // Keyboard @@ -235,8 +235,8 @@ public class ApplicationTests Application.QuitKey = Key.C; Application.KeyBindings.Add (Key.A, KeyBindingScope.Application, Command.Cancel); - //Application.OverlappedChildren = new List (); - //Application.OverlappedTop = + //ApplicationOverlapped.OverlappedChildren = new List (); + //ApplicationOverlapped.OverlappedTop = Application.MouseEnteredView = new (); //Application.WantContinuousButtonPressedView = new View (); @@ -378,12 +378,12 @@ public class ApplicationTests RunState runstate = null; - EventHandler NewRunStateFn = (s, e) => + EventHandler newRunStateFn = (s, e) => { Assert.NotNull (e.State); runstate = e.State; }; - Application.NotifyNewRunState += NewRunStateFn; + Application.NotifyNewRunState += newRunStateFn; RunState rs = Application.Begin (topLevel); Assert.NotNull (rs); @@ -393,7 +393,7 @@ public class ApplicationTests Assert.Equal (topLevel, Application.Top); Assert.Equal (topLevel, Application.Current); - Application.NotifyNewRunState -= NewRunStateFn; + Application.NotifyNewRunState -= newRunStateFn; Application.End (runstate); Assert.Null (Application.Current); @@ -419,7 +419,7 @@ public class ApplicationTests Assert.Equal (Application.Top, rs.Toplevel); Assert.Null (Application.MouseGrabView); // public Assert.Null (Application.WantContinuousButtonPressedView); // public - Assert.False (Application.MoveToOverlappedChild (Application.Top)); + Assert.False (ApplicationOverlapped.MoveToOverlappedChild (Application.Top!)); Application.Top.Dispose (); } diff --git a/UnitTests/Views/OverlappedTests.cs b/UnitTests/Views/OverlappedTests.cs index 2cf4fa96d..c1b0ec733 100644 --- a/UnitTests/Views/OverlappedTests.cs +++ b/UnitTests/Views/OverlappedTests.cs @@ -1,4 +1,5 @@ -using System.Threading; +#nullable enable +using System.Threading; using Xunit.Abstractions; namespace Terminal.Gui.ViewsTests; @@ -30,25 +31,25 @@ public class OverlappedTests overlapped.Ready += (s, e) => { - Assert.Empty (Application.OverlappedChildren); + Assert.Empty (ApplicationOverlapped.OverlappedChildren!); Application.Run (c1); }; c1.Ready += (s, e) => { - Assert.Single (Application.OverlappedChildren); + Assert.Single (ApplicationOverlapped.OverlappedChildren!); Application.Run (c2); }; c2.Ready += (s, e) => { - Assert.Equal (2, Application.OverlappedChildren.Count); + Assert.Equal (2, ApplicationOverlapped.OverlappedChildren!.Count); Application.Run (c3); }; c3.Ready += (s, e) => { - Assert.Equal (3, Application.OverlappedChildren.Count); + Assert.Equal (3, ApplicationOverlapped.OverlappedChildren!.Count); c3.RequestStop (); c2.RequestStop (); c1.RequestStop (); @@ -66,31 +67,31 @@ public class OverlappedTests Assert.False (Application.Current.Running); // But the Children order were reorder by Running = false - Assert.True (Application.OverlappedChildren [0] == c3); - Assert.True (Application.OverlappedChildren [1] == c2); - Assert.True (Application.OverlappedChildren [^1] == c1); + Assert.True (ApplicationOverlapped.OverlappedChildren! [0] == c3); + Assert.True (ApplicationOverlapped.OverlappedChildren [1] == c2); + Assert.True (ApplicationOverlapped.OverlappedChildren [^1] == c1); } else if (iterations == 2) { // The Current is c2 and Current.Running is false. Assert.True (Application.Current == c2); Assert.False (Application.Current.Running); - Assert.True (Application.OverlappedChildren [0] == c2); - Assert.True (Application.OverlappedChildren [^1] == c1); + Assert.True (ApplicationOverlapped.OverlappedChildren ![0] == c2); + Assert.True (ApplicationOverlapped.OverlappedChildren [^1] == c1); } else if (iterations == 1) { // The Current is c1 and Current.Running is false. Assert.True (Application.Current == c1); Assert.False (Application.Current.Running); - Assert.True (Application.OverlappedChildren [^1] == c1); + Assert.True (ApplicationOverlapped.OverlappedChildren! [^1] == c1); } else { // The Current is overlapped. Assert.True (Application.Current == overlapped); Assert.False (Application.Current.Running); - Assert.Empty (Application.OverlappedChildren); + Assert.Empty (ApplicationOverlapped.OverlappedChildren!); } iterations--; @@ -98,8 +99,8 @@ public class OverlappedTests Application.Run (overlapped); - Assert.Empty (Application.OverlappedChildren); - Assert.NotNull (Application.OverlappedTop); + Assert.Empty (ApplicationOverlapped.OverlappedChildren!); + Assert.NotNull (ApplicationOverlapped.OverlappedTop); Assert.NotNull (Application.Top); overlapped.Dispose (); } @@ -119,31 +120,31 @@ public class OverlappedTests top1.Ready += (s, e) => { - Assert.Null (Application.OverlappedChildren); + Assert.Null (ApplicationOverlapped.OverlappedChildren); Application.Run (top2); }; top2.Ready += (s, e) => { - Assert.Null (Application.OverlappedChildren); + Assert.Null (ApplicationOverlapped.OverlappedChildren); Application.Run (top3); }; top3.Ready += (s, e) => { - Assert.Null (Application.OverlappedChildren); + Assert.Null (ApplicationOverlapped.OverlappedChildren); Application.Run (top4); }; top4.Ready += (s, e) => { - Assert.Null (Application.OverlappedChildren); + Assert.Null (ApplicationOverlapped.OverlappedChildren); Application.Run (d); }; d.Ready += (s, e) => { - Assert.Null (Application.OverlappedChildren); + Assert.Null (ApplicationOverlapped.OverlappedChildren); // This will close the d because on a not OverlappedContainer the Application.Current it always used. Application.RequestStop (top1); @@ -154,7 +155,7 @@ public class OverlappedTests Application.Iteration += (s, a) => { - Assert.Null (Application.OverlappedChildren); + Assert.Null (ApplicationOverlapped.OverlappedChildren); if (iterations == 4) { @@ -183,7 +184,7 @@ public class OverlappedTests Application.Run (top1); - Assert.Null (Application.OverlappedChildren); + Assert.Null (ApplicationOverlapped.OverlappedChildren); top1.Dispose (); } @@ -261,37 +262,37 @@ public class OverlappedTests overlapped.Ready += (s, e) => { - Assert.Empty (Application.OverlappedChildren); + Assert.Empty (ApplicationOverlapped.OverlappedChildren!); Application.Run (c1); }; c1.Ready += (s, e) => { - Assert.Single (Application.OverlappedChildren); + Assert.Single (ApplicationOverlapped.OverlappedChildren!); Application.Run (c2); }; c2.Ready += (s, e) => { - Assert.Equal (2, Application.OverlappedChildren.Count); + Assert.Equal (2, ApplicationOverlapped.OverlappedChildren!.Count); Application.Run (c3); }; c3.Ready += (s, e) => { - Assert.Equal (3, Application.OverlappedChildren.Count); + Assert.Equal (3, ApplicationOverlapped.OverlappedChildren!.Count); Application.Run (d1); }; d1.Ready += (s, e) => { - Assert.Equal (3, Application.OverlappedChildren.Count); + Assert.Equal (3, ApplicationOverlapped.OverlappedChildren!.Count); Application.Run (d2); }; d2.Ready += (s, e) => { - Assert.Equal (3, Application.OverlappedChildren.Count); + Assert.Equal (3, ApplicationOverlapped.OverlappedChildren!.Count); Assert.True (Application.Current == d2); Assert.True (Application.Current.Running); @@ -326,11 +327,11 @@ public class OverlappedTests } else { - Assert.Equal (iterations, Application.OverlappedChildren.Count); + Assert.Equal (iterations, ApplicationOverlapped.OverlappedChildren!.Count); for (var i = 0; i < iterations; i++) { - Assert.Equal ((iterations - i + 1).ToString (), Application.OverlappedChildren [i].Id); + Assert.Equal ((iterations - i + 1).ToString (), ApplicationOverlapped.OverlappedChildren [i].Id); } } @@ -339,8 +340,8 @@ public class OverlappedTests Application.Run (overlapped); - Assert.Empty (Application.OverlappedChildren); - Assert.NotNull (Application.OverlappedTop); + Assert.Empty (ApplicationOverlapped.OverlappedChildren!); + Assert.NotNull (ApplicationOverlapped.OverlappedTop); Assert.NotNull (Application.Top); overlapped.Dispose (); } @@ -363,37 +364,37 @@ public class OverlappedTests overlapped.Ready += (s, e) => { - Assert.Empty (Application.OverlappedChildren); + Assert.Empty (ApplicationOverlapped.OverlappedChildren!); Application.Run (c1); }; c1.Ready += (s, e) => { - Assert.Single (Application.OverlappedChildren); + Assert.Single (ApplicationOverlapped.OverlappedChildren!); Application.Run (c2); }; c2.Ready += (s, e) => { - Assert.Equal (2, Application.OverlappedChildren.Count); + Assert.Equal (2, ApplicationOverlapped.OverlappedChildren!.Count); Application.Run (c3); }; c3.Ready += (s, e) => { - Assert.Equal (3, Application.OverlappedChildren.Count); + Assert.Equal (3, ApplicationOverlapped.OverlappedChildren!.Count); Application.Run (d1); }; d1.Ready += (s, e) => { - Assert.Equal (3, Application.OverlappedChildren.Count); + Assert.Equal (3, ApplicationOverlapped.OverlappedChildren!.Count); Application.Run (c4); }; c4.Ready += (s, e) => { - Assert.Equal (4, Application.OverlappedChildren.Count); + Assert.Equal (4, ApplicationOverlapped.OverlappedChildren!.Count); // Trying to close the Dialog1 d1.RequestStop (); @@ -415,13 +416,13 @@ public class OverlappedTests } else { - Assert.Equal (iterations, Application.OverlappedChildren.Count); + Assert.Equal (iterations, ApplicationOverlapped.OverlappedChildren!.Count); for (var i = 0; i < iterations; i++) { Assert.Equal ( (iterations - i + (iterations == 4 && i == 0 ? 2 : 1)).ToString (), - Application.OverlappedChildren [i].Id + ApplicationOverlapped.OverlappedChildren [i].Id ); } } @@ -431,8 +432,8 @@ public class OverlappedTests Application.Run (overlapped); - Assert.Empty (Application.OverlappedChildren); - Assert.NotNull (Application.OverlappedTop); + Assert.Empty (ApplicationOverlapped.OverlappedChildren!); + Assert.NotNull (ApplicationOverlapped.OverlappedTop); Assert.NotNull (Application.Top); overlapped.Dispose (); } @@ -441,7 +442,7 @@ public class OverlappedTests [AutoInitShutdown] public void MoveCurrent_Returns_False_If_The_Current_And_Top_Parameter_Are_Both_With_Running_Set_To_False () { - var overlapped = new Overlapped (); + Overlapped? overlapped = new Overlapped (); var c1 = new Toplevel (); var c2 = new Window (); var c3 = new Window (); @@ -451,25 +452,25 @@ public class OverlappedTests overlapped.Ready += (s, e) => { - Assert.Empty (Application.OverlappedChildren); + Assert.Empty (ApplicationOverlapped.OverlappedChildren!); Application.Run (c1); }; c1.Ready += (s, e) => { - Assert.Single (Application.OverlappedChildren); + Assert.Single (ApplicationOverlapped.OverlappedChildren!); Application.Run (c2); }; c2.Ready += (s, e) => { - Assert.Equal (2, Application.OverlappedChildren.Count); + Assert.Equal (2, ApplicationOverlapped.OverlappedChildren!.Count); Application.Run (c3); }; c3.Ready += (s, e) => { - Assert.Equal (3, Application.OverlappedChildren.Count); + Assert.Equal (3, ApplicationOverlapped.OverlappedChildren!.Count); c3.RequestStop (); c1.RequestStop (); }; @@ -486,30 +487,30 @@ public class OverlappedTests Assert.False (Application.Current.Running); // But the Children order were reorder by Running = false - Assert.True (Application.OverlappedChildren [0] == c3); - Assert.True (Application.OverlappedChildren [1] == c1); - Assert.True (Application.OverlappedChildren [^1] == c2); + Assert.True (ApplicationOverlapped.OverlappedChildren! [0] == c3); + Assert.True (ApplicationOverlapped.OverlappedChildren [1] == c1); + Assert.True (ApplicationOverlapped.OverlappedChildren [^1] == c2); } else if (iterations == 2) { // The Current is c1 and Current.Running is false. Assert.True (Application.Current == c1); Assert.False (Application.Current.Running); - Assert.True (Application.OverlappedChildren [0] == c1); - Assert.True (Application.OverlappedChildren [^1] == c2); + Assert.True (ApplicationOverlapped.OverlappedChildren! [0] == c1); + Assert.True (ApplicationOverlapped.OverlappedChildren [^1] == c2); } else if (iterations == 1) { // The Current is c2 and Current.Running is false. Assert.True (Application.Current == c2); Assert.False (Application.Current.Running); - Assert.True (Application.OverlappedChildren [^1] == c2); + Assert.True (ApplicationOverlapped.OverlappedChildren! [^1] == c2); } else { // The Current is overlapped. Assert.True (Application.Current == overlapped); - Assert.Empty (Application.OverlappedChildren); + Assert.Empty (ApplicationOverlapped.OverlappedChildren!); } iterations--; @@ -517,8 +518,8 @@ public class OverlappedTests Application.Run (overlapped); - Assert.Empty (Application.OverlappedChildren); - Assert.NotNull (Application.OverlappedTop); + Assert.Empty (ApplicationOverlapped.OverlappedChildren!); + Assert.NotNull (ApplicationOverlapped.OverlappedTop); Assert.NotNull (Application.Top); overlapped.Dispose (); } @@ -526,7 +527,7 @@ public class OverlappedTests [Fact] public void MoveToOverlappedChild_Throw_NullReferenceException_Passing_Null_Parameter () { - Assert.Throws (delegate { Application.MoveToOverlappedChild (null); }); + Assert.Throws (delegate { ApplicationOverlapped.MoveToOverlappedChild (null); }); } [Fact] @@ -544,11 +545,11 @@ public class OverlappedTests overlapped.Ready += (s, e) => { - Assert.Empty (Application.OverlappedChildren); + Assert.Empty (ApplicationOverlapped.OverlappedChildren!); Application.Run (logger); }; - logger.Ready += (s, e) => Assert.Single (Application.OverlappedChildren); + logger.Ready += (s, e) => Assert.Single (ApplicationOverlapped.OverlappedChildren!); Application.Iteration += (s, a) => { @@ -559,7 +560,7 @@ public class OverlappedTests stage.Ready += (s, e) => { - Assert.Equal (iterations, Application.OverlappedChildren.Count); + Assert.Equal (iterations, ApplicationOverlapped.OverlappedChildren!.Count); stage.RequestStop (); }; @@ -570,7 +571,7 @@ public class OverlappedTests allStageClosed = true; } - Assert.Equal (iterations, Application.OverlappedChildren.Count); + Assert.Equal (iterations, ApplicationOverlapped.OverlappedChildren!.Count); if (running) { @@ -581,7 +582,7 @@ public class OverlappedTests rpt.Ready += (s, e) => { iterations++; - Assert.Equal (iterations, Application.OverlappedChildren.Count); + Assert.Equal (iterations, ApplicationOverlapped.OverlappedChildren.Count); }; Application.Run (rpt); @@ -593,28 +594,28 @@ public class OverlappedTests else if (iterations == 11 && running) { running = false; - Assert.Equal (iterations, Application.OverlappedChildren.Count); + Assert.Equal (iterations, ApplicationOverlapped.OverlappedChildren!.Count); } else if (!overlappedRequestStop && running && !allStageClosed) { - Assert.Equal (iterations, Application.OverlappedChildren.Count); + Assert.Equal (iterations, ApplicationOverlapped.OverlappedChildren!.Count); } else if (!overlappedRequestStop && !running && allStageClosed) { - Assert.Equal (iterations, Application.OverlappedChildren.Count); + Assert.Equal (iterations, ApplicationOverlapped.OverlappedChildren!.Count); overlappedRequestStop = true; - overlapped.RequestStop (); + overlapped?.RequestStop (); } else { - Assert.Empty (Application.OverlappedChildren); + Assert.Empty (ApplicationOverlapped.OverlappedChildren!); } }; Application.Run (overlapped); - Assert.Empty (Application.OverlappedChildren); - Assert.NotNull (Application.OverlappedTop); + Assert.Empty (ApplicationOverlapped.OverlappedChildren!); + Assert.NotNull (ApplicationOverlapped.OverlappedTop); Assert.NotNull (Application.Top); overlapped.Dispose (); } @@ -652,32 +653,32 @@ public class OverlappedTests overlapped.Ready += (s, e) => { - Assert.Empty (Application.OverlappedChildren); + Assert.Empty (ApplicationOverlapped.OverlappedChildren!); Application.Run (c1); }; c1.Ready += (s, e) => { - Assert.Single (Application.OverlappedChildren); + Assert.Single (ApplicationOverlapped.OverlappedChildren!); Application.Run (c2); }; c2.Ready += (s, e) => { - Assert.Equal (2, Application.OverlappedChildren.Count); + Assert.Equal (2, ApplicationOverlapped.OverlappedChildren!.Count); Application.Run (c3); }; c3.Ready += (s, e) => { - Assert.Equal (3, Application.OverlappedChildren.Count); + Assert.Equal (3, ApplicationOverlapped.OverlappedChildren!.Count); Application.Run (d); }; // Also easy because the Overlapped Container handles all at once d.Ready += (s, e) => { - Assert.Equal (3, Application.OverlappedChildren.Count); + Assert.Equal (3, ApplicationOverlapped.OverlappedChildren!.Count); // This will not close the OverlappedContainer because d is a modal Toplevel Application.RequestStop (overlapped); @@ -696,11 +697,11 @@ public class OverlappedTests } else { - Assert.Equal (iterations, Application.OverlappedChildren.Count); + Assert.Equal (iterations, ApplicationOverlapped.OverlappedChildren!.Count); for (var i = 0; i < iterations; i++) { - Assert.Equal ((iterations - i + 1).ToString (), Application.OverlappedChildren [i].Id); + Assert.Equal ((iterations - i + 1).ToString (), ApplicationOverlapped.OverlappedChildren [i].Id); } } @@ -709,8 +710,8 @@ public class OverlappedTests Application.Run (overlapped); - Assert.Empty (Application.OverlappedChildren); - Assert.NotNull (Application.OverlappedTop); + Assert.Empty (ApplicationOverlapped.OverlappedChildren!); + Assert.NotNull (ApplicationOverlapped.OverlappedTop); Assert.NotNull (Application.Top); overlapped.Dispose (); } @@ -731,32 +732,32 @@ public class OverlappedTests overlapped.Ready += (s, e) => { - Assert.Empty (Application.OverlappedChildren); + Assert.Empty (ApplicationOverlapped.OverlappedChildren!); Application.Run (c1); }; c1.Ready += (s, e) => { - Assert.Single (Application.OverlappedChildren); + Assert.Single (ApplicationOverlapped.OverlappedChildren!); Application.Run (c2); }; c2.Ready += (s, e) => { - Assert.Equal (2, Application.OverlappedChildren.Count); + Assert.Equal (2, ApplicationOverlapped.OverlappedChildren!.Count); Application.Run (c3); }; c3.Ready += (s, e) => { - Assert.Equal (3, Application.OverlappedChildren.Count); + Assert.Equal (3, ApplicationOverlapped.OverlappedChildren!.Count); Application.Run (d); }; //More harder because it's sequential. d.Ready += (s, e) => { - Assert.Equal (3, Application.OverlappedChildren.Count); + Assert.Equal (3, ApplicationOverlapped.OverlappedChildren!.Count); // Close the Dialog Application.RequestStop (); @@ -776,11 +777,11 @@ public class OverlappedTests } else { - Assert.Equal (iterations, Application.OverlappedChildren.Count); + Assert.Equal (iterations, ApplicationOverlapped.OverlappedChildren!.Count); for (var i = 0; i < iterations; i++) { - Assert.Equal ((iterations - i + 1).ToString (), Application.OverlappedChildren [i].Id); + Assert.Equal ((iterations - i + 1).ToString (), ApplicationOverlapped.OverlappedChildren [i].Id); } } @@ -789,8 +790,8 @@ public class OverlappedTests Application.Run (overlapped); - Assert.Empty (Application.OverlappedChildren); - Assert.NotNull (Application.OverlappedTop); + Assert.Empty (ApplicationOverlapped.OverlappedChildren!); + Assert.NotNull (ApplicationOverlapped.OverlappedTop); Assert.NotNull (Application.Top); overlapped.Dispose (); } @@ -811,32 +812,32 @@ public class OverlappedTests overlapped.Ready += (s, e) => { - Assert.Empty (Application.OverlappedChildren); + Assert.Empty (ApplicationOverlapped.OverlappedChildren!); Application.Run (c1); }; c1.Ready += (s, e) => { - Assert.Single (Application.OverlappedChildren); + Assert.Single (ApplicationOverlapped.OverlappedChildren!); Application.Run (c2); }; c2.Ready += (s, e) => { - Assert.Equal (2, Application.OverlappedChildren.Count); + Assert.Equal (2, ApplicationOverlapped.OverlappedChildren!.Count); Application.Run (c3); }; c3.Ready += (s, e) => { - Assert.Equal (3, Application.OverlappedChildren.Count); + Assert.Equal (3, ApplicationOverlapped.OverlappedChildren!.Count); Application.Run (d); }; // More easy because the Overlapped Container handles all at once d.Ready += (s, e) => { - Assert.Equal (3, Application.OverlappedChildren.Count); + Assert.Equal (3, ApplicationOverlapped.OverlappedChildren!.Count); // This will not close the OverlappedContainer because d is a modal Toplevel and will be closed. overlapped.RequestStop (); @@ -855,11 +856,11 @@ public class OverlappedTests } else { - Assert.Equal (iterations, Application.OverlappedChildren.Count); + Assert.Equal (iterations, ApplicationOverlapped.OverlappedChildren!.Count); for (var i = 0; i < iterations; i++) { - Assert.Equal ((iterations - i + 1).ToString (), Application.OverlappedChildren [i].Id); + Assert.Equal ((iterations - i + 1).ToString (), ApplicationOverlapped.OverlappedChildren [i].Id); } } @@ -868,8 +869,8 @@ public class OverlappedTests Application.Run (overlapped); - Assert.Empty (Application.OverlappedChildren); - Assert.NotNull (Application.OverlappedTop); + Assert.Empty (ApplicationOverlapped.OverlappedChildren!); + Assert.NotNull (ApplicationOverlapped.OverlappedTop); Assert.NotNull (Application.Top); overlapped.Dispose (); } @@ -885,7 +886,7 @@ public class OverlappedTests RunState rsOverlapped = Application.Begin (overlapped); // Need to fool MainLoop into thinking it's running - Application.MainLoop.Running = true; + Application.MainLoop!.Running = true; // RunIteration must be call on each iteration because // it's using the Begin and not the Run method @@ -894,7 +895,7 @@ public class OverlappedTests Assert.Equal (overlapped, rsOverlapped.Toplevel); Assert.Equal (Application.Top, rsOverlapped.Toplevel); - Assert.Equal (Application.OverlappedTop, rsOverlapped.Toplevel); + Assert.Equal (ApplicationOverlapped.OverlappedTop, rsOverlapped.Toplevel); Assert.Equal (Application.Current, rsOverlapped.Toplevel); Assert.Equal (overlapped, Application.Current); @@ -903,7 +904,7 @@ public class OverlappedTests Assert.Equal (overlapped, rsOverlapped.Toplevel); Assert.Equal (Application.Top, rsOverlapped.Toplevel); - Assert.Equal (Application.OverlappedTop, rsOverlapped.Toplevel); + Assert.Equal (ApplicationOverlapped.OverlappedTop, rsOverlapped.Toplevel); // The win1 Visible is false and cannot be set as the Current Assert.Equal (Application.Current, rsOverlapped.Toplevel); Assert.Equal (overlapped, Application.Current); @@ -916,7 +917,7 @@ public class OverlappedTests // and not the original overlapped Assert.Equal (win2, rsOverlapped.Toplevel); Assert.Equal (Application.Top, overlapped); - Assert.Equal (Application.OverlappedTop, overlapped); + Assert.Equal (ApplicationOverlapped.OverlappedTop, overlapped); Assert.Equal (Application.Current, rsWin2.Toplevel); Assert.Equal (win2, Application.Current); Assert.Equal (win1, rsWin1.Toplevel); @@ -931,7 +932,7 @@ public class OverlappedTests Assert.Equal (win2, rsOverlapped.Toplevel); Assert.Equal (Application.Top, overlapped); - Assert.Equal (Application.OverlappedTop, overlapped); + Assert.Equal (ApplicationOverlapped.OverlappedTop, overlapped); Assert.Equal (Application.Current, rsWin2.Toplevel); Assert.Equal (win2, Application.Current); Assert.Equal (win1, rsWin1.Toplevel); @@ -945,7 +946,7 @@ public class OverlappedTests Assert.Equal (win2, rsOverlapped.Toplevel); Assert.Equal (Application.Top, overlapped); - Assert.Equal (Application.OverlappedTop, overlapped); + Assert.Equal (ApplicationOverlapped.OverlappedTop, overlapped); Assert.Equal (Application.Current, rsWin2.Toplevel); Assert.Equal (win2, Application.Current); Assert.Equal (win1, rsWin1.Toplevel); @@ -963,7 +964,7 @@ public class OverlappedTests #endif Assert.Null (rsOverlapped.Toplevel); Assert.Equal (Application.Top, overlapped); - Assert.Equal (Application.OverlappedTop, overlapped); + Assert.Equal (ApplicationOverlapped.OverlappedTop, overlapped); Assert.Equal (Application.Current, rsWin1.Toplevel); Assert.Equal (win1, Application.Current); Assert.Equal (win1, rsWin1.Toplevel); @@ -978,7 +979,7 @@ public class OverlappedTests #endif Assert.Null (rsOverlapped.Toplevel); Assert.Equal (Application.Top, overlapped); - Assert.Equal (Application.OverlappedTop, overlapped); + Assert.Equal (ApplicationOverlapped.OverlappedTop, overlapped); Assert.Equal (Application.Current, overlapped); Assert.Null (rsWin1.Toplevel); // See here that the only Toplevel that needs to End is the overlapped @@ -994,7 +995,7 @@ public class OverlappedTests #endif Assert.Null (rsOverlapped.Toplevel); Assert.Equal (Application.Top, overlapped); - Assert.Equal (Application.OverlappedTop, overlapped); + Assert.Equal (ApplicationOverlapped.OverlappedTop, overlapped); Assert.Null (Application.Current); Assert.Null (rsWin1.Toplevel); Assert.Null (rsWin2.Toplevel); @@ -1021,10 +1022,10 @@ public class OverlappedTests public void KeyBindings_Command_With_OverlappedTop () { Toplevel top = new (); - Assert.Null (Application.OverlappedTop); + Assert.Null (ApplicationOverlapped.OverlappedTop); top.IsOverlappedContainer = true; Application.Begin (top); - Assert.Equal (Application.Top, Application.OverlappedTop); + Assert.Equal (Application.Top, ApplicationOverlapped.OverlappedTop); var isRunning = true; @@ -1060,7 +1061,7 @@ public class OverlappedTests Assert.Null (top.Focused); Assert.Equal (top, Application.Current); Assert.True (top.IsCurrentTop); - Assert.Equal (top, Application.OverlappedTop); + Assert.Equal (top, ApplicationOverlapped.OverlappedTop); Application.Begin (win1); Assert.Equal (new (0, 0, 40, 25), win1.Frame); Assert.NotEqual (top, Application.Current); @@ -1072,7 +1073,7 @@ public class OverlappedTests Assert.Null (top.MostFocused); Assert.Equal (tf1W1, win1.MostFocused); Assert.True (win1.IsOverlapped); - Assert.Single (Application.OverlappedChildren); + Assert.Single (ApplicationOverlapped.OverlappedChildren!); Application.Begin (win2); Assert.Equal (new (0, 0, 40, 25), win2.Frame); Assert.NotEqual (top, Application.Current); @@ -1083,16 +1084,16 @@ public class OverlappedTests Assert.Null (top.Focused); Assert.Null (top.MostFocused); Assert.Equal (tf1W2, win2.MostFocused); - Assert.Equal (2, Application.OverlappedChildren.Count); + Assert.Equal (2, ApplicationOverlapped.OverlappedChildren!.Count); - Application.MoveToOverlappedChild (win1); + ApplicationOverlapped.MoveToOverlappedChild (win1); Assert.Equal (win1, Application.Current); - Assert.Equal (win1, Application.OverlappedChildren [0]); + Assert.Equal (win1, ApplicationOverlapped.OverlappedChildren [0]); win1.Running = true; Assert.True (Application.OnKeyDown (Application.QuitKey)); Assert.False (isRunning); Assert.False (win1.Running); - Assert.Equal (win1, Application.OverlappedChildren [0]); + Assert.Equal (win1, ApplicationOverlapped.OverlappedChildren [0]); Assert.True ( Application.OnKeyDown (Key.Z.WithCtrl) @@ -1110,77 +1111,77 @@ public class OverlappedTests Assert.Equal ($"First line Win1{Environment.NewLine}Second line Win1", tvW1.Text); Assert.True (Application.OnKeyDown (Key.Tab.WithCtrl)); // move to win2 - Assert.Equal (win2, Application.OverlappedChildren [0]); + Assert.Equal (win2, ApplicationOverlapped.OverlappedChildren [0]); Assert.True (Application.OnKeyDown (Key.Tab.WithCtrl.WithShift)); // move back to win1 - Assert.Equal (win1, Application.OverlappedChildren [0]); + Assert.Equal (win1, ApplicationOverlapped.OverlappedChildren [0]); Assert.Equal (tvW1, win1.MostFocused); Assert.True (Application.OnKeyDown (Key.Tab)); // text view eats tab - Assert.Equal (win1, Application.OverlappedChildren [0]); + Assert.Equal (win1, ApplicationOverlapped.OverlappedChildren [0]); Assert.Equal (tvW1, win1.MostFocused); tvW1.AllowsTab = false; Assert.True (Application.OnKeyDown (Key.Tab)); // text view eats tab - Assert.Equal (win1, Application.OverlappedChildren [0]); + Assert.Equal (win1, ApplicationOverlapped.OverlappedChildren [0]); Assert.Equal (tf2W1, win1.MostFocused); Assert.True (Application.OnKeyDown (Key.CursorRight)); - Assert.Equal (win1, Application.OverlappedChildren [0]); + Assert.Equal (win1, ApplicationOverlapped.OverlappedChildren [0]); Assert.Equal (tf2W1, win1.MostFocused); Assert.True (Application.OnKeyDown (Key.CursorDown)); - Assert.Equal (win1, Application.OverlappedChildren [0]); + Assert.Equal (win1, ApplicationOverlapped.OverlappedChildren [0]); Assert.Equal (tf1W1, win1.MostFocused); #if UNIX_KEY_BINDINGS - Assert.True (Application.OverlappedChildren [0].ProcessKeyDown (new (Key.I.WithCtrl))); - Assert.Equal (win1, Application.OverlappedChildren [0]); + Assert.True (ApplicationOverlapped.OverlappedChildren [0].ProcessKeyDown (new (Key.I.WithCtrl))); + Assert.Equal (win1, ApplicationOverlapped.OverlappedChildren [0]); Assert.Equal (tf2W1, win1.MostFocused); #endif Assert.True (Application.OnKeyDown (Key.Tab)); - Assert.Equal (win1, Application.OverlappedChildren [0]); + Assert.Equal (win1, ApplicationOverlapped.OverlappedChildren [0]); Assert.Equal (tvW1, win1.MostFocused); Assert.True (Application.OnKeyDown (Key.CursorLeft)); // The view to the left of tvW1 is tf2W1, but tvW1 is still focused and eats cursor keys - Assert.Equal (win1, Application.OverlappedChildren [0]); + Assert.Equal (win1, ApplicationOverlapped.OverlappedChildren [0]); Assert.Equal (tvW1, win1.MostFocused); Assert.True (Application.OnKeyDown (Key.CursorUp)); - Assert.Equal (win1, Application.OverlappedChildren [0]); + Assert.Equal (win1, ApplicationOverlapped.OverlappedChildren [0]); Assert.Equal (tvW1, win1.MostFocused); Assert.True (Application.OnKeyDown (Key.Tab)); - Assert.Equal (win1, Application.OverlappedChildren [0]); + Assert.Equal (win1, ApplicationOverlapped.OverlappedChildren [0]); Assert.Equal (tf2W1, win1.MostFocused); Assert.True (Application.OnKeyDown (Key.Tab.WithCtrl)); // Move to win2 - Assert.Equal (win2, Application.OverlappedChildren [0]); + Assert.Equal (win2, ApplicationOverlapped.OverlappedChildren [0]); Assert.Equal (tf1W2, win2.MostFocused); tf2W2.SetFocus (); Assert.True (tf2W2.HasFocus); Assert.True (Application.OnKeyDown (Key.Tab.WithCtrl.WithShift)); - Assert.Equal (win1, Application.OverlappedChildren [0]); + Assert.Equal (win1, ApplicationOverlapped.OverlappedChildren [0]); Assert.Equal (tf2W1, win1.MostFocused); Assert.True (Application.OnKeyDown (Application.AlternateForwardKey)); - Assert.Equal (win2, Application.OverlappedChildren [0]); + Assert.Equal (win2, ApplicationOverlapped.OverlappedChildren [0]); Assert.Equal (tf2W2, win2.MostFocused); Assert.True (Application.OnKeyDown (Application.AlternateBackwardKey)); - Assert.Equal (win1, Application.OverlappedChildren [0]); + Assert.Equal (win1, ApplicationOverlapped.OverlappedChildren [0]); Assert.Equal (tf2W1, win1.MostFocused); Assert.True (Application.OnKeyDown (Key.CursorDown)); - Assert.Equal (win1, Application.OverlappedChildren [0]); + Assert.Equal (win1, ApplicationOverlapped.OverlappedChildren [0]); Assert.Equal (tf1W1, win1.MostFocused); #if UNIX_KEY_BINDINGS Assert.True (Application.OnKeyDown (new (Key.B.WithCtrl))); #else Assert.True (Application.OnKeyDown (Key.CursorLeft)); #endif - Assert.Equal (win1, Application.OverlappedChildren [0]); + Assert.Equal (win1, ApplicationOverlapped.OverlappedChildren [0]); Assert.Equal (tf1W1, win1.MostFocused); Assert.True (Application.OnKeyDown (Key.CursorDown)); - Assert.Equal (win1, Application.OverlappedChildren [0]); + Assert.Equal (win1, ApplicationOverlapped.OverlappedChildren [0]); Assert.Equal (tvW1, win1.MostFocused); Assert.Equal (Point.Empty, tvW1.CursorPosition); Assert.True (Application.OnKeyDown (Key.End.WithCtrl)); - Assert.Equal (win1, Application.OverlappedChildren [0]); + Assert.Equal (win1, ApplicationOverlapped.OverlappedChildren [0]); Assert.Equal (tvW1, win1.MostFocused); Assert.Equal (new (16, 1), tvW1.CursorPosition); #if UNIX_KEY_BINDINGS @@ -1188,11 +1189,11 @@ public class OverlappedTests #else Assert.True (Application.OnKeyDown (Key.CursorRight)); #endif - Assert.Equal (win1, Application.OverlappedChildren [0]); + Assert.Equal (win1, ApplicationOverlapped.OverlappedChildren [0]); Assert.Equal (tvW1, win1.MostFocused); #if UNIX_KEY_BINDINGS - Assert.True (Application.OverlappedChildren [0].ProcessKeyDown (new (Key.L.WithCtrl))); + Assert.True (ApplicationOverlapped.OverlappedChildren [0].ProcessKeyDown (new (Key.L.WithCtrl))); #endif win2.Dispose (); win1.Dispose (); From cb3e80666f645c7ca31e0a98e7af9e03f4068fd9 Mon Sep 17 00:00:00 2001 From: Tig Date: Wed, 24 Jul 2024 15:43:35 -0600 Subject: [PATCH 25/33] Moved Overlapped stuff to ApplicationOverlap static class. Fixed nullable warnings. --- Terminal.Gui/Application/Application.Overlapped.cs | 6 ++---- UnitTests/Views/OverlappedTests.cs | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Terminal.Gui/Application/Application.Overlapped.cs b/Terminal.Gui/Application/Application.Overlapped.cs index ce88c1567..cf9212d80 100644 --- a/Terminal.Gui/Application/Application.Overlapped.cs +++ b/Terminal.Gui/Application/Application.Overlapped.cs @@ -106,10 +106,8 @@ public static class ApplicationOverlapped /// public static bool MoveToOverlappedChild (Toplevel? top) { - if (top is null) - { - return false; - } + ArgumentNullException.ThrowIfNull (top); + if (top.Visible && OverlappedTop is { } && Application.Current?.Modal == false) { lock (Application.TopLevels) diff --git a/UnitTests/Views/OverlappedTests.cs b/UnitTests/Views/OverlappedTests.cs index c1b0ec733..91555ca91 100644 --- a/UnitTests/Views/OverlappedTests.cs +++ b/UnitTests/Views/OverlappedTests.cs @@ -527,7 +527,7 @@ public class OverlappedTests [Fact] public void MoveToOverlappedChild_Throw_NullReferenceException_Passing_Null_Parameter () { - Assert.Throws (delegate { ApplicationOverlapped.MoveToOverlappedChild (null); }); + Assert.Throws (delegate { ApplicationOverlapped.MoveToOverlappedChild (null); }); } [Fact] From cbecae5d47aea31611137cd060679e6445b6830c Mon Sep 17 00:00:00 2001 From: Tig Date: Wed, 24 Jul 2024 15:48:41 -0600 Subject: [PATCH 26/33] Moved Overlapped stuff to ApplicationOverlap static class. Fixed nullable warnings. --- Terminal.Gui/Application/Application.Navigation.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Terminal.Gui/Application/Application.Navigation.cs b/Terminal.Gui/Application/Application.Navigation.cs index 44bc6e99a..fde03913b 100644 --- a/Terminal.Gui/Application/Application.Navigation.cs +++ b/Terminal.Gui/Application/Application.Navigation.cs @@ -1,6 +1,9 @@ #nullable enable namespace Terminal.Gui; +/// +/// Helper class for navigation. +/// internal static class ApplicationNavigation { /// From 331d9726d7210578dfb195f1c4d5886288dc42cf Mon Sep 17 00:00:00 2001 From: Tig Date: Wed, 24 Jul 2024 16:18:20 -0600 Subject: [PATCH 27/33] nullable enable TopLevel --- .../Application/Application.Overlapped.cs | 21 +++++- Terminal.Gui/Application/Application.Run.cs | 2 +- Terminal.Gui/View/Layout/ViewLayout.cs | 2 +- Terminal.Gui/Views/Toplevel.cs | 73 +++++++++++-------- Terminal.Gui/Views/ToplevelOverlapped.cs | 3 - UICatalog/UICatalog.cs | 6 +- UnitTests/Views/OverlappedTests.cs | 16 ++-- UnitTests/Views/ToplevelTests.cs | 2 +- 8 files changed, 72 insertions(+), 53 deletions(-) diff --git a/Terminal.Gui/Application/Application.Overlapped.cs b/Terminal.Gui/Application/Application.Overlapped.cs index cf9212d80..8ae645717 100644 --- a/Terminal.Gui/Application/Application.Overlapped.cs +++ b/Terminal.Gui/Application/Application.Overlapped.cs @@ -1,4 +1,6 @@ #nullable enable +using System.Reflection; + namespace Terminal.Gui; /// @@ -6,6 +8,17 @@ namespace Terminal.Gui; /// public static class ApplicationOverlapped { + + /// + /// Gets or sets if is in overlapped mode within a Toplevel container. + /// + /// + /// + public static bool IsOverlapped (Toplevel? top) + { + return ApplicationOverlapped.OverlappedTop is { } && ApplicationOverlapped.OverlappedTop != top && !top!.Modal; + } + /// /// Gets the list of the Overlapped children which are not modal from the /// . @@ -99,7 +112,7 @@ public static class ApplicationOverlapped } /// - /// Move to the next Overlapped child from the and set it as the if + /// Move to the next Overlapped child from the and set it as the if /// it is not already. /// /// @@ -262,7 +275,7 @@ public static class ApplicationOverlapped } /// - /// Given , returns the first Superview up the chain that is . + /// Given , returns the first Superview up the chain that is . /// internal static View? FindTopFromView (View? view) { @@ -284,7 +297,7 @@ public static class ApplicationOverlapped } /// - /// If the is not the then is moved to the top of + /// If the is not the then is moved to the top of /// the Toplevel stack and made Current. /// /// @@ -361,7 +374,7 @@ public static class ApplicationOverlapped { lock (Application.TopLevels) { - Application.TopLevels.MoveTo (top, 0, new ToplevelEqualityComparer ()); + Application.TopLevels.MoveTo (top!, 0, new ToplevelEqualityComparer ()); Application.Current = top; } } diff --git a/Terminal.Gui/Application/Application.Run.cs b/Terminal.Gui/Application/Application.Run.cs index 3e088c348..57097cbda 100644 --- a/Terminal.Gui/Application/Application.Run.cs +++ b/Terminal.Gui/Application/Application.Run.cs @@ -579,7 +579,7 @@ public static partial class Application // Run (Begin, Run, End, Stop) { ApplicationOverlapped.OverlappedTop?.OnDeactivate (state.Toplevel); state.Toplevel = Current; - ApplicationOverlapped.OverlappedTop?.OnActivate (state.Toplevel); + ApplicationOverlapped.OverlappedTop?.OnActivate (state.Toplevel!); Top!.SetSubViewNeedsDisplay (); Refresh (); } diff --git a/Terminal.Gui/View/Layout/ViewLayout.cs b/Terminal.Gui/View/Layout/ViewLayout.cs index 4e2531745..72a9bf14c 100644 --- a/Terminal.Gui/View/Layout/ViewLayout.cs +++ b/Terminal.Gui/View/Layout/ViewLayout.cs @@ -404,7 +404,7 @@ public partial class View int targetY, out int nx, out int ny, - out StatusBar statusBar + out StatusBar? statusBar ) { int maxDimension; diff --git a/Terminal.Gui/Views/Toplevel.cs b/Terminal.Gui/Views/Toplevel.cs index d13ae0766..26fe724c8 100644 --- a/Terminal.Gui/Views/Toplevel.cs +++ b/Terminal.Gui/Views/Toplevel.cs @@ -1,3 +1,4 @@ +#nullable enable namespace Terminal.Gui; /// @@ -60,7 +61,7 @@ public partial class Toplevel : View /// public bool Modal { get; set; } - private void Toplevel_MouseClick (object sender, MouseEventEventArgs e) { e.Handled = InvokeCommand (Command.HotKey) == true; } + private void Toplevel_MouseClick (object? sender, MouseEventEventArgs e) { e.Handled = InvokeCommand (Command.HotKey) == true; } #endregion @@ -68,11 +69,11 @@ public partial class Toplevel : View // TODO: Deprecate - Any view can host a menubar in v2 /// Gets or sets the menu for this Toplevel. - public virtual MenuBar MenuBar { get; set; } + public MenuBar? MenuBar { get; set; } // TODO: Deprecate - Any view can host a statusbar in v2 /// Gets or sets the status bar for this Toplevel. - public virtual StatusBar StatusBar { get; set; } + public StatusBar? StatusBar { get; set; } /// public override View Add (View view) @@ -141,22 +142,22 @@ public partial class Toplevel : View /// Invoked when the last child of the Toplevel is closed from by /// . /// - public event EventHandler AllChildClosed; + public event EventHandler? AllChildClosed; // TODO: Overlapped - Rename to *Subviews* - Move to View? /// /// Invoked when a child of the Toplevel is closed by /// . /// - public event EventHandler ChildClosed; + public event EventHandler? ChildClosed; // TODO: Overlapped - Rename to *Subviews* - Move to View? /// Invoked when a child Toplevel's has been loaded. - public event EventHandler ChildLoaded; + public event EventHandler? ChildLoaded; // TODO: Overlapped - Rename to *Subviews* - Move to View? /// Invoked when a cjhild Toplevel's has been unloaded. - public event EventHandler ChildUnloaded; + public event EventHandler? ChildUnloaded; #endregion @@ -176,26 +177,26 @@ public partial class Toplevel : View // TODO: IRunnable: Re-implement as an event on IRunnable; IRunnable.Activating/Activate /// Invoked when the Toplevel becomes the Toplevel. - public event EventHandler Activate; + public event EventHandler? Activate; // TODO: IRunnable: Re-implement as an event on IRunnable; IRunnable.Deactivating/Deactivate? /// Invoked when the Toplevel ceases to be the Toplevel. - public event EventHandler Deactivate; + public event EventHandler? Deactivate; /// Invoked when the Toplevel's is closed by . - public event EventHandler Closed; + public event EventHandler? Closed; /// /// Invoked when the Toplevel's is being closed by /// . /// - public event EventHandler Closing; + public event EventHandler? Closing; /// /// Invoked when the has begun to be loaded. A Loaded event handler /// is a good place to finalize initialization before calling . /// - public event EventHandler Loaded; + public event EventHandler? Loaded; /// /// Called from before the redraws for the first @@ -209,8 +210,9 @@ public partial class Toplevel : View { IsLoaded = true; - foreach (Toplevel tl in Subviews.Where (v => v is Toplevel)) + foreach (var view in Subviews.Where (v => v is Toplevel)) { + var tl = (Toplevel)view; tl.OnLoaded (); } @@ -225,7 +227,7 @@ public partial class Toplevel : View /// on this . /// /// - public event EventHandler Ready; + public event EventHandler? Ready; /// /// Stops and closes this . If this Toplevel is the top-most Toplevel, @@ -288,7 +290,7 @@ public partial class Toplevel : View /// Invoked when the Toplevel has been unloaded. A Unloaded event handler is a good place /// to dispose objects after calling . /// - public event EventHandler Unloaded; + public event EventHandler? Unloaded; internal virtual void OnActivate (Toplevel deactivated) { Activate?.Invoke (this, new (deactivated)); } @@ -331,8 +333,9 @@ public partial class Toplevel : View /// internal virtual void OnReady () { - foreach (Toplevel tl in Subviews.Where (v => v is Toplevel)) + foreach (var view in Subviews.Where (v => v is Toplevel)) { + var tl = (Toplevel)view; tl.OnReady (); } @@ -342,8 +345,9 @@ public partial class Toplevel : View /// Called from before the is disposed. internal virtual void OnUnloaded () { - foreach (Toplevel tl in Subviews.Where (v => v is Toplevel)) + foreach (var view in Subviews.Where (v => v is Toplevel)) { + var tl = (Toplevel)view; tl.OnUnloaded (); } @@ -411,7 +415,7 @@ public partial class Toplevel : View /// public override bool OnLeave (View view) { return MostFocused?.OnLeave (view) ?? base.OnLeave (view); } - + #endregion #region Size / Position Management @@ -458,15 +462,20 @@ public partial class Toplevel : View /// implementation of specific positions for inherited views. /// /// The Toplevel to adjust. - public virtual void PositionToplevel (Toplevel top) + public virtual void PositionToplevel (Toplevel? top) { - View superView = GetLocationEnsuringFullVisibility ( + if (top is null) + { + return; + } + + View? superView = GetLocationEnsuringFullVisibility ( top, top.Frame.X, top.Frame.Y, out int nx, out int ny, - out StatusBar sb + out StatusBar? sb ); if (superView is null) @@ -482,25 +491,25 @@ public partial class Toplevel : View maxWidth -= superView.GetAdornmentsThickness ().Left + superView.GetAdornmentsThickness ().Right; } - if ((superView != top || top?.SuperView is { } || (top != Application.Top && top.Modal) || (top?.SuperView is null && top.IsOverlapped)) - && (top.Frame.X + top.Frame.Width > maxWidth || ny > top.Frame.Y)) + if ((superView != top || top?.SuperView is { } || (top != Application.Top && top!.Modal) || (top?.SuperView is null && ApplicationOverlapped.IsOverlapped (top))) + && (top!.Frame.X + top.Frame.Width > maxWidth || ny > top.Frame.Y)) { - if ((top.X is null || top.X is PosAbsolute) && top.Frame.X != nx) + if (top?.X is null or PosAbsolute && top?.Frame.X != nx) { - top.X = nx; + top!.X = nx; layoutSubviews = true; } - if ((top.Y is null || top.Y is PosAbsolute) && top.Frame.Y != ny) + if (top?.Y is null or PosAbsolute && top?.Frame.Y != ny) { - top.Y = ny; + top!.Y = ny; layoutSubviews = true; } } // TODO: v2 - This is a hack to get the StatusBar to be positioned correctly. if (sb != null - && !top.Subviews.Contains (sb) + && !top!.Subviews.Contains (sb) && ny + top.Frame.Height != superView.Frame.Height - (sb.Visible ? 1 : 0) && top.Height is DimFill && -top.Height.GetAnchor (0) < 1) @@ -521,7 +530,7 @@ public partial class Toplevel : View } /// Invoked when the terminal has been resized. The new of the terminal is provided. - public event EventHandler SizeChanging; + public event EventHandler? SizeChanging; private bool OutsideTopFrame (Toplevel top) { @@ -560,7 +569,7 @@ public class ToplevelEqualityComparer : IEqualityComparer /// The first object of type to compare. /// The second object of type to compare. /// if the specified objects are equal; otherwise, . - public bool Equals (Toplevel x, Toplevel y) + public bool Equals (Toplevel? x, Toplevel? y) { if (y is null && x is null) { @@ -623,7 +632,7 @@ public sealed class ToplevelComparer : IComparer /// equals .Greater than zero is greater than /// . /// - public int Compare (Toplevel x, Toplevel y) + public int Compare (Toplevel? x, Toplevel? y) { if (ReferenceEquals (x, y)) { @@ -640,6 +649,6 @@ public sealed class ToplevelComparer : IComparer return 1; } - return string.Compare (x.Id, y.Id); + return string.CompareOrdinal (x.Id, y.Id); } } diff --git a/Terminal.Gui/Views/ToplevelOverlapped.cs b/Terminal.Gui/Views/ToplevelOverlapped.cs index 06dac3694..28513c4ce 100644 --- a/Terminal.Gui/Views/ToplevelOverlapped.cs +++ b/Terminal.Gui/Views/ToplevelOverlapped.cs @@ -2,9 +2,6 @@ public partial class Toplevel { - /// Gets or sets if this Toplevel is in overlapped mode within a Toplevel container. - public bool IsOverlapped => ApplicationOverlapped.OverlappedTop is { } && ApplicationOverlapped.OverlappedTop != this && !Modal; - /// Gets or sets if this Toplevel is a container for overlapped children. public bool IsOverlappedContainer { get; set; } } diff --git a/UICatalog/UICatalog.cs b/UICatalog/UICatalog.cs index a9014d017..86352717a 100644 --- a/UICatalog/UICatalog.cs +++ b/UICatalog/UICatalog.cs @@ -658,7 +658,7 @@ internal class UICatalogApp ColorScheme = Colors.ColorSchemes [_topLevelColorScheme]; - MenuBar.Menus [0].Children [0].Shortcut = (KeyCode)Application.QuitKey; + MenuBar!.Menus [0].Children [0].Shortcut = (KeyCode)Application.QuitKey; if (StatusBar is { }) { @@ -942,7 +942,7 @@ internal class UICatalogApp { MiIsMenuBorderDisabled.Checked = (bool)!MiIsMenuBorderDisabled.Checked!; - MenuBar.MenusBorderStyle = !(bool)MiIsMenuBorderDisabled.Checked + MenuBar!.MenusBorderStyle = !(bool)MiIsMenuBorderDisabled.Checked ? LineStyle.Single : LineStyle.None; }; @@ -985,7 +985,7 @@ internal class UICatalogApp MiUseSubMenusSingleFrame.Action += () => { MiUseSubMenusSingleFrame.Checked = (bool)!MiUseSubMenusSingleFrame.Checked!; - MenuBar.UseSubMenusSingleFrame = (bool)MiUseSubMenusSingleFrame.Checked; + MenuBar!.UseSubMenusSingleFrame = (bool)MiUseSubMenusSingleFrame.Checked; }; menuItems.Add (MiUseSubMenusSingleFrame); diff --git a/UnitTests/Views/OverlappedTests.cs b/UnitTests/Views/OverlappedTests.cs index 91555ca91..80c8e18c2 100644 --- a/UnitTests/Views/OverlappedTests.cs +++ b/UnitTests/Views/OverlappedTests.cs @@ -231,11 +231,11 @@ public class OverlappedTests Application.Iteration += (s, a) => { - Assert.False (overlapped.IsOverlapped); - Assert.True (c1.IsOverlapped); - Assert.True (c2.IsOverlapped); - Assert.True (c3.IsOverlapped); - Assert.False (d.IsOverlapped); + Assert.False (ApplicationOverlapped.IsOverlapped(overlapped)); + Assert.True (ApplicationOverlapped.IsOverlapped(c1)); + Assert.True (ApplicationOverlapped.IsOverlapped(c2)); + Assert.True (ApplicationOverlapped.IsOverlapped(c3)); + Assert.False (ApplicationOverlapped.IsOverlapped(d)); overlapped.RequestStop (); }; @@ -1068,11 +1068,11 @@ public class OverlappedTests Assert.False (top.IsCurrentTop); Assert.Equal (win1, Application.Current); Assert.True (win1.IsCurrentTop); - Assert.True (win1.IsOverlapped); + Assert.True (ApplicationOverlapped.IsOverlapped(win1)); Assert.Null (top.Focused); Assert.Null (top.MostFocused); Assert.Equal (tf1W1, win1.MostFocused); - Assert.True (win1.IsOverlapped); + Assert.True (ApplicationOverlapped.IsOverlapped(win1)); Assert.Single (ApplicationOverlapped.OverlappedChildren!); Application.Begin (win2); Assert.Equal (new (0, 0, 40, 25), win2.Frame); @@ -1080,7 +1080,7 @@ public class OverlappedTests Assert.False (top.IsCurrentTop); Assert.Equal (win2, Application.Current); Assert.True (win2.IsCurrentTop); - Assert.True (win2.IsOverlapped); + Assert.True (ApplicationOverlapped.IsOverlapped(win2)); Assert.Null (top.Focused); Assert.Null (top.MostFocused); Assert.Equal (tf1W2, win2.MostFocused); diff --git a/UnitTests/Views/ToplevelTests.cs b/UnitTests/Views/ToplevelTests.cs index 8688c5d4a..9af790add 100644 --- a/UnitTests/Views/ToplevelTests.cs +++ b/UnitTests/Views/ToplevelTests.cs @@ -17,7 +17,7 @@ public partial class ToplevelTests (ITestOutputHelper output) Assert.Null (top.MenuBar); Assert.Null (top.StatusBar); Assert.False (top.IsOverlappedContainer); - Assert.False (top.IsOverlapped); + Assert.False (ApplicationOverlapped.IsOverlapped(top)); } [Fact] From ca4d10b5093dc406f1b07c648e27f17086b2cee0 Mon Sep 17 00:00:00 2001 From: Tig Date: Wed, 24 Jul 2024 19:22:59 -0600 Subject: [PATCH 28/33] WIP: Modify Focus logic to properly deal with ViewArrangement.Overlapped. --- Terminal.Gui/View/Adornment/Border.cs | 9 +- Terminal.Gui/View/ViewArrangement.cs | 13 +- Terminal.Gui/View/ViewSubViews.cs | 62 ++++-- UICatalog/Scenarios/ViewExperiments.cs | 249 ++++++------------------- 4 files changed, 128 insertions(+), 205 deletions(-) diff --git a/Terminal.Gui/View/Adornment/Border.cs b/Terminal.Gui/View/Adornment/Border.cs index 41fd02f9d..241b795c5 100644 --- a/Terminal.Gui/View/Adornment/Border.cs +++ b/Terminal.Gui/View/Adornment/Border.cs @@ -279,10 +279,11 @@ public class Border : Adornment return true; } - if (!Parent.CanFocus) - { - return false; - } + // BUGBUG: Shouldn't non-focusable views be draggable?? + //if (!Parent.CanFocus) + //{ + // return false; + //} if (!Parent.Arrangement.HasFlag (ViewArrangement.Movable)) { diff --git a/Terminal.Gui/View/ViewArrangement.cs b/Terminal.Gui/View/ViewArrangement.cs index b24529caf..df13c4762 100644 --- a/Terminal.Gui/View/ViewArrangement.cs +++ b/Terminal.Gui/View/ViewArrangement.cs @@ -53,7 +53,18 @@ public enum ViewArrangement /// /// If is also set, the top will not be resizable. /// - Resizable = LeftResizable | RightResizable | TopResizable | BottomResizable + Resizable = LeftResizable | RightResizable | TopResizable | BottomResizable, + + /// + /// The view overlap other views. + /// + /// + /// + /// When set, Tab and Shift-Tab will be constrained to the subviews of the view (normally, they will navigate to the next/prev view in the next/prev Tabindex). + /// Use Ctrl-Tab (Ctrl-PageDown) / Ctrl-Shift-Tab (Ctrl-PageUp) to move between overlapped views. + /// + /// + Overlapped = 32 } public partial class View { diff --git a/Terminal.Gui/View/ViewSubViews.cs b/Terminal.Gui/View/ViewSubViews.cs index 5da8dd76a..79cca431e 100644 --- a/Terminal.Gui/View/ViewSubViews.cs +++ b/Terminal.Gui/View/ViewSubViews.cs @@ -672,7 +672,7 @@ public partial class View /// /// Focuses the last focusable view in if one exists. If there are no views in then the focus is set to the view itself. /// - public void FocusFirst () + public void FocusFirst (bool overlapped = false) { if (!CanBeVisible (this)) { @@ -686,7 +686,7 @@ public partial class View return; } - foreach (View view in _tabIndexes) + foreach (View view in _tabIndexes.Where (v => !overlapped || v.Arrangement.HasFlag (ViewArrangement.Overlapped))) { if (view.CanFocus && view._tabStop && view.Visible && view.Enabled) { @@ -700,7 +700,7 @@ public partial class View /// /// Focuses the last focusable view in if one exists. If there are no views in then the focus is set to the view itself. /// - public void FocusLast () + public void FocusLast (bool overlapped = false) { if (!CanBeVisible (this)) { @@ -714,15 +714,11 @@ public partial class View return; } - for (int i = _tabIndexes.Count; i > 0;) + foreach (View view in _tabIndexes.Where (v => !overlapped || v.Arrangement.HasFlag (ViewArrangement.Overlapped)).Reverse ()) { - i--; - - View v = _tabIndexes [i]; - - if (v.CanFocus && v._tabStop && v.Visible && v.Enabled) + if (view.CanFocus && view._tabStop && view.Visible && view.Enabled) { - SetFocus (v); + SetFocus (view); return; } @@ -777,6 +773,18 @@ public partial class View { Focused.SetHasFocus (false, w); + // If the focused view is overlapped don't focus on the next if it's not overlapped. + if (Focused.Arrangement.HasFlag (ViewArrangement.Overlapped) && !w.Arrangement.HasFlag (ViewArrangement.Overlapped)) + { + return false; + } + + // If the focused view is not overlapped and the next is, skip it + if (!Focused.Arrangement.HasFlag (ViewArrangement.Overlapped) && w.Arrangement.HasFlag (ViewArrangement.Overlapped)) + { + continue; + } + if (w.CanFocus && w._tabStop && w.Visible && w.Enabled) { w.FocusLast (); @@ -788,9 +796,19 @@ public partial class View } } + // There's no prev view in tab indexes. if (Focused is { }) { + // Leave Focused Focused.SetHasFocus (false, this); + + if (Focused.Arrangement.HasFlag (ViewArrangement.Overlapped)) + { + FocusLast (true); + return true; + } + + // Signal to caller no next view was found Focused = null; } @@ -798,7 +816,7 @@ public partial class View } /// - /// Focuses the previous view in . If there is no previous view, the focus is set to the view itself. + /// Focuses the next view in . If there is no next view, the focus is set to the view itself. /// /// if next was focused, otherwise. public bool FocusNext () @@ -844,6 +862,18 @@ public partial class View { Focused.SetHasFocus (false, w); + // If the focused view is overlapped don't focus on the next if it's not overlapped. + if (Focused.Arrangement.HasFlag (ViewArrangement.Overlapped) && !w.Arrangement.HasFlag (ViewArrangement.Overlapped)) + { + return false; + } + + // If the focused view is not overlapped and the next is, skip it + if (!Focused.Arrangement.HasFlag (ViewArrangement.Overlapped) && w.Arrangement.HasFlag (ViewArrangement.Overlapped)) + { + continue; + } + if (w.CanFocus && w._tabStop && w.Visible && w.Enabled) { w.FocusFirst (); @@ -855,9 +885,19 @@ public partial class View } } + // There's no next view in tab indexes. if (Focused is { }) { + // Leave Focused Focused.SetHasFocus (false, this); + + if (Focused.Arrangement.HasFlag (ViewArrangement.Overlapped)) + { + FocusFirst (true); + return true; + } + + // Signal to caller no next view was found Focused = null; } diff --git a/UICatalog/Scenarios/ViewExperiments.cs b/UICatalog/Scenarios/ViewExperiments.cs index 5d08f0aa6..263a507d9 100644 --- a/UICatalog/Scenarios/ViewExperiments.cs +++ b/UICatalog/Scenarios/ViewExperiments.cs @@ -18,233 +18,104 @@ public class ViewExperiments : Scenario Title = GetQuitKeyAndName () }; - var containerLabel = new Label - { - X = 0, - Y = 0, - - Width = Dim.Fill (), - Height = 3 - }; - app.Add (containerLabel); - var view = new View { X = 2, - Y = Pos.Bottom (containerLabel), - Height = Dim.Fill (2), - Width = Dim.Fill (2), - Title = "View with 2xMargin, 2xBorder, & 2xPadding", + Y = 2, + Height = Dim.Auto (), + Width = Dim.Auto (), + Title = "View1", ColorScheme = Colors.ColorSchemes ["Base"], - Id = "DaView" + Id = "View1", + CanFocus = true, // Can't drag without this? BUGBUG + Arrangement = ViewArrangement.Movable | ViewArrangement.Overlapped }; + Button button = new () + { + Title = "Button_1", + }; + view.Add (button); + + button = new () + { + Y = Pos.Bottom (button), + Title = "Button_2", + }; + view.Add (button); + //app.Add (view); - view.Margin.Thickness = new (2, 2, 2, 2); + view.Margin.Thickness = new (0); view.Margin.ColorScheme = Colors.ColorSchemes ["Toplevel"]; view.Margin.Data = "Margin"; - view.Border.Thickness = new (3); - view.Border.LineStyle = LineStyle.Single; + view.Border.Thickness = new (1); + view.Border.LineStyle = LineStyle.Double; view.Border.ColorScheme = view.ColorScheme; view.Border.Data = "Border"; - view.Padding.Thickness = new (2); + view.Padding.Thickness = new (0); view.Padding.ColorScheme = Colors.ColorSchemes ["Error"]; view.Padding.Data = "Padding"; - var window1 = new Window + var view2 = new View { - X = 2, - Y = 3, - Height = 7, - Width = 17, - Title = "Window 1", - Text = "Window #2", - TextAlignment = Alignment.Center + X = Pos.Right (view), + Y = Pos.Bottom (view), + Height = Dim.Auto (), + Width = Dim.Auto (), + Title = "View2", + ColorScheme = Colors.ColorSchemes ["Base"], + Id = "View2", + CanFocus = true, // Can't drag without this? BUGBUG + Arrangement = ViewArrangement.Movable | ViewArrangement.Overlapped }; - window1.Margin.Thickness = new (0); - window1.Margin.ColorScheme = Colors.ColorSchemes ["Toplevel"]; - window1.Margin.Data = "Margin"; - window1.Border.Thickness = new (1); - window1.Border.LineStyle = LineStyle.Single; - window1.Border.ColorScheme = view.ColorScheme; - window1.Border.Data = "Border"; - window1.Padding.Thickness = new (0); - window1.Padding.ColorScheme = Colors.ColorSchemes ["Error"]; - window1.Padding.Data = "Padding"; - view.Add (window1); - - var window2 = new Window + button = new () { - X = Pos.Right (window1) + 1, - Y = 3, - Height = 5, - Width = 37, - Title = "Window2", - Text = "Window #2 (Right(window1)+1", - TextAlignment = Alignment.Center + Title = "Button_3", }; + view2.Add (button); - //view3.InitializeFrames (); - window2.Margin.Thickness = new (1, 1, 0, 0); - window2.Margin.ColorScheme = Colors.ColorSchemes ["Toplevel"]; - window2.Margin.Data = "Margin"; - window2.Border.Thickness = new (1, 1, 1, 1); - window2.Border.LineStyle = LineStyle.Single; - window2.Border.ColorScheme = view.ColorScheme; - window2.Border.Data = "Border"; - window2.Padding.Thickness = new (1, 1, 0, 0); - window2.Padding.ColorScheme = Colors.ColorSchemes ["Error"]; - window2.Padding.Data = "Padding"; - - view.Add (window2); - - var view4 = new View + button = new () { - X = Pos.Right (window2) + 1, - Y = 3, - Height = 5, - Width = 37, - Title = "View4", - Text = "View #4 (Right(window2)+1", - TextAlignment = Alignment.Center + Y = Pos.Bottom (button), + Title = "Button_4", }; + view2.Add (button); - //view4.InitializeFrames (); - view4.Margin.Thickness = new (0, 0, 1, 1); - view4.Margin.ColorScheme = Colors.ColorSchemes ["Toplevel"]; - view4.Margin.Data = "Margin"; - view4.Border.Thickness = new (1, 1, 1, 1); - view4.Border.LineStyle = LineStyle.Single; - view4.Border.ColorScheme = view.ColorScheme; - view4.Border.Data = "Border"; - view4.Padding.Thickness = new (0, 0, 1, 1); - view4.Padding.ColorScheme = Colors.ColorSchemes ["Error"]; - view4.Padding.Data = "Padding"; + view2.Add (button); + view2.Margin.Thickness = new (0); + view2.Margin.ColorScheme = Colors.ColorSchemes ["Toplevel"]; + view2.Margin.Data = "Margin"; + view2.Border.Thickness = new (1); + view2.Border.LineStyle = LineStyle.Double; + view2.Border.ColorScheme = view2.ColorScheme; + view2.Border.Data = "Border"; + view2.Padding.Thickness = new (0); + view2.Padding.ColorScheme = Colors.ColorSchemes ["Error"]; + view2.Padding.Data = "Padding"; - view.Add (view4); - - var view5 = new View + button = new () { - X = Pos.Right (view4) + 1, - Y = 3, - Height = Dim.Fill (2), - Width = Dim.Fill (), - Title = "View5", - Text = "View #5 (Right(view4)+1 Fill", - TextAlignment = Alignment.Center + X = Pos.AnchorEnd (), + Y = Pos.AnchorEnd (), + Title = "Button_5", }; - //view5.InitializeFrames (); - view5.Margin.Thickness = new (0, 0, 0, 0); - view5.Margin.ColorScheme = Colors.ColorSchemes ["Toplevel"]; - view5.Margin.Data = "Margin"; - view5.Border.Thickness = new (1, 1, 1, 1); - view5.Border.LineStyle = LineStyle.Single; - view5.Border.ColorScheme = view.ColorScheme; - view5.Border.Data = "Border"; - view5.Padding.Thickness = new (0, 0, 0, 0); - view5.Padding.ColorScheme = Colors.ColorSchemes ["Error"]; - view5.Padding.Data = "Padding"; - - view.Add (view5); - - var label = new Label { Text = "AutoSize true; 1;1:", X = 1, Y = 1 }; - view.Add (label); - - var edit = new TextField - { - Text = "Right (label)", - X = Pos.Right (label), - Y = 1, - Width = 15, - Height = 1 - }; - view.Add (edit); - - edit = new() - { - Text = "Right (edit) + 1", - X = Pos.Right (edit) + 1, - Y = 1, - Width = 20, - Height = 1 - }; - view.Add (edit); - - var label50 = new View - { - Title = "Border Inherit Demo", - Text = "Center();50%", - X = Pos.Center (), - Y = Pos.Percent (50), - Width = 30, - TextAlignment = Alignment.Center - }; - label50.Border.Thickness = new (1, 3, 1, 1); - label50.Height = 5; - view.Add (label50); - - edit = new() - { - Text = "0 + Percent(50);70%", - X = 0 + Pos.Percent (50), - Y = Pos.Percent (70), - Width = 30, - Height = 1 - }; - view.Add (edit); - - edit = new() { Text = "AnchorEnd ();AnchorEnd ()", X = Pos.AnchorEnd (), Y = Pos.AnchorEnd (), Width = 30, Height = 1 }; - view.Add (edit); - - edit = new() - { - Text = "Left;AnchorEnd (2)", - X = 0, - Y = Pos.AnchorEnd (2), - Width = 30, - Height = 1 - }; - view.Add (edit); - - view.LayoutComplete += (s, e) => - { - containerLabel.Text = - $"Container.Frame: { - app.Frame - } .Bounds: { - app.Viewport - }\nView.Frame: { - view.Frame - } .Viewport: { - view.Viewport - } .viewportOffset: { - view.GetViewportOffsetFromFrame () - }\n .Padding.Frame: { - view.Padding.Frame - } .Padding.Viewport: { - view.Padding.Viewport - }"; - }; - - view.X = Pos.Center (); - var editor = new AdornmentsEditor { X = 0, - Y = Pos.Bottom (containerLabel), + Y = 0, AutoSelectViewToEdit = true }; app.Add (editor); - view.X = 36; + view.X = 34; view.Y = 4; - view.Width = Dim.Fill (); - view.Height = Dim.Fill (); app.Add (view); + app.Add (view2); + app.Add (button); Application.Run (app); app.Dispose (); From 3a40851848ef741076f80a5de2dafcabdbf57d37 Mon Sep 17 00:00:00 2001 From: Tig Date: Wed, 24 Jul 2024 19:45:25 -0600 Subject: [PATCH 29/33] WIP: More - Modify Focus logic to properly deal with ViewArrangement.Overlapped. --- .../Application/Application.Navigation.cs | 39 ++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/Terminal.Gui/Application/Application.Navigation.cs b/Terminal.Gui/Application/Application.Navigation.cs index fde03913b..6f80320b8 100644 --- a/Terminal.Gui/Application/Application.Navigation.cs +++ b/Terminal.Gui/Application/Application.Navigation.cs @@ -1,4 +1,6 @@ #nullable enable +using System.Security.Cryptography; + namespace Terminal.Gui; /// @@ -78,8 +80,10 @@ internal static class ApplicationNavigation idx++; } } + /// - /// Moves the focus to + /// Moves the focus to the next view. Honors and will only move to the next subview + /// if the current and next subviews are not overlapped. /// internal static void MoveNextView () { @@ -101,19 +105,40 @@ internal static class ApplicationNavigation } } + /// + /// Moves the focus to the next subview or the next subview that has set. + /// internal static void MoveNextViewOrTop () { if (ApplicationOverlapped.OverlappedTop is null) { Toplevel? top = Application.Current!.Modal ? Application.Current : Application.Top; - top!.FocusNext (); - if (top.Focused is null) + if (!Application.Current.FocusNext ()) { - top.FocusNext (); + Application.Current.FocusNext (); } - top.SetNeedsDisplay (); + if (top != Application.Current.Focused && top != Application.Current.Focused?.Focused) + { + top?.SetNeedsDisplay (); + Application.Current.Focused?.SetNeedsDisplay (); + } + else + { + FocusNearestView (Application.Current.SuperView?.TabIndexes, View.NavigationDirection.Forward); + } + + + + //top!.FocusNext (); + + //if (top.Focused is null) + //{ + // top.FocusNext (); + //} + + //top.SetNeedsDisplay (); ApplicationOverlapped.BringOverlappedTopToFront (); } else @@ -122,6 +147,10 @@ internal static class ApplicationNavigation } } + /// + /// Moves the focus to the next view. Honors and will only move to the next subview + /// if the current and next subviews are not overlapped. + /// internal static void MovePreviousView () { View? old = GetDeepestFocusedSubview (Application.Current!.Focused); From d874f5628295f2811f59ba1321be11f5bb385af5 Mon Sep 17 00:00:00 2001 From: Tig Date: Thu, 25 Jul 2024 10:40:28 -0600 Subject: [PATCH 30/33] Reorganized View source files to get my head straight --- .../Application/Application.Keyboard.cs | 30 +- Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs | 42 - .../ConsoleDrivers/CursorVisibility.cs | 44 + Terminal.Gui/View/DrawEventArgs.cs | 29 + Terminal.Gui/View/Layout/LayoutEventArgs.cs | 12 + .../View/Navigation/FocusEventArgs.cs | 27 + .../{ViewAdornments.cs => View.Adornments.cs} | 2 +- Terminal.Gui/View/View.Arrangement.cs | 15 + .../View/{ViewContent.cs => View.Content.cs} | 0 Terminal.Gui/View/View.Cursor.cs | 35 + ...ViewDiagnostics.cs => View.Diagnostics.cs} | 0 .../View/{ViewDrawing.cs => View.Drawing.cs} | 2 +- Terminal.Gui/View/View.Hierarchy.cs | 320 ++++++ .../{ViewKeyboard.cs => View.Keyboard.cs} | 115 +-- .../{Layout/ViewLayout.cs => View.Layout.cs} | 2 +- .../View/{ViewMouse.cs => View.Mouse.cs} | 2 +- Terminal.Gui/View/View.Navigation.cs | 813 +++++++++++++++ .../View/{ViewText.cs => View.Text.cs} | 2 +- Terminal.Gui/View/ViewArrangement.cs | 30 +- Terminal.Gui/View/ViewEventArgs.cs | 67 +- Terminal.Gui/View/ViewSubViews.cs | 948 ------------------ UICatalog/Scenarios/ViewExperiments.cs | 25 +- 22 files changed, 1318 insertions(+), 1244 deletions(-) create mode 100644 Terminal.Gui/ConsoleDrivers/CursorVisibility.cs create mode 100644 Terminal.Gui/View/DrawEventArgs.cs create mode 100644 Terminal.Gui/View/Layout/LayoutEventArgs.cs create mode 100644 Terminal.Gui/View/Navigation/FocusEventArgs.cs rename Terminal.Gui/View/{ViewAdornments.cs => View.Adornments.cs} (99%) create mode 100644 Terminal.Gui/View/View.Arrangement.cs rename Terminal.Gui/View/{ViewContent.cs => View.Content.cs} (100%) create mode 100644 Terminal.Gui/View/View.Cursor.cs rename Terminal.Gui/View/{ViewDiagnostics.cs => View.Diagnostics.cs} (100%) rename Terminal.Gui/View/{ViewDrawing.cs => View.Drawing.cs} (99%) create mode 100644 Terminal.Gui/View/View.Hierarchy.cs rename Terminal.Gui/View/{ViewKeyboard.cs => View.Keyboard.cs} (91%) rename Terminal.Gui/View/{Layout/ViewLayout.cs => View.Layout.cs} (99%) rename Terminal.Gui/View/{ViewMouse.cs => View.Mouse.cs} (99%) create mode 100644 Terminal.Gui/View/View.Navigation.cs rename Terminal.Gui/View/{ViewText.cs => View.Text.cs} (99%) delete mode 100644 Terminal.Gui/View/ViewSubViews.cs diff --git a/Terminal.Gui/Application/Application.Keyboard.cs b/Terminal.Gui/Application/Application.Keyboard.cs index d26dcd432..969d4c31e 100644 --- a/Terminal.Gui/Application/Application.Keyboard.cs +++ b/Terminal.Gui/Application/Application.Keyboard.cs @@ -267,34 +267,6 @@ public static partial class Application // Keyboard handling CommandImplementations [command] = ctx => f (); } - ///// - ///// The key bindings. - ///// - //private static readonly Dictionary> _keyBindings = new (); - - ///// - ///// Gets the list of key bindings. - ///// - //public static Dictionary> GetKeyBindings () { return _keyBindings; } - - ///// - ///// Adds an scoped key binding. - ///// - ///// - ///// This is an internal method used by the class to add Application key bindings. - ///// - ///// The key being bound. - ///// The view that is bound to the key. If , will be used. - //internal static void AddKeyBinding (Key key, View? view) - //{ - // if (!_keyBindings.ContainsKey (key)) - // { - // _keyBindings [key] = []; - // } - - // _keyBindings [key].Add (view); - //} - internal static void AddApplicationKeyBindings () { // Things this view knows how to do @@ -326,7 +298,7 @@ public static partial class Application // Keyboard handling ); AddCommand ( - Command.NextView, + Command.NextView, () => { // TODO: Move this method to Application.Navigation.cs diff --git a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs index e99521d1e..dc5c785a0 100644 --- a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs @@ -603,48 +603,6 @@ public abstract class ConsoleDriver #endregion } -/// Terminal Cursor Visibility settings. -/// -/// Hex value are set as 0xAABBCCDD where : AA stand for the TERMINFO DECSUSR parameter value to be used under -/// Linux and MacOS BB stand for the NCurses curs_set parameter value to be used under Linux and MacOS CC stand for the -/// CONSOLE_CURSOR_INFO.bVisible parameter value to be used under Windows DD stand for the CONSOLE_CURSOR_INFO.dwSize -/// parameter value to be used under Windows -/// -public enum CursorVisibility -{ - /// Cursor caret has default - /// - /// Works under Xterm-like terminal otherwise this is equivalent to . This default directly - /// depends on the XTerm user configuration settings, so it could be Block, I-Beam, Underline with possible blinking. - /// - Default = 0x00010119, - - /// Cursor caret is hidden - Invisible = 0x03000019, - - /// Cursor caret is normally shown as a blinking underline bar _ - Underline = 0x03010119, - - /// Cursor caret is normally shown as a underline bar _ - /// Under Windows, this is equivalent to - UnderlineFix = 0x04010119, - - /// Cursor caret is displayed a blinking vertical bar | - /// Works under Xterm-like terminal otherwise this is equivalent to - Vertical = 0x05010119, - - /// Cursor caret is displayed a blinking vertical bar | - /// Works under Xterm-like terminal otherwise this is equivalent to - VerticalFix = 0x06010119, - - /// Cursor caret is displayed as a blinking block ▉ - Box = 0x01020164, - - /// Cursor caret is displayed a block ▉ - /// Works under Xterm-like terminal otherwise this is equivalent to - BoxFix = 0x02020164 -} - /// /// The enumeration encodes key information from s and provides a /// consistent way for application code to specify keys and receive key events. diff --git a/Terminal.Gui/ConsoleDrivers/CursorVisibility.cs b/Terminal.Gui/ConsoleDrivers/CursorVisibility.cs new file mode 100644 index 000000000..b96d31fd4 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/CursorVisibility.cs @@ -0,0 +1,44 @@ +#nullable enable +namespace Terminal.Gui; + +/// Terminal Cursor Visibility settings. +/// +/// Hex value are set as 0xAABBCCDD where : AA stand for the TERMINFO DECSUSR parameter value to be used under +/// Linux and MacOS BB stand for the NCurses curs_set parameter value to be used under Linux and MacOS CC stand for the +/// CONSOLE_CURSOR_INFO.bVisible parameter value to be used under Windows DD stand for the CONSOLE_CURSOR_INFO.dwSize +/// parameter value to be used under Windows +/// +public enum CursorVisibility +{ + /// Cursor caret has default + /// + /// Works under Xterm-like terminal otherwise this is equivalent to . This default directly + /// depends on the XTerm user configuration settings, so it could be Block, I-Beam, Underline with possible blinking. + /// + Default = 0x00010119, + + /// Cursor caret is hidden + Invisible = 0x03000019, + + /// Cursor caret is normally shown as a blinking underline bar _ + Underline = 0x03010119, + + /// Cursor caret is normally shown as a underline bar _ + /// Under Windows, this is equivalent to + UnderlineFix = 0x04010119, + + /// Cursor caret is displayed a blinking vertical bar | + /// Works under Xterm-like terminal otherwise this is equivalent to + Vertical = 0x05010119, + + /// Cursor caret is displayed a blinking vertical bar | + /// Works under Xterm-like terminal otherwise this is equivalent to + VerticalFix = 0x06010119, + + /// Cursor caret is displayed as a blinking block ▉ + Box = 0x01020164, + + /// Cursor caret is displayed a block ▉ + /// Works under Xterm-like terminal otherwise this is equivalent to + BoxFix = 0x02020164 +} diff --git a/Terminal.Gui/View/DrawEventArgs.cs b/Terminal.Gui/View/DrawEventArgs.cs new file mode 100644 index 000000000..32c07c711 --- /dev/null +++ b/Terminal.Gui/View/DrawEventArgs.cs @@ -0,0 +1,29 @@ +namespace Terminal.Gui; + +/// Event args for draw events +public class DrawEventArgs : EventArgs +{ + /// Creates a new instance of the class. + /// + /// The Content-relative rectangle describing the new visible viewport into the + /// . + /// + /// + /// The Content-relative rectangle describing the old visible viewport into the + /// . + /// + public DrawEventArgs (Rectangle newViewport, Rectangle oldViewport) + { + NewViewport = newViewport; + OldViewport = oldViewport; + } + + /// If set to true, the draw operation will be canceled, if applicable. + public bool Cancel { get; set; } + + /// Gets the Content-relative rectangle describing the old visible viewport into the . + public Rectangle OldViewport { get; } + + /// Gets the Content-relative rectangle describing the currently visible viewport into the . + public Rectangle NewViewport { get; } +} diff --git a/Terminal.Gui/View/Layout/LayoutEventArgs.cs b/Terminal.Gui/View/Layout/LayoutEventArgs.cs new file mode 100644 index 000000000..dac959af0 --- /dev/null +++ b/Terminal.Gui/View/Layout/LayoutEventArgs.cs @@ -0,0 +1,12 @@ +namespace Terminal.Gui; + +/// Event arguments for the event. +public class LayoutEventArgs : EventArgs +{ + /// Creates a new instance of the class. + /// The view that the event is about. + public LayoutEventArgs (Size oldContentSize) { OldContentSize = oldContentSize; } + + /// The viewport of the before it was laid out. + public Size OldContentSize { get; set; } +} diff --git a/Terminal.Gui/View/Navigation/FocusEventArgs.cs b/Terminal.Gui/View/Navigation/FocusEventArgs.cs new file mode 100644 index 000000000..6d8d28267 --- /dev/null +++ b/Terminal.Gui/View/Navigation/FocusEventArgs.cs @@ -0,0 +1,27 @@ +namespace Terminal.Gui; + +/// Defines the event arguments for +public class FocusEventArgs : EventArgs +{ + /// Constructs. + /// The view that is losing focus. + /// The view that is gaining focus. + public FocusEventArgs (View leaving, View entering) { + Leaving = leaving; + Entering = entering; + } + + /// + /// Indicates if the current focus event has already been processed and the driver should stop notifying any other + /// event subscriber. It's important to set this value to true specially when updating any View's layout from inside the + /// subscriber method. + /// + public bool Handled { get; set; } + + /// Indicates the view that is losing focus. + public View Leaving { get; set; } + + /// Indicates the view that is gaining focus. + public View Entering { get; set; } + +} diff --git a/Terminal.Gui/View/ViewAdornments.cs b/Terminal.Gui/View/View.Adornments.cs similarity index 99% rename from Terminal.Gui/View/ViewAdornments.cs rename to Terminal.Gui/View/View.Adornments.cs index accb15aba..2d179079e 100644 --- a/Terminal.Gui/View/ViewAdornments.cs +++ b/Terminal.Gui/View/View.Adornments.cs @@ -3,7 +3,7 @@ using System.Text.Json.Serialization; namespace Terminal.Gui; -public partial class View +public partial class View // Adornments { /// /// Initializes the Adornments of the View. Called by the constructor. diff --git a/Terminal.Gui/View/View.Arrangement.cs b/Terminal.Gui/View/View.Arrangement.cs new file mode 100644 index 000000000..0fea93324 --- /dev/null +++ b/Terminal.Gui/View/View.Arrangement.cs @@ -0,0 +1,15 @@ +namespace Terminal.Gui; + +public partial class View +{ + /// + /// Gets or sets the user actions that are enabled for the view within it's . + /// + /// + /// + /// Sizing or moving a view is only possible if the is part of a and + /// the relevant position and dimensions of the are independent of other SubViews + /// + /// + public ViewArrangement Arrangement { get; set; } +} diff --git a/Terminal.Gui/View/ViewContent.cs b/Terminal.Gui/View/View.Content.cs similarity index 100% rename from Terminal.Gui/View/ViewContent.cs rename to Terminal.Gui/View/View.Content.cs diff --git a/Terminal.Gui/View/View.Cursor.cs b/Terminal.Gui/View/View.Cursor.cs new file mode 100644 index 000000000..bdba7d85f --- /dev/null +++ b/Terminal.Gui/View/View.Cursor.cs @@ -0,0 +1,35 @@ +namespace Terminal.Gui; + +public partial class View +{ + /// + /// Gets or sets the cursor style to be used when the view is focused. The default is + /// . + /// + public CursorVisibility CursorVisibility { get; set; } = CursorVisibility.Invisible; + + /// + /// Positions the cursor in the right position based on the currently focused view in the chain. + /// + /// + /// + /// Views that are focusable should override to make sure that the cursor is + /// placed in a location that makes sense. Some terminals do not have a way of hiding the cursor, so it can be + /// distracting to have the cursor left at the last focused view. So views should make sure that they place the + /// cursor in a visually sensible place. The default implementation of will place the + /// cursor at either the hotkey (if defined) or 0,0. + /// + /// + /// Viewport-relative cursor position. Return to ensure the cursor is not visible. + public virtual Point? PositionCursor () + { + if (IsInitialized && CanFocus && HasFocus) + { + // By default, position the cursor at the hotkey (if any) or 0, 0. + Move (TextFormatter.HotKeyPos == -1 ? 0 : TextFormatter.CursorPosition, 0); + } + + // Returning null will hide the cursor. + return null; + } +} diff --git a/Terminal.Gui/View/ViewDiagnostics.cs b/Terminal.Gui/View/View.Diagnostics.cs similarity index 100% rename from Terminal.Gui/View/ViewDiagnostics.cs rename to Terminal.Gui/View/View.Diagnostics.cs diff --git a/Terminal.Gui/View/ViewDrawing.cs b/Terminal.Gui/View/View.Drawing.cs similarity index 99% rename from Terminal.Gui/View/ViewDrawing.cs rename to Terminal.Gui/View/View.Drawing.cs index 73fa5d550..1077f3917 100644 --- a/Terminal.Gui/View/ViewDrawing.cs +++ b/Terminal.Gui/View/View.Drawing.cs @@ -2,7 +2,7 @@ namespace Terminal.Gui; -public partial class View +public partial class View // Drawing APIs { private ColorScheme _colorScheme; diff --git a/Terminal.Gui/View/View.Hierarchy.cs b/Terminal.Gui/View/View.Hierarchy.cs new file mode 100644 index 000000000..125baf33f --- /dev/null +++ b/Terminal.Gui/View/View.Hierarchy.cs @@ -0,0 +1,320 @@ +namespace Terminal.Gui; + +public partial class View // SuperView/SubView hierarchy management (SuperView, SubViews, Add, Remove, etc.) +{ + private static readonly IList _empty = new List (0).AsReadOnly (); + internal bool _addingView; + private List _subviews; // This is null, and allocated on demand. + private View _superView; + + /// Indicates whether the view was added to . + public bool IsAdded { get; private set; } + + /// This returns a list of the subviews contained by this view. + /// The subviews. + public IList Subviews => _subviews?.AsReadOnly () ?? _empty; + + /// Returns the container for this view, or null if this view has not been added to a container. + /// The super view. + public virtual View SuperView + { + get => _superView; + set => throw new NotImplementedException (); + } + + // Internally, we use InternalSubviews rather than subviews, as we do not expect us + // to make the same mistakes our users make when they poke at the Subviews. + internal IList InternalSubviews => _subviews ?? _empty; + + /// Adds a subview (child) to this view. + /// + /// + /// The Views that have been added to this view can be retrieved via the property. See also + /// + /// + /// + /// Subviews will be disposed when this View is disposed. In other-words, calling this method causes + /// the lifecycle of the subviews to be transferred to this View. + /// + /// + /// The view to add. + /// The view that was added. + public virtual View Add (View view) + { + if (view is null) + { + return view; + } + + if (_subviews is null) + { + _subviews = new (); + } + + if (_tabIndexes is null) + { + _tabIndexes = new (); + } + + _subviews.Add (view); + _tabIndexes.Add (view); + view._superView = this; + + if (view.CanFocus) + { + _addingView = true; + + if (SuperView?.CanFocus == false) + { + SuperView._addingView = true; + SuperView.CanFocus = true; + SuperView._addingView = false; + } + + // QUESTION: This automatic behavior of setting CanFocus to true on the SuperView is not documented, and is annoying. + CanFocus = true; + view._tabIndex = _tabIndexes.IndexOf (view); + _addingView = false; + } + + if (view.Enabled && !Enabled) + { + view._oldEnabled = true; + view.Enabled = false; + } + + OnAdded (new (this, view)); + + if (IsInitialized && !view.IsInitialized) + { + view.BeginInit (); + view.EndInit (); + } + + CheckDimAuto (); + SetNeedsLayout (); + SetNeedsDisplay (); + + return view; + } + + /// Adds the specified views (children) to the view. + /// Array of one or more views (can be optional parameter). + /// + /// + /// The Views that have been added to this view can be retrieved via the property. See also + /// and . + /// + /// + /// Subviews will be disposed when this View is disposed. In other-words, calling this method causes + /// the lifecycle of the subviews to be transferred to this View. + /// + /// + public void Add (params View [] views) + { + if (views is null) + { + return; + } + + foreach (View view in views) + { + Add (view); + } + } + + /// Event fired when this view is added to another. + public event EventHandler Added; + + /// Get the top superview of a given . + /// The superview view. + public View GetTopSuperView (View view = null, View superview = null) + { + View top = superview ?? Application.Top; + + for (View v = view?.SuperView ?? this?.SuperView; v != null; v = v.SuperView) + { + top = v; + + if (top == superview) + { + break; + } + } + + return top; + } + + /// Method invoked when a subview is being added to this view. + /// Event where is the subview being added. + public virtual void OnAdded (SuperViewChangedEventArgs e) + { + View view = e.Child; + view.IsAdded = true; + view.OnResizeNeeded (); + view.Added?.Invoke (this, e); + } + + /// Method invoked when a subview is being removed from this view. + /// Event args describing the subview being removed. + public virtual void OnRemoved (SuperViewChangedEventArgs e) + { + View view = e.Child; + view.IsAdded = false; + view.Removed?.Invoke (this, e); + } + + /// Removes a subview added via or from this View. + /// + /// + /// Normally Subviews will be disposed when this View is disposed. Removing a Subview causes ownership of the + /// Subview's + /// lifecycle to be transferred to the caller; the caller muse call . + /// + /// + public virtual View Remove (View view) + { + if (view is null || _subviews is null) + { + return view; + } + + Rectangle touched = view.Frame; + _subviews.Remove (view); + _tabIndexes.Remove (view); + view._superView = null; + view._tabIndex = -1; + SetNeedsLayout (); + SetNeedsDisplay (); + + foreach (View v in _subviews) + { + if (v.Frame.IntersectsWith (touched)) + { + view.SetNeedsDisplay (); + } + } + + OnRemoved (new (this, view)); + + if (Focused == view) + { + Focused = null; + } + + return view; + } + + /// + /// Removes all subviews (children) added via or from this View. + /// + /// + /// + /// Normally Subviews will be disposed when this View is disposed. Removing a Subview causes ownership of the + /// Subview's + /// lifecycle to be transferred to the caller; the caller must call on any Views that were + /// added. + /// + /// + public virtual void RemoveAll () + { + if (_subviews is null) + { + return; + } + + while (_subviews.Count > 0) + { + Remove (_subviews [0]); + } + } + + /// Event fired when this view is removed from another. + public event EventHandler Removed; + + + /// Moves one position towards the start of the list + /// The subview to move forward. + public void BringSubviewForward (View subview) + { + PerformActionForSubview ( + subview, + x => + { + int idx = _subviews.IndexOf (x); + + if (idx + 1 < _subviews.Count) + { + _subviews.Remove (x); + _subviews.Insert (idx + 1, x); + } + } + ); + } + + /// Moves to the start of the list. + /// The subview to send to the start. + public void BringSubviewToFront (View subview) + { + PerformActionForSubview ( + subview, + x => + { + _subviews.Remove (x); + _subviews.Add (x); + } + ); + } + + + /// Moves one position towards the end of the list + /// The subview to move backwards. + public void SendSubviewBackwards (View subview) + { + PerformActionForSubview ( + subview, + x => + { + int idx = _subviews.IndexOf (x); + + if (idx > 0) + { + _subviews.Remove (x); + _subviews.Insert (idx - 1, x); + } + } + ); + } + + /// Moves to the end of the list. + /// The subview to send to the end. + public void SendSubviewToBack (View subview) + { + PerformActionForSubview ( + subview, + x => + { + _subviews.Remove (x); + _subviews.Insert (0, subview); + } + ); + } + + /// + /// Internal API that runs on a subview if it is part of the list. + /// + /// + /// + private void PerformActionForSubview (View subview, Action action) + { + if (_subviews.Contains (subview)) + { + action (subview); + } + + // BUGBUG: this is odd. Why is this needed? + SetNeedsDisplay (); + subview.SetNeedsDisplay (); + } + +} diff --git a/Terminal.Gui/View/ViewKeyboard.cs b/Terminal.Gui/View/View.Keyboard.cs similarity index 91% rename from Terminal.Gui/View/ViewKeyboard.cs rename to Terminal.Gui/View/View.Keyboard.cs index 7a905f129..7009ab4c6 100644 --- a/Terminal.Gui/View/ViewKeyboard.cs +++ b/Terminal.Gui/View/View.Keyboard.cs @@ -3,7 +3,7 @@ using System.Diagnostics; namespace Terminal.Gui; -public partial class View +public partial class View // Keyboard APIs { /// /// Helper to configure all things keyboard related for a View. Called from the View constructor. @@ -254,119 +254,6 @@ public partial class View #endregion HotKey Support - #region Tab/Focus Handling - - // This is null, and allocated on demand. - private List _tabIndexes; - - /// Gets a list of the subviews that are s. - /// The tabIndexes. - public IList TabIndexes => _tabIndexes?.AsReadOnly () ?? _empty; - - private int _tabIndex = -1; - private int _oldTabIndex; - - /// - /// Indicates the index of the current from the list. See also: - /// . - /// - public int TabIndex - { - get => _tabIndex; - set - { - if (!CanFocus) - { - _tabIndex = -1; - - return; - } - - if (SuperView?._tabIndexes is null || SuperView?._tabIndexes.Count == 1) - { - _tabIndex = 0; - - return; - } - - if (_tabIndex == value && TabIndexes.IndexOf (this) == value) - { - return; - } - - _tabIndex = value > SuperView._tabIndexes.Count - 1 ? SuperView._tabIndexes.Count - 1 : - value < 0 ? 0 : value; - _tabIndex = GetTabIndex (_tabIndex); - - if (SuperView._tabIndexes.IndexOf (this) != _tabIndex) - { - SuperView._tabIndexes.Remove (this); - SuperView._tabIndexes.Insert (_tabIndex, this); - SetTabIndex (); - } - } - } - - private int GetTabIndex (int idx) - { - var i = 0; - - foreach (View v in SuperView._tabIndexes) - { - if (v._tabIndex == -1 || v == this) - { - continue; - } - - i++; - } - - return Math.Min (i, idx); - } - - private void SetTabIndex () - { - var i = 0; - - foreach (View v in SuperView._tabIndexes) - { - if (v._tabIndex == -1) - { - continue; - } - - v._tabIndex = i; - i++; - } - } - - private bool _tabStop = true; - - /// - /// Gets or sets whether the view is a stop-point for keyboard navigation of focus. Will be - /// only if the is also . Set to to prevent the - /// view from being a stop-point for keyboard navigation. - /// - /// - /// The default keyboard navigation keys are Key.Tab and Key>Tab.WithShift. These can be changed by - /// modifying the key bindings (see ) of the SuperView. - /// - public bool TabStop - { - get => _tabStop; - set - { - if (_tabStop == value) - { - return; - } - - _tabStop = CanFocus && value; - } - } - - #endregion Tab/Focus Handling - #region Low-level Key handling #region Key Down Event diff --git a/Terminal.Gui/View/Layout/ViewLayout.cs b/Terminal.Gui/View/View.Layout.cs similarity index 99% rename from Terminal.Gui/View/Layout/ViewLayout.cs rename to Terminal.Gui/View/View.Layout.cs index 72a9bf14c..deb7da682 100644 --- a/Terminal.Gui/View/Layout/ViewLayout.cs +++ b/Terminal.Gui/View/View.Layout.cs @@ -3,7 +3,7 @@ using System.Diagnostics; namespace Terminal.Gui; -public partial class View +public partial class View // Layout APIs { #region Frame diff --git a/Terminal.Gui/View/ViewMouse.cs b/Terminal.Gui/View/View.Mouse.cs similarity index 99% rename from Terminal.Gui/View/ViewMouse.cs rename to Terminal.Gui/View/View.Mouse.cs index 24314f583..5f1318e21 100644 --- a/Terminal.Gui/View/ViewMouse.cs +++ b/Terminal.Gui/View/View.Mouse.cs @@ -2,7 +2,7 @@ namespace Terminal.Gui; -public partial class View +public partial class View // Mouse APIs { [CanBeNull] private ColorScheme _savedHighlightColorScheme; diff --git a/Terminal.Gui/View/View.Navigation.cs b/Terminal.Gui/View/View.Navigation.cs new file mode 100644 index 000000000..4ff99c3d7 --- /dev/null +++ b/Terminal.Gui/View/View.Navigation.cs @@ -0,0 +1,813 @@ +namespace Terminal.Gui; + +public partial class View // Focus and cross-view navigation management (TabStop, TabIndex, etc...) +{ + /// Returns a value indicating if this View is currently on Top (Active) + public bool IsCurrentTop => Application.Current == this; + + // BUGBUG: This API is poorly defined and implemented. It deeply intertwines the view hierarchy with the tab order. + /// Exposed as `internal` for unit tests. Indicates focus navigation direction. + internal enum NavigationDirection + { + /// Navigate forward. + Forward, + + /// Navigate backwards. + Backward + } + + /// Invoked when this view is gaining focus (entering). + /// The view that is leaving focus. + /// , if the event was handled, otherwise. + /// + /// + /// Overrides must call the base class method to ensure that the event is raised. If the event + /// is handled, the method should return . + /// + /// + public virtual bool OnEnter (View leavingView) + { + var args = new FocusEventArgs (leavingView, this); + Enter?.Invoke (this, args); + + if (args.Handled) + { + return true; + } + + return false; + } + + /// Invoked when this view is losing focus (leaving). + /// The view that is entering focus. + /// , if the event was handled, otherwise. + /// + /// + /// Overrides must call the base class method to ensure that the event is raised. If the event + /// is handled, the method should return . + /// + /// + public virtual bool OnLeave (View enteringView) + { + var args = new FocusEventArgs (this, enteringView); + Leave?.Invoke (this, args); + + if (args.Handled) + { + return true; + } + + return false; + } + + /// Raised when the view is gaining (entering) focus. Can be cancelled. + /// + /// Raised by the virtual method. + /// + public event EventHandler Enter; + + /// Raised when the view is losing (leaving) focus. Can be cancelled. + /// + /// Raised by the virtual method. + /// + public event EventHandler Leave; + + private NavigationDirection _focusDirection; + + /// + /// Gets or sets the focus direction for this view and all subviews. + /// Setting this property will set the focus direction for all views up the SuperView hierarchy. + /// + internal NavigationDirection FocusDirection + { + get => SuperView?.FocusDirection ?? _focusDirection; + set + { + if (SuperView is { }) + { + SuperView.FocusDirection = value; + } + else + { + _focusDirection = value; + } + } + } + + private bool _hasFocus; + + /// + /// Gets or sets whether this view has focus. + /// + /// + /// + /// Causes the and virtual methods (and and + /// events to be raised) when the value changes. + /// + /// + /// Setting this property to will recursively set to + /// + /// for any focused subviews. + /// + /// + public bool HasFocus + { + // Force the specified view to have focus + set => SetHasFocus (value, this, true); + get => _hasFocus; + } + + /// + /// Internal API that sets . This method is called by HasFocus_set and other methods that + /// need to set or remove focus from a view. + /// + /// The new setting for . + /// The view that will be gaining or losing focus. + /// + /// to force Enter/Leave on regardless of whether it + /// already HasFocus or not. + /// + /// + /// If is and there is a focused subview ( + /// is not ), + /// this method will recursively remove focus from any focused subviews of . + /// + private void SetHasFocus (bool newHasFocus, View view, bool force = false) + { + if (HasFocus != newHasFocus || force) + { + _hasFocus = newHasFocus; + + if (newHasFocus) + { + OnEnter (view); + } + else + { + OnLeave (view); + } + + SetNeedsDisplay (); + } + + // Remove focus down the chain of subviews if focus is removed + if (!newHasFocus && Focused is { }) + { + View f = Focused; + f.OnLeave (view); + f.SetHasFocus (false, view); + Focused = null; + } + } + + /// Raised when has been changed. + /// + /// Raised by the virtual method. + /// + public event EventHandler CanFocusChanged; + + /// Invoked when the property from a view is changed. + /// + /// Raises the event. + /// + public virtual void OnCanFocusChanged () { CanFocusChanged?.Invoke (this, EventArgs.Empty); } + + private bool _oldCanFocus; + private bool _canFocus; + + /// Gets or sets a value indicating whether this can be focused. + /// + /// + /// must also have set to . + /// + /// + public bool CanFocus + { + get => _canFocus; + set + { + if (!_addingView && IsInitialized && SuperView?.CanFocus == false && value) + { + throw new InvalidOperationException ("Cannot set CanFocus to true if the SuperView CanFocus is false!"); + } + + if (_canFocus == value) + { + return; + } + + _canFocus = value; + + switch (_canFocus) + { + case false when _tabIndex > -1: + TabIndex = -1; + + break; + case true when SuperView?.CanFocus == false && _addingView: + SuperView.CanFocus = true; + + break; + } + + if (_canFocus && _tabIndex == -1) + { + TabIndex = SuperView is { } ? SuperView._tabIndexes.IndexOf (this) : -1; + } + + TabStop = _canFocus; + + if (!_canFocus && SuperView?.Focused == this) + { + SuperView.Focused = null; + } + + if (!_canFocus && HasFocus) + { + SetHasFocus (false, this); + SuperView?.EnsureFocus (); + + if (SuperView is { Focused: null }) + { + SuperView.FocusNext (); + + if (SuperView.Focused is null && Application.Current is { }) + { + Application.Current.FocusNext (); + } + + ApplicationOverlapped.BringOverlappedTopToFront (); + } + } + + if (_subviews is { } && IsInitialized) + { + foreach (View view in _subviews) + { + if (view.CanFocus != value) + { + if (!value) + { + view._oldCanFocus = view.CanFocus; + view._oldTabIndex = view._tabIndex; + view.CanFocus = false; + view._tabIndex = -1; + } + else + { + if (_addingView) + { + view._addingView = true; + } + + view.CanFocus = view._oldCanFocus; + view._tabIndex = view._oldTabIndex; + view._addingView = false; + } + } + } + + if (this is Toplevel && Application.Current.Focused != this) + { + ApplicationOverlapped.BringOverlappedTopToFront (); + } + } + + OnCanFocusChanged (); + SetNeedsDisplay (); + } + } + + /// Returns the currently focused Subview inside this view, or if nothing is focused. + /// The currently focused Subview. + public View Focused { get; private set; } + + /// + /// Returns the most focused Subview in the chain of subviews (the leaf view that has the focus), or + /// if nothing is focused. + /// + /// The most focused Subview. + public View MostFocused + { + get + { + if (Focused is null) + { + return null; + } + + View most = Focused.MostFocused; + + if (most is { }) + { + return most; + } + + return Focused; + } + } + + /// Causes subview specified by to enter focus. + /// View. + private void SetFocus (View view) + { + if (view is null) + { + return; + } + + //Console.WriteLine ($"Request to focus {view}"); + if (!view.CanFocus || !view.Visible || !view.Enabled) + { + return; + } + + if (Focused?._hasFocus == true && Focused == view) + { + return; + } + + if ((Focused?._hasFocus == true && Focused?.SuperView == view) || view == this) + { + if (!view._hasFocus) + { + view._hasFocus = true; + } + + return; + } + + // Make sure that this view is a subview + View c; + + for (c = view._superView; c != null; c = c._superView) + { + if (c == this) + { + break; + } + } + + if (c is null) + { + throw new ArgumentException ("the specified view is not part of the hierarchy of this view"); + } + + if (Focused is { }) + { + Focused.SetHasFocus (false, view); + } + + View f = Focused; + Focused = view; + Focused.SetHasFocus (true, f); + Focused.EnsureFocus (); + + // Send focus upwards + if (SuperView is { }) + { + SuperView.SetFocus (this); + } + else + { + SetFocus (this); + } + } + + /// Causes this view to be focused and entire Superview hierarchy to have the focused order updated. + public void SetFocus () + { + if (!CanBeVisible (this) || !Enabled) + { + if (HasFocus) + { + SetHasFocus (false, this); + } + + return; + } + + if (SuperView is { }) + { + SuperView.SetFocus (this); + } + else + { + SetFocus (this); + } + } + + /// + /// If there is no focused subview, calls or based on + /// . + /// does nothing. + /// + public void EnsureFocus () + { + if (Focused is null && _subviews?.Count > 0) + { + if (FocusDirection == NavigationDirection.Forward) + { + FocusFirst (); + } + else + { + FocusLast (); + } + } + } + + /// + /// Focuses the last focusable view in if one exists. If there are no views in + /// then the focus is set to the view itself. + /// + public void FocusFirst (bool overlapped = false) + { + if (!CanBeVisible (this)) + { + return; + } + + if (_tabIndexes is null) + { + SuperView?.SetFocus (this); + + return; + } + + foreach (View view in _tabIndexes.Where (v => !overlapped || v.Arrangement.HasFlag (ViewArrangement.Overlapped))) + { + if (view.CanFocus && view._tabStop && view.Visible && view.Enabled) + { + SetFocus (view); + + return; + } + } + } + + /// + /// Focuses the last focusable view in if one exists. If there are no views in + /// then the focus is set to the view itself. + /// + public void FocusLast (bool overlapped = false) + { + if (!CanBeVisible (this)) + { + return; + } + + if (_tabIndexes is null) + { + SuperView?.SetFocus (this); + + return; + } + + foreach (View view in _tabIndexes.Where (v => !overlapped || v.Arrangement.HasFlag (ViewArrangement.Overlapped)).Reverse ()) + { + if (view.CanFocus && view._tabStop && view.Visible && view.Enabled) + { + SetFocus (view); + + return; + } + } + } + + /// + /// Focuses the previous view in . If there is no previous view, the focus is set to the + /// view itself. + /// + /// if previous was focused, otherwise. + public bool FocusPrev () + { + if (!CanBeVisible (this)) + { + return false; + } + + FocusDirection = NavigationDirection.Backward; + + if (TabIndexes is null || TabIndexes.Count == 0) + { + return false; + } + + if (Focused is null) + { + FocusLast (); + + return Focused != null; + } + + int focusedIdx = -1; + + for (int i = TabIndexes.Count; i > 0;) + { + i--; + View w = TabIndexes [i]; + + if (w.HasFocus) + { + if (w.FocusPrev ()) + { + return true; + } + + focusedIdx = i; + + continue; + } + + if (w.CanFocus && focusedIdx != -1 && w._tabStop && w.Visible && w.Enabled) + { + Focused.SetHasFocus (false, w); + + // If the focused view is overlapped don't focus on the next if it's not overlapped. + if (Focused.Arrangement.HasFlag (ViewArrangement.Overlapped) && !w.Arrangement.HasFlag (ViewArrangement.Overlapped)) + { + FocusLast (true); + + return true; + } + + // If the focused view is not overlapped and the next is, skip it + if (!Focused.Arrangement.HasFlag (ViewArrangement.Overlapped) && w.Arrangement.HasFlag (ViewArrangement.Overlapped)) + { + continue; + } + + if (w.CanFocus && w._tabStop && w.Visible && w.Enabled) + { + w.FocusLast (); + } + + SetFocus (w); + + return true; + } + } + + // There's no prev view in tab indexes. + if (Focused is { }) + { + // Leave Focused + Focused.SetHasFocus (false, this); + + if (Focused.Arrangement.HasFlag (ViewArrangement.Overlapped)) + { + FocusLast (true); + + return true; + } + + // Signal to caller no next view was found + Focused = null; + } + + return false; + } + + /// + /// Focuses the next view in . If there is no next view, the focus is set to the view + /// itself. + /// + /// if next was focused, otherwise. + public bool FocusNext () + { + if (!CanBeVisible (this)) + { + return false; + } + + FocusDirection = NavigationDirection.Forward; + + if (TabIndexes is null || TabIndexes.Count == 0) + { + return false; + } + + if (Focused is null) + { + FocusFirst (); + + return Focused != null; + } + + int focusedIdx = -1; + + for (var i = 0; i < TabIndexes.Count; i++) + { + View w = TabIndexes [i]; + + if (w.HasFocus) + { + if (w.FocusNext ()) + { + return true; + } + + focusedIdx = i; + + continue; + } + + if (w.CanFocus && focusedIdx != -1 && w._tabStop && w.Visible && w.Enabled) + { + Focused.SetHasFocus (false, w); + + //// If the focused view is overlapped don't focus on the next if it's not overlapped. + //if (Focused.Arrangement.HasFlag (ViewArrangement.Overlapped)/* && !w.Arrangement.HasFlag (ViewArrangement.Overlapped)*/) + //{ + // return false; + //} + + //// If the focused view is not overlapped and the next is, skip it + //if (!Focused.Arrangement.HasFlag (ViewArrangement.Overlapped) && w.Arrangement.HasFlag (ViewArrangement.Overlapped)) + //{ + // continue; + //} + + if (w.CanFocus && w._tabStop && w.Visible && w.Enabled) + { + w.FocusFirst (); + } + + SetFocus (w); + + return true; + } + } + + // There's no next view in tab indexes. + if (Focused is { }) + { + // Leave Focused + Focused.SetHasFocus (false, this); + + //if (Focused.Arrangement.HasFlag (ViewArrangement.Overlapped)) + //{ + // FocusFirst (true); + // return true; + //} + + // Signal to caller no next view was found + Focused = null; + } + + return false; + } + + private View GetMostFocused (View view) + { + if (view is null) + { + return null; + } + + return view.Focused is { } ? GetMostFocused (view.Focused) : view; + } + + #region Tab/Focus Handling + + private List _tabIndexes; + + // TODO: This should be a get-only property? + // BUGBUG: This returns an AsReadOnly list, but isn't declared as such. + /// Gets a list of the subviews that are a . + /// The tabIndexes. + public IList TabIndexes => _tabIndexes?.AsReadOnly () ?? _empty; + + // TODO: Change this to int? and use null to indicate the view is not in the tab order. + private int _tabIndex = -1; + private int _oldTabIndex; + + /// + /// Indicates the index of the current from the list. See also: + /// . + /// + /// + /// + /// If the value is -1, the view is not part of the tab order. + /// + /// + /// On set, if is , will be set to -1. + /// + /// + /// On set, if is or has not TabStops, will + /// be set to 0. + /// + /// + /// On set, if has only one TabStop, will be set to 0. + /// + /// + public int TabIndex + { + get => _tabIndex; + set + { + if (!CanFocus) + { + // BUGBUG: Property setters should set the property to the value passed in and not have side effects. + _tabIndex = -1; + + return; + } + + if (SuperView?._tabIndexes is null || SuperView?._tabIndexes.Count == 1) + { + // BUGBUG: Property setters should set the property to the value passed in and not have side effects. + _tabIndex = 0; + + return; + } + + if (_tabIndex == value && TabIndexes.IndexOf (this) == value) + { + return; + } + + _tabIndex = value > SuperView!.TabIndexes.Count - 1 ? SuperView._tabIndexes.Count - 1 : + value < 0 ? 0 : value; + _tabIndex = GetGreatestTabIndexInSuperView (_tabIndex); + + if (SuperView._tabIndexes.IndexOf (this) != _tabIndex) + { + // BUGBUG: we have to use _tabIndexes and not TabIndexes because TabIndexes returns is a read-only version of _tabIndexes + SuperView._tabIndexes.Remove (this); + SuperView._tabIndexes.Insert (_tabIndex, this); + ReorderSuperViewTabIndexes (); + } + } + } + + /// + /// Gets the greatest of the 's that is less + /// than or equal to . + /// + /// + /// The minimum of and the 's . + private int GetGreatestTabIndexInSuperView (int idx) + { + var i = 0; + + foreach (View superViewTabStop in SuperView._tabIndexes) + { + if (superViewTabStop._tabIndex == -1 || superViewTabStop == this) + { + continue; + } + + i++; + } + + return Math.Min (i, idx); + } + + /// + /// Re-orders the s of the views in the 's . + /// + private void ReorderSuperViewTabIndexes () + { + var i = 0; + + foreach (View superViewTabStop in SuperView._tabIndexes) + { + if (superViewTabStop._tabIndex == -1) + { + continue; + } + + superViewTabStop._tabIndex = i; + i++; + } + } + + private bool _tabStop = true; + + /// + /// Gets or sets whether the view is a stop-point for keyboard navigation of focus. Will be + /// only if is . Set to to prevent the + /// view from being a stop-point for keyboard navigation. + /// + /// + /// The default keyboard navigation keys are Key.Tab and Key>Tab.WithShift. These can be changed by + /// modifying the key bindings (see ) of the SuperView. + /// + public bool TabStop + { + get => _tabStop; + set + { + if (_tabStop == value) + { + return; + } + + _tabStop = CanFocus && value; + } + } + + #endregion Tab/Focus Handling +} diff --git a/Terminal.Gui/View/ViewText.cs b/Terminal.Gui/View/View.Text.cs similarity index 99% rename from Terminal.Gui/View/ViewText.cs rename to Terminal.Gui/View/View.Text.cs index 6b9e0cf94..664640730 100644 --- a/Terminal.Gui/View/ViewText.cs +++ b/Terminal.Gui/View/View.Text.cs @@ -2,7 +2,7 @@ namespace Terminal.Gui; -public partial class View +public partial class View // Text Property APIs { /// /// Initializes the Text of the View. Called by the constructor. diff --git a/Terminal.Gui/View/ViewArrangement.cs b/Terminal.Gui/View/ViewArrangement.cs index df13c4762..0143b082e 100644 --- a/Terminal.Gui/View/ViewArrangement.cs +++ b/Terminal.Gui/View/ViewArrangement.cs @@ -1,14 +1,16 @@ namespace Terminal.Gui; /// -/// Describes what user actions are enabled for arranging a within it's . +/// Describes what user actions are enabled for arranging a within it's +/// . /// See . /// /// -/// -/// Sizing or moving a view is only possible if the is part of a and -/// the relevant position and dimensions of the are independent of other SubViews -/// +/// +/// Sizing or moving a view is only possible if the is part of a +/// and +/// the relevant position and dimensions of the are independent of other SubViews +/// /// [Flags] public enum ViewArrangement @@ -56,26 +58,14 @@ public enum ViewArrangement Resizable = LeftResizable | RightResizable | TopResizable | BottomResizable, /// - /// The view overlap other views. + /// The view overlap other views. /// /// /// - /// When set, Tab and Shift-Tab will be constrained to the subviews of the view (normally, they will navigate to the next/prev view in the next/prev Tabindex). + /// When set, Tab and Shift-Tab will be constrained to the subviews of the view (normally, they will navigate to + /// the next/prev view in the next/prev Tabindex). /// Use Ctrl-Tab (Ctrl-PageDown) / Ctrl-Shift-Tab (Ctrl-PageUp) to move between overlapped views. /// /// Overlapped = 32 } -public partial class View -{ - /// - /// Gets or sets the user actions that are enabled for the view within it's . - /// - /// - /// - /// Sizing or moving a view is only possible if the is part of a and - /// the relevant position and dimensions of the are independent of other SubViews - /// - /// - public ViewArrangement Arrangement { get; set; } -} diff --git a/Terminal.Gui/View/ViewEventArgs.cs b/Terminal.Gui/View/ViewEventArgs.cs index b17b98afe..cdcbaa009 100644 --- a/Terminal.Gui/View/ViewEventArgs.cs +++ b/Terminal.Gui/View/ViewEventArgs.cs @@ -13,69 +13,4 @@ public class ViewEventArgs : EventArgs /// child then sender may be the parent while is the child being added. /// public View View { get; } -} - -/// Event arguments for the event. -public class LayoutEventArgs : EventArgs -{ - /// Creates a new instance of the class. - /// The view that the event is about. - public LayoutEventArgs (Size oldContentSize) { OldContentSize = oldContentSize; } - - /// The viewport of the before it was laid out. - public Size OldContentSize { get; set; } -} - -/// Event args for draw events -public class DrawEventArgs : EventArgs -{ - /// Creates a new instance of the class. - /// - /// The Content-relative rectangle describing the new visible viewport into the - /// . - /// - /// - /// The Content-relative rectangle describing the old visible viewport into the - /// . - /// - public DrawEventArgs (Rectangle newViewport, Rectangle oldViewport) - { - NewViewport = newViewport; - OldViewport = oldViewport; - } - - /// If set to true, the draw operation will be canceled, if applicable. - public bool Cancel { get; set; } - - /// Gets the Content-relative rectangle describing the old visible viewport into the . - public Rectangle OldViewport { get; } - - /// Gets the Content-relative rectangle describing the currently visible viewport into the . - public Rectangle NewViewport { get; } -} - -/// Defines the event arguments for -public class FocusEventArgs : EventArgs -{ - /// Constructs. - /// The view that is losing focus. - /// The view that is gaining focus. - public FocusEventArgs (View leaving, View entering) { - Leaving = leaving; - Entering = entering; - } - - /// - /// Indicates if the current focus event has already been processed and the driver should stop notifying any other - /// event subscriber. It's important to set this value to true specially when updating any View's layout from inside the - /// subscriber method. - /// - public bool Handled { get; set; } - - /// Indicates the view that is losing focus. - public View Leaving { get; set; } - - /// Indicates the view that is gaining focus. - public View Entering { get; set; } - -} +} \ No newline at end of file diff --git a/Terminal.Gui/View/ViewSubViews.cs b/Terminal.Gui/View/ViewSubViews.cs deleted file mode 100644 index 79cca431e..000000000 --- a/Terminal.Gui/View/ViewSubViews.cs +++ /dev/null @@ -1,948 +0,0 @@ -using System.Diagnostics; - -namespace Terminal.Gui; - -public partial class View -{ - private static readonly IList _empty = new List (0).AsReadOnly (); - internal bool _addingView; - private List _subviews; // This is null, and allocated on demand. - private View _superView; - - /// Indicates whether the view was added to . - public bool IsAdded { get; private set; } - - /// Returns a value indicating if this View is currently on Top (Active) - public bool IsCurrentTop => Application.Current == this; - - /// This returns a list of the subviews contained by this view. - /// The subviews. - public IList Subviews => _subviews?.AsReadOnly () ?? _empty; - - /// Returns the container for this view, or null if this view has not been added to a container. - /// The super view. - public virtual View SuperView - { - get => _superView; - set => throw new NotImplementedException (); - } - - // Internally, we use InternalSubviews rather than subviews, as we do not expect us - // to make the same mistakes our users make when they poke at the Subviews. - internal IList InternalSubviews => _subviews ?? _empty; - - /// Adds a subview (child) to this view. - /// - /// - /// The Views that have been added to this view can be retrieved via the property. See also - /// - /// - /// - /// Subviews will be disposed when this View is disposed. In other-words, calling this method causes - /// the lifecycle of the subviews to be transferred to this View. - /// - /// - /// The view to add. - /// The view that was added. - public virtual View Add (View view) - { - if (view is null) - { - return view; - } - - if (_subviews is null) - { - _subviews = new (); - } - - if (_tabIndexes is null) - { - _tabIndexes = new (); - } - - _subviews.Add (view); - _tabIndexes.Add (view); - view._superView = this; - - if (view.CanFocus) - { - _addingView = true; - - if (SuperView?.CanFocus == false) - { - SuperView._addingView = true; - SuperView.CanFocus = true; - SuperView._addingView = false; - } - - // QUESTION: This automatic behavior of setting CanFocus to true on the SuperView is not documented, and is annoying. - CanFocus = true; - view._tabIndex = _tabIndexes.IndexOf (view); - _addingView = false; - } - - if (view.Enabled && !Enabled) - { - view._oldEnabled = true; - view.Enabled = false; - } - - OnAdded (new (this, view)); - - if (IsInitialized && !view.IsInitialized) - { - view.BeginInit (); - view.EndInit (); - } - - CheckDimAuto (); - SetNeedsLayout (); - SetNeedsDisplay (); - - return view; - } - - /// Adds the specified views (children) to the view. - /// Array of one or more views (can be optional parameter). - /// - /// - /// The Views that have been added to this view can be retrieved via the property. See also - /// and . - /// - /// - /// Subviews will be disposed when this View is disposed. In other-words, calling this method causes - /// the lifecycle of the subviews to be transferred to this View. - /// - /// - public void Add (params View [] views) - { - if (views is null) - { - return; - } - - foreach (View view in views) - { - Add (view); - } - } - - /// Event fired when this view is added to another. - public event EventHandler Added; - - /// Moves the subview backwards in the hierarchy, only one step - /// The subview to send backwards - /// If you want to send the view all the way to the back use SendSubviewToBack. - public void BringSubviewForward (View subview) - { - PerformActionForSubview ( - subview, - x => - { - int idx = _subviews.IndexOf (x); - - if (idx + 1 < _subviews.Count) - { - _subviews.Remove (x); - _subviews.Insert (idx + 1, x); - } - } - ); - } - - /// Brings the specified subview to the front so it is drawn on top of any other views. - /// The subview to send to the front - /// . - public void BringSubviewToFront (View subview) - { - PerformActionForSubview ( - subview, - x => - { - _subviews.Remove (x); - _subviews.Add (x); - } - ); - } - - /// Get the top superview of a given . - /// The superview view. - public View GetTopSuperView (View view = null, View superview = null) - { - View top = superview ?? Application.Top; - - for (View v = view?.SuperView ?? this?.SuperView; v != null; v = v.SuperView) - { - top = v; - - if (top == superview) - { - break; - } - } - - return top; - } - - /// Method invoked when a subview is being added to this view. - /// Event where is the subview being added. - public virtual void OnAdded (SuperViewChangedEventArgs e) - { - View view = e.Child; - view.IsAdded = true; - view.OnResizeNeeded (); - view.Added?.Invoke (this, e); - } - - /// Method invoked when a subview is being removed from this view. - /// Event args describing the subview being removed. - public virtual void OnRemoved (SuperViewChangedEventArgs e) - { - View view = e.Child; - view.IsAdded = false; - view.Removed?.Invoke (this, e); - } - - /// Removes a subview added via or from this View. - /// - /// - /// Normally Subviews will be disposed when this View is disposed. Removing a Subview causes ownership of the - /// Subview's - /// lifecycle to be transferred to the caller; the caller muse call . - /// - /// - public virtual View Remove (View view) - { - if (view is null || _subviews is null) - { - return view; - } - - Rectangle touched = view.Frame; - _subviews.Remove (view); - _tabIndexes.Remove (view); - view._superView = null; - view._tabIndex = -1; - SetNeedsLayout (); - SetNeedsDisplay (); - - foreach (View v in _subviews) - { - if (v.Frame.IntersectsWith (touched)) - { - view.SetNeedsDisplay (); - } - } - - OnRemoved (new (this, view)); - - if (Focused == view) - { - Focused = null; - } - - return view; - } - - /// - /// Removes all subviews (children) added via or from this View. - /// - /// - /// - /// Normally Subviews will be disposed when this View is disposed. Removing a Subview causes ownership of the - /// Subview's - /// lifecycle to be transferred to the caller; the caller must call on any Views that were - /// added. - /// - /// - public virtual void RemoveAll () - { - if (_subviews is null) - { - return; - } - - while (_subviews.Count > 0) - { - Remove (_subviews [0]); - } - } - - /// Event fired when this view is removed from another. - public event EventHandler Removed; - - /// Moves the subview backwards in the hierarchy, only one step - /// The subview to send backwards - /// If you want to send the view all the way to the back use SendSubviewToBack. - public void SendSubviewBackwards (View subview) - { - PerformActionForSubview ( - subview, - x => - { - int idx = _subviews.IndexOf (x); - - if (idx > 0) - { - _subviews.Remove (x); - _subviews.Insert (idx - 1, x); - } - } - ); - } - - /// Sends the specified subview to the front so it is the first view drawn - /// The subview to send to the front - /// . - public void SendSubviewToBack (View subview) - { - PerformActionForSubview ( - subview, - x => - { - _subviews.Remove (x); - _subviews.Insert (0, subview); - } - ); - } - - private void PerformActionForSubview (View subview, Action action) - { - if (_subviews.Contains (subview)) - { - action (subview); - } - - SetNeedsDisplay (); - subview.SetNeedsDisplay (); - } - - #region Focus - - /// Exposed as `internal` for unit tests. Indicates focus navigation direction. - internal enum NavigationDirection - { - /// Navigate forward. - Forward, - - /// Navigate backwards. - Backward - } - - /// Event fired when the view gets focus. - public event EventHandler Enter; - - /// Event fired when the view looses focus. - public event EventHandler Leave; - - private NavigationDirection _focusDirection; - - internal NavigationDirection FocusDirection - { - get => SuperView?.FocusDirection ?? _focusDirection; - set - { - if (SuperView is { }) - { - SuperView.FocusDirection = value; - } - else - { - _focusDirection = value; - } - } - } - - private bool _hasFocus; - - /// - public bool HasFocus - { - set => SetHasFocus (value, this, true); - get => _hasFocus; - } - - private void SetHasFocus (bool value, View view, bool force = false) - { - if (HasFocus != value || force) - { - _hasFocus = value; - - if (value) - { - OnEnter (view); - } - else - { - OnLeave (view); - } - - SetNeedsDisplay (); - } - - // Remove focus down the chain of subviews if focus is removed - if (!value && Focused is { }) - { - View f = Focused; - f.OnLeave (view); - f.SetHasFocus (false, view); - Focused = null; - } - } - - /// Event fired when the value is being changed. - public event EventHandler CanFocusChanged; - - /// Method invoked when the property from a view is changed. - public virtual void OnCanFocusChanged () { CanFocusChanged?.Invoke (this, EventArgs.Empty); } - - private bool _oldCanFocus; - private bool _canFocus; - - /// Gets or sets a value indicating whether this can focus. - public bool CanFocus - { - get => _canFocus; - set - { - if (!_addingView && IsInitialized && SuperView?.CanFocus == false && value) - { - throw new InvalidOperationException ("Cannot set CanFocus to true if the SuperView CanFocus is false!"); - } - - if (_canFocus == value) - { - return; - } - - _canFocus = value; - - switch (_canFocus) - { - case false when _tabIndex > -1: - TabIndex = -1; - - break; - case true when SuperView?.CanFocus == false && _addingView: - SuperView.CanFocus = true; - - break; - } - - if (_canFocus && _tabIndex == -1) - { - TabIndex = SuperView is { } ? SuperView._tabIndexes.IndexOf (this) : -1; - } - - TabStop = _canFocus; - - if (!_canFocus && SuperView?.Focused == this) - { - SuperView.Focused = null; - } - - if (!_canFocus && HasFocus) - { - SetHasFocus (false, this); - SuperView?.EnsureFocus (); - - if (SuperView is { Focused: null }) - { - SuperView.FocusNext (); - - if (SuperView.Focused is null && Application.Current is { }) - { - Application.Current.FocusNext (); - } - - ApplicationOverlapped.BringOverlappedTopToFront (); - } - } - - if (_subviews is { } && IsInitialized) - { - foreach (View view in _subviews) - { - if (view.CanFocus != value) - { - if (!value) - { - view._oldCanFocus = view.CanFocus; - view._oldTabIndex = view._tabIndex; - view.CanFocus = false; - view._tabIndex = -1; - } - else - { - if (_addingView) - { - view._addingView = true; - } - - view.CanFocus = view._oldCanFocus; - view._tabIndex = view._oldTabIndex; - view._addingView = false; - } - } - } - - if (this is Toplevel && Application.Current.Focused != this) - { - ApplicationOverlapped.BringOverlappedTopToFront (); - } - } - - OnCanFocusChanged (); - SetNeedsDisplay (); - } - } - - /// - /// Called when a view gets focus. - /// - /// The view that is losing focus. - /// true, if the event was handled, false otherwise. - public virtual bool OnEnter (View view) - { - var args = new FocusEventArgs (view, this); - Enter?.Invoke (this, args); - - if (args.Handled) - { - return true; - } - - return false; - } - - /// Method invoked when a view loses focus. - /// The view that is getting focus. - /// true, if the event was handled, false otherwise. - public virtual bool OnLeave (View view) - { - var args = new FocusEventArgs (this, view); - Leave?.Invoke (this, args); - - if (args.Handled) - { - return true; - } - - return false; - } - - // BUGBUG: This API is poorly defined and implemented. It does not specify what it means if THIS view is focused and has no subviews. - /// Returns the currently focused Subview inside this view, or null if nothing is focused. - /// The focused. - public View Focused { get; private set; } - - // BUGBUG: This API is poorly defined and implemented. It does not specify what it means if THIS view is focused and has no subviews. - /// Returns the most focused Subview in the chain of subviews (the leaf view that has the focus). - /// The most focused View. - public View MostFocused - { - get - { - if (Focused is null) - { - return null; - } - - View most = Focused.MostFocused; - - if (most is { }) - { - return most; - } - - return Focused; - } - } - - /// Causes the specified subview to have focus. - /// View. - private void SetFocus (View view) - { - if (view is null) - { - return; - } - - //Console.WriteLine ($"Request to focus {view}"); - if (!view.CanFocus || !view.Visible || !view.Enabled) - { - return; - } - - if (Focused?._hasFocus == true && Focused == view) - { - return; - } - - if ((Focused?._hasFocus == true && Focused?.SuperView == view) || view == this) - { - if (!view._hasFocus) - { - view._hasFocus = true; - } - - return; - } - - // Make sure that this view is a subview - View c; - - for (c = view._superView; c != null; c = c._superView) - { - if (c == this) - { - break; - } - } - - if (c is null) - { - throw new ArgumentException ("the specified view is not part of the hierarchy of this view"); - } - - if (Focused is { }) - { - Focused.SetHasFocus (false, view); - } - - View f = Focused; - Focused = view; - Focused.SetHasFocus (true, f); - Focused.EnsureFocus (); - - // Send focus upwards - if (SuperView is { }) - { - SuperView.SetFocus (this); - } - else - { - SetFocus (this); - } - } - - /// Causes this view to be focused and entire Superview hierarchy to have the focused order updated. - public void SetFocus () - { - if (!CanBeVisible (this) || !Enabled) - { - if (HasFocus) - { - SetHasFocus (false, this); - } - - return; - } - - if (SuperView is { }) - { - SuperView.SetFocus (this); - } - else - { - SetFocus (this); - } - } - - /// - /// If there is no focused subview, calls or based on . - /// does nothing. - /// - public void EnsureFocus () - { - if (Focused is null && _subviews?.Count > 0) - { - if (FocusDirection == NavigationDirection.Forward) - { - FocusFirst (); - } - else - { - FocusLast (); - } - } - } - - /// - /// Focuses the last focusable view in if one exists. If there are no views in then the focus is set to the view itself. - /// - public void FocusFirst (bool overlapped = false) - { - if (!CanBeVisible (this)) - { - return; - } - - if (_tabIndexes is null) - { - SuperView?.SetFocus (this); - - return; - } - - foreach (View view in _tabIndexes.Where (v => !overlapped || v.Arrangement.HasFlag (ViewArrangement.Overlapped))) - { - if (view.CanFocus && view._tabStop && view.Visible && view.Enabled) - { - SetFocus (view); - - return; - } - } - } - - /// - /// Focuses the last focusable view in if one exists. If there are no views in then the focus is set to the view itself. - /// - public void FocusLast (bool overlapped = false) - { - if (!CanBeVisible (this)) - { - return; - } - - if (_tabIndexes is null) - { - SuperView?.SetFocus (this); - - return; - } - - foreach (View view in _tabIndexes.Where (v => !overlapped || v.Arrangement.HasFlag (ViewArrangement.Overlapped)).Reverse ()) - { - if (view.CanFocus && view._tabStop && view.Visible && view.Enabled) - { - SetFocus (view); - - return; - } - } - } - - /// - /// Focuses the previous view in . If there is no previous view, the focus is set to the view itself. - /// - /// if previous was focused, otherwise. - public bool FocusPrev () - { - if (!CanBeVisible (this)) - { - return false; - } - - FocusDirection = NavigationDirection.Backward; - - if (TabIndexes is null || TabIndexes.Count == 0) - { - return false; - } - - if (Focused is null) - { - FocusLast (); - - return Focused != null; - } - - int focusedIdx = -1; - - for (int i = TabIndexes.Count; i > 0;) - { - i--; - View w = TabIndexes [i]; - - if (w.HasFocus) - { - if (w.FocusPrev ()) - { - return true; - } - - focusedIdx = i; - - continue; - } - - if (w.CanFocus && focusedIdx != -1 && w._tabStop && w.Visible && w.Enabled) - { - Focused.SetHasFocus (false, w); - - // If the focused view is overlapped don't focus on the next if it's not overlapped. - if (Focused.Arrangement.HasFlag (ViewArrangement.Overlapped) && !w.Arrangement.HasFlag (ViewArrangement.Overlapped)) - { - return false; - } - - // If the focused view is not overlapped and the next is, skip it - if (!Focused.Arrangement.HasFlag (ViewArrangement.Overlapped) && w.Arrangement.HasFlag (ViewArrangement.Overlapped)) - { - continue; - } - - if (w.CanFocus && w._tabStop && w.Visible && w.Enabled) - { - w.FocusLast (); - } - - SetFocus (w); - - return true; - } - } - - // There's no prev view in tab indexes. - if (Focused is { }) - { - // Leave Focused - Focused.SetHasFocus (false, this); - - if (Focused.Arrangement.HasFlag (ViewArrangement.Overlapped)) - { - FocusLast (true); - return true; - } - - // Signal to caller no next view was found - Focused = null; - } - - return false; - } - - /// - /// Focuses the next view in . If there is no next view, the focus is set to the view itself. - /// - /// if next was focused, otherwise. - public bool FocusNext () - { - if (!CanBeVisible (this)) - { - return false; - } - - FocusDirection = NavigationDirection.Forward; - - if (TabIndexes is null || TabIndexes.Count == 0) - { - return false; - } - - if (Focused is null) - { - FocusFirst (); - - return Focused != null; - } - - int focusedIdx = -1; - - for (var i = 0; i < TabIndexes.Count; i++) - { - View w = TabIndexes [i]; - - if (w.HasFocus) - { - if (w.FocusNext ()) - { - return true; - } - - focusedIdx = i; - - continue; - } - - if (w.CanFocus && focusedIdx != -1 && w._tabStop && w.Visible && w.Enabled) - { - Focused.SetHasFocus (false, w); - - // If the focused view is overlapped don't focus on the next if it's not overlapped. - if (Focused.Arrangement.HasFlag (ViewArrangement.Overlapped) && !w.Arrangement.HasFlag (ViewArrangement.Overlapped)) - { - return false; - } - - // If the focused view is not overlapped and the next is, skip it - if (!Focused.Arrangement.HasFlag (ViewArrangement.Overlapped) && w.Arrangement.HasFlag (ViewArrangement.Overlapped)) - { - continue; - } - - if (w.CanFocus && w._tabStop && w.Visible && w.Enabled) - { - w.FocusFirst (); - } - - SetFocus (w); - - return true; - } - } - - // There's no next view in tab indexes. - if (Focused is { }) - { - // Leave Focused - Focused.SetHasFocus (false, this); - - if (Focused.Arrangement.HasFlag (ViewArrangement.Overlapped)) - { - FocusFirst (true); - return true; - } - - // Signal to caller no next view was found - Focused = null; - } - - return false; - } - - private View GetMostFocused (View view) - { - if (view is null) - { - return null; - } - - return view.Focused is { } ? GetMostFocused (view.Focused) : view; - } - - /// - /// Gets or sets the cursor style to be used when the view is focused. The default is . - /// - public CursorVisibility CursorVisibility { get; set; } = CursorVisibility.Invisible; - - /// - /// Positions the cursor in the right position based on the currently focused view in the chain. - /// - /// - /// - /// Views that are focusable should override to make sure that the cursor is - /// placed in a location that makes sense. Some terminals do not have a way of hiding the cursor, so it can be - /// distracting to have the cursor left at the last focused view. So views should make sure that they place the - /// cursor in a visually sensible place. The default implementation of will place the - /// cursor at either the hotkey (if defined) or 0,0. - /// - /// - /// Viewport-relative cursor position. Return to ensure the cursor is not visible. - public virtual Point? PositionCursor () - { - if (IsInitialized && CanFocus && HasFocus) - { - // By default, position the cursor at the hotkey (if any) or 0, 0. - Move (TextFormatter.HotKeyPos == -1 ? 0 : TextFormatter.CursorPosition, 0); - } - - // Returning null will hide the cursor. - return null; - } - - #endregion Focus -} diff --git a/UICatalog/Scenarios/ViewExperiments.cs b/UICatalog/Scenarios/ViewExperiments.cs index 263a507d9..4c212b8b7 100644 --- a/UICatalog/Scenarios/ViewExperiments.cs +++ b/UICatalog/Scenarios/ViewExperiments.cs @@ -27,6 +27,8 @@ public class ViewExperiments : Scenario Title = "View1", ColorScheme = Colors.ColorSchemes ["Base"], Id = "View1", + ShadowStyle = ShadowStyle.Transparent, + BorderStyle = LineStyle.Double, CanFocus = true, // Can't drag without this? BUGBUG Arrangement = ViewArrangement.Movable | ViewArrangement.Overlapped }; @@ -46,16 +48,7 @@ public class ViewExperiments : Scenario //app.Add (view); - view.Margin.Thickness = new (0); - view.Margin.ColorScheme = Colors.ColorSchemes ["Toplevel"]; - view.Margin.Data = "Margin"; - view.Border.Thickness = new (1); - view.Border.LineStyle = LineStyle.Double; - view.Border.ColorScheme = view.ColorScheme; - view.Border.Data = "Border"; - view.Padding.Thickness = new (0); - view.Padding.ColorScheme = Colors.ColorSchemes ["Error"]; - view.Padding.Data = "Padding"; + view.BorderStyle = LineStyle.Double; var view2 = new View { @@ -66,6 +59,8 @@ public class ViewExperiments : Scenario Title = "View2", ColorScheme = Colors.ColorSchemes ["Base"], Id = "View2", + ShadowStyle = ShadowStyle.Transparent, + BorderStyle = LineStyle.Double, CanFocus = true, // Can't drag without this? BUGBUG Arrangement = ViewArrangement.Movable | ViewArrangement.Overlapped }; @@ -85,16 +80,6 @@ public class ViewExperiments : Scenario view2.Add (button); view2.Add (button); - view2.Margin.Thickness = new (0); - view2.Margin.ColorScheme = Colors.ColorSchemes ["Toplevel"]; - view2.Margin.Data = "Margin"; - view2.Border.Thickness = new (1); - view2.Border.LineStyle = LineStyle.Double; - view2.Border.ColorScheme = view2.ColorScheme; - view2.Border.Data = "Border"; - view2.Padding.Thickness = new (0); - view2.Padding.ColorScheme = Colors.ColorSchemes ["Error"]; - view2.Padding.Data = "Padding"; button = new () { From 9b89fe6466576ba58cbb26a96bd1eb10ef72c98a Mon Sep 17 00:00:00 2001 From: Tig Date: Thu, 25 Jul 2024 11:21:49 -0600 Subject: [PATCH 31/33] Code cleanup and API docs - getting better understanding of navigation code. --- Terminal.Gui/View/View.Hierarchy.cs | 10 ++-- Terminal.Gui/View/View.Navigation.cs | 86 +++++++++++++++++++--------- Terminal.Gui/View/View.cs | 2 +- UICatalog/Scenarios/Notepad.cs | 1 - 4 files changed, 64 insertions(+), 35 deletions(-) diff --git a/Terminal.Gui/View/View.Hierarchy.cs b/Terminal.Gui/View/View.Hierarchy.cs index 125baf33f..d66253849 100644 --- a/Terminal.Gui/View/View.Hierarchy.cs +++ b/Terminal.Gui/View/View.Hierarchy.cs @@ -3,7 +3,6 @@ namespace Terminal.Gui; public partial class View // SuperView/SubView hierarchy management (SuperView, SubViews, Add, Remove, etc.) { private static readonly IList _empty = new List (0).AsReadOnly (); - internal bool _addingView; private List _subviews; // This is null, and allocated on demand. private View _superView; @@ -62,19 +61,20 @@ public partial class View // SuperView/SubView hierarchy management (SuperView, if (view.CanFocus) { - _addingView = true; + // BUGBUG: This is a poor API design. Automatic behavior like this is non-obvious and should be avoided. Instead, callers to Add should be explicit about what they want. + _addingViewSoCanFocusAlsoUpdatesSuperView = true; if (SuperView?.CanFocus == false) { - SuperView._addingView = true; + SuperView._addingViewSoCanFocusAlsoUpdatesSuperView = true; SuperView.CanFocus = true; - SuperView._addingView = false; + SuperView._addingViewSoCanFocusAlsoUpdatesSuperView = false; } // QUESTION: This automatic behavior of setting CanFocus to true on the SuperView is not documented, and is annoying. CanFocus = true; view._tabIndex = _tabIndexes.IndexOf (view); - _addingView = false; + _addingViewSoCanFocusAlsoUpdatesSuperView = false; } if (view.Enabled && !Enabled) diff --git a/Terminal.Gui/View/View.Navigation.cs b/Terminal.Gui/View/View.Navigation.cs index 4ff99c3d7..3cc0c1f11 100644 --- a/Terminal.Gui/View/View.Navigation.cs +++ b/Terminal.Gui/View/View.Navigation.cs @@ -75,7 +75,7 @@ public partial class View // Focus and cross-view navigation management (TabStop private NavigationDirection _focusDirection; /// - /// Gets or sets the focus direction for this view and all subviews. + /// INTERNAL API that gets or sets the focus direction for this view and all subviews. /// Setting this property will set the focus direction for all views up the SuperView hierarchy. /// internal NavigationDirection FocusDirection @@ -160,19 +160,14 @@ public partial class View // Focus and cross-view navigation management (TabStop } } - /// Raised when has been changed. - /// - /// Raised by the virtual method. - /// - public event EventHandler CanFocusChanged; - - /// Invoked when the property from a view is changed. - /// - /// Raises the event. - /// - public virtual void OnCanFocusChanged () { CanFocusChanged?.Invoke (this, EventArgs.Empty); } + // BUGBUG: This is a poor API design. Automatic behavior like this is non-obvious and should be avoided. Instead, callers to Add should be explicit about what they want. + // Set to true in Add() to indicate that the view being added to a SuperView has CanFocus=true. + // Makes it so CanFocus will update the SuperView's CanFocus property. + internal bool _addingViewSoCanFocusAlsoUpdatesSuperView; + // Used to cache CanFocus on subviews when CanFocus is set to false so that it can be restored when CanFocus is changed back to true private bool _oldCanFocus; + private bool _canFocus; /// Gets or sets a value indicating whether this can be focused. @@ -180,13 +175,24 @@ public partial class View // Focus and cross-view navigation management (TabStop /// /// must also have set to . /// + /// + /// When set to , if this view is focused, the focus will be set to the next focusable view. + /// + /// + /// When set to , the will be set to -1. + /// + /// + /// When set to , the values of and for all + /// subviews will be cached so that when is set back to , the subviews + /// will be restored to their previous values. + /// /// public bool CanFocus { get => _canFocus; set { - if (!_addingView && IsInitialized && SuperView?.CanFocus == false && value) + if (!_addingViewSoCanFocusAlsoUpdatesSuperView && IsInitialized && SuperView?.CanFocus == false && value) { throw new InvalidOperationException ("Cannot set CanFocus to true if the SuperView CanFocus is false!"); } @@ -204,7 +210,8 @@ public partial class View // Focus and cross-view navigation management (TabStop TabIndex = -1; break; - case true when SuperView?.CanFocus == false && _addingView: + + case true when SuperView?.CanFocus == false && _addingViewSoCanFocusAlsoUpdatesSuperView: SuperView.CanFocus = true; break; @@ -225,8 +232,9 @@ public partial class View // Focus and cross-view navigation management (TabStop if (!_canFocus && HasFocus) { SetHasFocus (false, this); - SuperView?.EnsureFocus (); + SuperView?.FocusFirstOrLast (); + // If EnsureFocus () didn't set focus to a view, focus the next focusable view in the application if (SuperView is { Focused: null }) { SuperView.FocusNext (); @@ -248,6 +256,7 @@ public partial class View // Focus and cross-view navigation management (TabStop { if (!value) { + // Cache the old CanFocus and TabIndex so that they can be restored when CanFocus is changed back to true view._oldCanFocus = view.CanFocus; view._oldTabIndex = view._tabIndex; view.CanFocus = false; @@ -255,19 +264,20 @@ public partial class View // Focus and cross-view navigation management (TabStop } else { - if (_addingView) + if (_addingViewSoCanFocusAlsoUpdatesSuperView) { - view._addingView = true; + view._addingViewSoCanFocusAlsoUpdatesSuperView = true; } + // Restore the old CanFocus and TabIndex to the values they held before CanFocus was set to false view.CanFocus = view._oldCanFocus; view._tabIndex = view._oldTabIndex; - view._addingView = false; + view._addingViewSoCanFocusAlsoUpdatesSuperView = false; } } } - if (this is Toplevel && Application.Current.Focused != this) + if (this is Toplevel && Application.Current!.Focused != this) { ApplicationOverlapped.BringOverlappedTopToFront (); } @@ -278,6 +288,18 @@ public partial class View // Focus and cross-view navigation management (TabStop } } + /// Raised when has been changed. + /// + /// Raised by the virtual method. + /// + public event EventHandler CanFocusChanged; + + /// Invoked when the property from a view is changed. + /// + /// Raises the event. + /// + public virtual void OnCanFocusChanged () { CanFocusChanged?.Invoke (this, EventArgs.Empty); } + /// Returns the currently focused Subview inside this view, or if nothing is focused. /// The currently focused Subview. public View Focused { get; private set; } @@ -361,7 +383,7 @@ public partial class View // Focus and cross-view navigation management (TabStop View f = Focused; Focused = view; Focused.SetHasFocus (true, f); - Focused.EnsureFocus (); + Focused.FocusFirstOrLast (); // Send focus upwards if (SuperView is { }) @@ -398,11 +420,11 @@ public partial class View // Focus and cross-view navigation management (TabStop } /// - /// If there is no focused subview, calls or based on + /// INTERNAL helper for calling or based on /// . - /// does nothing. + /// FocusDirection is not public. This API is thus non-deterministic from a public API perspective. /// - public void EnsureFocus () + internal void FocusFirstOrLast () { if (Focused is null && _subviews?.Count > 0) { @@ -418,10 +440,14 @@ public partial class View // Focus and cross-view navigation management (TabStop } /// - /// Focuses the last focusable view in if one exists. If there are no views in + /// Focuses the first focusable view in if one exists. If there are no views in /// then the focus is set to the view itself. /// - public void FocusFirst (bool overlapped = false) + /// + /// If , only subviews where has set + /// will be considered. + /// + public void FocusFirst (bool overlappedOnly = false) { if (!CanBeVisible (this)) { @@ -435,7 +461,7 @@ public partial class View // Focus and cross-view navigation management (TabStop return; } - foreach (View view in _tabIndexes.Where (v => !overlapped || v.Arrangement.HasFlag (ViewArrangement.Overlapped))) + foreach (View view in _tabIndexes.Where (v => !overlappedOnly || v.Arrangement.HasFlag (ViewArrangement.Overlapped))) { if (view.CanFocus && view._tabStop && view.Visible && view.Enabled) { @@ -450,7 +476,11 @@ public partial class View // Focus and cross-view navigation management (TabStop /// Focuses the last focusable view in if one exists. If there are no views in /// then the focus is set to the view itself. /// - public void FocusLast (bool overlapped = false) + /// + /// If , only subviews where has set + /// will be considered. + /// + public void FocusLast (bool overlappedOnly = false) { if (!CanBeVisible (this)) { @@ -464,7 +494,7 @@ public partial class View // Focus and cross-view navigation management (TabStop return; } - foreach (View view in _tabIndexes.Where (v => !overlapped || v.Arrangement.HasFlag (ViewArrangement.Overlapped)).Reverse ()) + foreach (View view in _tabIndexes.Where (v => !overlappedOnly || v.Arrangement.HasFlag (ViewArrangement.Overlapped)).Reverse ()) { if (view.CanFocus && view._tabStop && view.Visible && view.Enabled) { diff --git a/Terminal.Gui/View/View.cs b/Terminal.Gui/View/View.cs index d340a5aa4..8eb5a9374 100644 --- a/Terminal.Gui/View/View.cs +++ b/Terminal.Gui/View/View.cs @@ -332,7 +332,7 @@ public partial class View : Responder, ISupportInitializeNotification else { view.Enabled = view._oldEnabled; - view._addingView = _enabled; + view._addingViewSoCanFocusAlsoUpdatesSuperView = _enabled; } } } diff --git a/UICatalog/Scenarios/Notepad.cs b/UICatalog/Scenarios/Notepad.cs index dd962bfc1..2dbca0117 100644 --- a/UICatalog/Scenarios/Notepad.cs +++ b/UICatalog/Scenarios/Notepad.cs @@ -309,7 +309,6 @@ public class Notepad : Scenario tab.CloneTo (newTabView); newTile.ContentView.Add (newTabView); - newTabView.EnsureFocus (); newTabView.FocusFirst (); newTabView.FocusNext (); } From ccec0eec1191140c945c6419a03e57126793f910 Mon Sep 17 00:00:00 2001 From: Tig Date: Thu, 25 Jul 2024 12:16:10 -0600 Subject: [PATCH 32/33] Documenting focus code --- Terminal.Gui/View/View.Navigation.cs | 21 ++++++++++++++++----- Terminal.Gui/Views/Toplevel.cs | 2 +- UnitTests/Views/TextFieldTests.cs | 6 +++--- UnitTests/Views/TreeTableSourceTests.cs | 2 +- 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/Terminal.Gui/View/View.Navigation.cs b/Terminal.Gui/View/View.Navigation.cs index 3cc0c1f11..edbe5eabc 100644 --- a/Terminal.Gui/View/View.Navigation.cs +++ b/Terminal.Gui/View/View.Navigation.cs @@ -1,3 +1,5 @@ +using System.Diagnostics; + namespace Terminal.Gui; public partial class View // Focus and cross-view navigation management (TabStop, TabIndex, etc...) @@ -603,7 +605,7 @@ public partial class View // Focus and cross-view navigation management (TabStop /// Focuses the next view in . If there is no next view, the focus is set to the view /// itself. /// - /// if next was focused, otherwise. + /// if focus was changed to another subview (or stayed on this one), otherwise. public bool FocusNext () { if (!CanBeVisible (this)) @@ -622,7 +624,7 @@ public partial class View // Focus and cross-view navigation management (TabStop { FocusFirst (); - return Focused != null; + return Focused is { }; } int focusedIdx = -1; @@ -633,18 +635,25 @@ public partial class View // Focus and cross-view navigation management (TabStop if (w.HasFocus) { + // A subview has focus, tell *it* to FocusNext if (w.FocusNext ()) { + // The subview changed which of it's subviews had focus return true; } + Debug.Assert (w.HasFocus); + + // The subview has no subviews that can be next. Cache that we found a focused subview. focusedIdx = i; continue; } - if (w.CanFocus && focusedIdx != -1 && w._tabStop && w.Visible && w.Enabled) + // The subview does not have focus, but at least one other that can. Can this one be focused? + if (focusedIdx != -1 && w.CanFocus && w._tabStop && w.Visible && w.Enabled) { + // Make w Leave Focused.SetHasFocus (false, w); //// If the focused view is overlapped don't focus on the next if it's not overlapped. @@ -659,6 +668,7 @@ public partial class View // Focus and cross-view navigation management (TabStop // continue; //} + // QUESTION: Why do we check these again here? if (w.CanFocus && w._tabStop && w.Visible && w.Enabled) { w.FocusFirst (); @@ -673,7 +683,7 @@ public partial class View // Focus and cross-view navigation management (TabStop // There's no next view in tab indexes. if (Focused is { }) { - // Leave Focused + // Leave Focused.SetHasFocus (false, this); //if (Focused.Arrangement.HasFlag (ViewArrangement.Overlapped)) @@ -682,7 +692,8 @@ public partial class View // Focus and cross-view navigation management (TabStop // return true; //} - // Signal to caller no next view was found + // Signal to caller no next view was found; this will enable it to make a peer + // or view up the superview hierarchy have focus. Focused = null; } diff --git a/Terminal.Gui/Views/Toplevel.cs b/Terminal.Gui/Views/Toplevel.cs index 26fe724c8..435bd6fc7 100644 --- a/Terminal.Gui/Views/Toplevel.cs +++ b/Terminal.Gui/Views/Toplevel.cs @@ -430,7 +430,7 @@ public partial class Toplevel : View { if (Focused is null) { - EnsureFocus (); + FocusFirstOrLast (); } return null; diff --git a/UnitTests/Views/TextFieldTests.cs b/UnitTests/Views/TextFieldTests.cs index ed3a35775..4096ee404 100644 --- a/UnitTests/Views/TextFieldTests.cs +++ b/UnitTests/Views/TextFieldTests.cs @@ -78,7 +78,7 @@ public class TextFieldTests (ITestOutputHelper output) public void Cancel_TextChanging_ThenBackspace () { var tf = new TextField (); - tf.EnsureFocus (); + tf.FocusFirstOrLast (); tf.NewKeyDownEvent (Key.A.WithShift); Assert.Equal ("A", tf.Text); @@ -929,7 +929,7 @@ public class TextFieldTests (ITestOutputHelper output) public void Backspace_From_End () { var tf = new TextField { Text = "ABC" }; - tf.EnsureFocus (); + tf.FocusFirstOrLast (); Assert.Equal ("ABC", tf.Text); tf.BeginInit (); tf.EndInit (); @@ -956,7 +956,7 @@ public class TextFieldTests (ITestOutputHelper output) public void Backspace_From_Middle () { var tf = new TextField { Text = "ABC" }; - tf.EnsureFocus (); + tf.FocusFirstOrLast (); tf.CursorPosition = 2; Assert.Equal ("ABC", tf.Text); diff --git a/UnitTests/Views/TreeTableSourceTests.cs b/UnitTests/Views/TreeTableSourceTests.cs index a1a319b1b..02625b6cf 100644 --- a/UnitTests/Views/TreeTableSourceTests.cs +++ b/UnitTests/Views/TreeTableSourceTests.cs @@ -289,7 +289,7 @@ public class TreeTableSourceTests : IDisposable var top = new Toplevel (); top.Add (tableView); - top.EnsureFocus (); + top.FocusFirstOrLast (); Assert.Equal (tableView, top.MostFocused); return tableView; From 15e6b4eff2cc759ca9d3c6f36ebb8d95d38c160b Mon Sep 17 00:00:00 2001 From: Tig Date: Thu, 25 Jul 2024 14:40:11 -0600 Subject: [PATCH 33/33] Documenting focus code --- Terminal.Gui/View/View.Navigation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Terminal.Gui/View/View.Navigation.cs b/Terminal.Gui/View/View.Navigation.cs index edbe5eabc..292cb5c52 100644 --- a/Terminal.Gui/View/View.Navigation.cs +++ b/Terminal.Gui/View/View.Navigation.cs @@ -653,7 +653,7 @@ public partial class View // Focus and cross-view navigation management (TabStop // The subview does not have focus, but at least one other that can. Can this one be focused? if (focusedIdx != -1 && w.CanFocus && w._tabStop && w.Visible && w.Enabled) { - // Make w Leave + // Make Focused Leave Focused.SetHasFocus (false, w); //// If the focused view is overlapped don't focus on the next if it's not overlapped.