diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 444ac8c4b..28c7dff8e 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -150,12 +150,44 @@ dotnet run --project Examples/UICatalog/UICatalog.csproj 5. **Documentation is the Spec** - API docs are source of truth ### Coding Conventions -- Use explicit types (avoid `var` except for basic types like `int`, `string`) -- Use target-typed `new()` + +**⚠️ CRITICAL - These rules MUST be followed in ALL new code:** + +#### Type Declarations and Object Creation +- **ALWAYS use explicit types** - Never use `var` except for basic types (`int`, `string`, `bool`, `double`, `float`, `decimal`, `char`, `byte`) + ```csharp + // ✅ CORRECT - Explicit types + View view = new () { Width = 10 }; + MouseEventArgs args = new () { Position = new Point(5, 5) }; + List views = new (); + var count = 0; // OK - int is a basic type + var name = "test"; // OK - string is a basic type + + // ❌ WRONG - Using var for non-basic types + var view = new View { Width = 10 }; + var args = new MouseEventArgs { Position = new Point(5, 5) }; + var views = new List(); + ``` + +- **ALWAYS use target-typed `new()`** - Use `new ()` instead of `new TypeName()` when the type is already declared + ```csharp + // ✅ CORRECT - Target-typed new + View view = new () { Width = 10 }; + MouseEventArgs args = new (); + + // ❌ WRONG - Redundant type name + View view = new View() { Width = 10 }; + MouseEventArgs args = new MouseEventArgs(); + ``` + +#### Other Conventions - Follow `.editorconfig` settings (e.g., braces on new lines, spaces after keywords) - 4-space indentation +- No trailing whitespace - See `CONTRIBUTING.md` for full guidelines +**These conventions apply to ALL code - production code, test code, examples, and samples.** + ## Testing Requirements ### Code Coverage @@ -278,6 +310,8 @@ dotnet build --configuration Release --no-restore - ❌ Don't add tests to `UnitTests` if they can be parallelizable - ❌ Don't use `Application.Init` in new tests - ❌ Don't decrease code coverage +- ❌ **Don't use `var` for non-basic types** (use explicit types) +- ❌ **Don't use redundant type names with `new`** (use target-typed `new()`) - ❌ Don't add `var` everywhere (use explicit types) ## Additional Resources diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 76defbb7f..94066aff4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -99,6 +99,49 @@ Follow the template instructions found on Github. * **Documentation is the Spec** - We care deeply about providing delightful developer documentation and are sticklers for grammar and clarity. If the code and the docs conflict, we are biased to believe what we wrote in the API documentation. This drives a virtuous cycle of clear thinking. **Terminal.Gui** uses a derivative of the [Microsoft C# Coding Conventions](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions), with any deviations from those (somewhat older) conventions codified in the .editorconfig for the solution, as well as even more specific definitions in team-shared dotsettings files, used by ReSharper and Rider.\ + +### Critical Coding Standards + +**⚠️ These rules MUST be followed in ALL new code (production, tests, examples, samples):** + +#### Type Declarations and Object Creation + +1. **ALWAYS use explicit types** - Never use `var` except for basic types (`int`, `string`, `bool`, `double`, `float`, `decimal`, `char`, `byte`) + + ```csharp + // ✅ CORRECT - Explicit types + View view = new () { Width = 10 }; + MouseEventArgs args = new () { Position = new Point(5, 5) }; + List views = new (); + var count = 0; // OK - int is a basic type + var name = "test"; // OK - string is a basic type + + // ❌ WRONG - Using var for non-basic types + var view = new View { Width = 10 }; + var args = new MouseEventArgs { Position = new Point(5, 5) }; + var views = new List(); + ``` + +2. **ALWAYS use target-typed `new()`** - Use `new ()` instead of `new TypeName()` when the type is already declared + + ```csharp + // ✅ CORRECT - Target-typed new + View view = new () { Width = 10 }; + MouseEventArgs args = new (); + + // ❌ WRONG - Redundant type name + View view = new View() { Width = 10 }; + MouseEventArgs args = new MouseEventArgs(); + ``` + +**Why these rules matter:** +- Explicit types improve code readability and make the type system more apparent +- Target-typed `new()` reduces redundancy while maintaining clarity +- Consistency across the codebase makes it easier for all contributors to read and maintain code +- These conventions align with modern C# best practices (C# 9.0+) + +### Code Formatting + Before you commit code, please run the formatting rules on **only the code file(s) you have modified**, in one of the following ways, in order of most preferred to least preferred: 1. `Ctrl-E-C` if using ReSharper or Rider diff --git a/Terminal.Gui/App/Application.Driver.cs b/Terminal.Gui/App/Application.Driver.cs index aa3289530..a14e88d21 100644 --- a/Terminal.Gui/App/Application.Driver.cs +++ b/Terminal.Gui/App/Application.Driver.cs @@ -6,9 +6,11 @@ public static partial class Application // Driver abstractions { internal static bool _forceFakeConsole; + // TODO: Add to IApplication /// Gets the that has been selected. See also . public static IConsoleDriver? Driver { get; internal set; } + // TODO: Add to IApplication // BUGBUG: Force16Colors should be nullable. /// /// Gets or sets whether will be forced to output only the 16 colors defined in @@ -18,6 +20,7 @@ public static partial class Application // Driver abstractions [ConfigurationProperty (Scope = typeof (SettingsScope))] public static bool Force16Colors { get; set; } + // TODO: Add to IApplication // BUGBUG: ForceDriver should be nullable. /// /// Forces the use of the specified driver (one of "fake", "dotnet", "windows", or "unix"). If not @@ -30,9 +33,10 @@ public static partial class Application // Driver abstractions [ConfigurationProperty (Scope = typeof (SettingsScope))] public static string ForceDriver { get; set; } = string.Empty; + // TODO: Add to IApplication /// /// Collection of sixel images to write out to screen when updating. /// Only add to this collection if you are sure terminal supports sixel format. /// - public static List Sixel = new List (); + public static List Sixel { get; } = new List (); } diff --git a/Terminal.Gui/App/Application.Initialization.cs b/Terminal.Gui/App/Application.Initialization.cs index 04becee71..1d061209e 100644 --- a/Terminal.Gui/App/Application.Initialization.cs +++ b/Terminal.Gui/App/Application.Initialization.cs @@ -5,10 +5,42 @@ using System.Reflection; namespace Terminal.Gui.App; -public static partial class Application // Initialization (Init/Shutdown) +public static partial class Application // Lifecycle (Init/Shutdown) { + // TODO: Add to IApplication + /// Gets of list of types and type names that are available. + /// + [RequiresUnreferencedCode ("AOT")] + public static (List, List) GetDriverTypes () + { + // use reflection to get the list of drivers + List driverTypes = new (); - /// Initializes a new instance of a Terminal.Gui Application. must be called when the application is closing. + // Only inspect the IConsoleDriver assembly + Assembly asm = typeof (IConsoleDriver).Assembly; + + foreach (Type? type in asm.GetTypes ()) + { + if (typeof (IConsoleDriver).IsAssignableFrom (type) && type is { IsAbstract: false, IsClass: true }) + { + driverTypes.Add (type); + } + } + + List driverTypeNames = driverTypes + .Where (d => !typeof (IConsoleDriverFacade).IsAssignableFrom (d)) + .Select (d => d!.Name) + .Union (["dotnet", "windows", "unix", "fake"]) + .ToList ()!; + + return (driverTypes, driverTypeNames); + } + + // TODO: Add to IApplicationLifecycle + /// + /// Initializes a new instance of a Terminal.Gui Application. must be called when the + /// application is closing. + /// /// Call this method once per instance (or after has been called). /// /// This function loads the right for the platform, Creates a . and @@ -44,27 +76,60 @@ public static partial class Application // Initialization (Init/Shutdown) // that isn't supported by the modern application architecture if (driver is null) { - var driverNameToCheck = string.IsNullOrWhiteSpace (driverName) ? ForceDriver : driverName; + string driverNameToCheck = string.IsNullOrWhiteSpace (driverName) ? ForceDriver : driverName; + if (!string.IsNullOrEmpty (driverNameToCheck)) { (List drivers, List driverTypeNames) = GetDriverTypes (); Type? driverType = drivers.FirstOrDefault (t => t!.Name.Equals (driverNameToCheck, StringComparison.InvariantCultureIgnoreCase)); - + // If it's a legacy IConsoleDriver (not a Facade), use InternalInit which supports legacy drivers if (driverType is { } && !typeof (IConsoleDriverFacade).IsAssignableFrom (driverType)) { InternalInit (driver, driverName); + return; } } } - + // Otherwise delegate to the ApplicationImpl instance (which uses the modern architecture) ApplicationImpl.Instance.Init (driver, driverName ?? ForceDriver); } - internal static int MainThreadId { get; set; } = -1; + // TODO: Add to IApplicationLifecycle + /// + /// Gets whether the application has been initialized with and not yet shutdown with + /// . + /// + /// + /// + /// The event is raised after the and + /// methods have been called. + /// + /// + public static bool Initialized { get; internal set; } + // TODO: Add to IApplicationLifecycle + /// + /// 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; + + // TODO: Add to IApplicationLifecycle + /// 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 () { ApplicationImpl.Instance.Shutdown (); } + + // TODO: Add to IApplicationLifecycle // INTERNAL function for initializing an app with a Toplevel factory object, driver, and mainloop. // // Called from: @@ -95,7 +160,7 @@ public static partial class Application // Initialization (Init/Shutdown) if (!calledViaRunT) { // Reset all class variables (Application is a singleton). - ResetState (ignoreDisposed: true); + ResetState (true); } // For UnitTests @@ -120,7 +185,7 @@ public static partial class Application // Initialization (Init/Shutdown) //{ // (List drivers, List driverTypeNames) = GetDriverTypes (); // Type? driverType = drivers.FirstOrDefault (t => t!.Name.Equals (ForceDriver, StringComparison.InvariantCultureIgnoreCase)); - + // if (driverType is { } && !typeof (IConsoleDriverFacade).IsAssignableFrom (driverType)) // { // // This is a legacy driver (not a ConsoleDriverFacade) @@ -128,12 +193,13 @@ public static partial class Application // Initialization (Init/Shutdown) // useLegacyDriver = true; // } //} - + //// Use the modern application architecture //if (!useLegacyDriver) { ApplicationImpl.Instance.Init (driver, driverName); Debug.Assert (Driver is { }); + return; } } @@ -174,6 +240,14 @@ public static partial class Application // Initialization (Init/Shutdown) InitializedChanged?.Invoke (null, new (init)); } + internal static int MainThreadId { get; set; } = -1; + + // TODO: Add to IApplicationLifecycle + /// + /// Raises the event. + /// + internal static void OnInitializedChanged (object sender, EventArgs e) { InitializedChanged?.Invoke (sender, e); } + internal static void SubscribeDriverEvents () { ArgumentNullException.ThrowIfNull (Driver); @@ -194,74 +268,9 @@ public static partial class Application // Initialization (Init/Shutdown) Driver.MouseEvent -= Driver_MouseEvent; } - private static void Driver_SizeChanged (object? sender, SizeChangedEventArgs e) { OnSizeChanging (e); } private static void Driver_KeyDown (object? sender, Key e) { RaiseKeyDownEvent (e); } private static void Driver_KeyUp (object? sender, Key e) { RaiseKeyUpEvent (e); } private static void Driver_MouseEvent (object? sender, MouseEventArgs e) { RaiseMouseEvent (e); } - /// Gets of list of types and type names that are available. - /// - [RequiresUnreferencedCode ("AOT")] - public static (List, List) GetDriverTypes () - { - // use reflection to get the list of drivers - List driverTypes = new (); - - // Only inspect the IConsoleDriver assembly - var asm = typeof (IConsoleDriver).Assembly; - - foreach (Type? type in asm.GetTypes ()) - { - if (typeof (IConsoleDriver).IsAssignableFrom (type) && - type is { IsAbstract: false, IsClass: true }) - { - driverTypes.Add (type); - } - } - - List driverTypeNames = driverTypes - .Where (d => !typeof (IConsoleDriverFacade).IsAssignableFrom (d)) - .Select (d => d!.Name) - .Union (["dotnet", "windows", "unix", "fake"]) - .ToList ()!; - - - - return (driverTypes, driverTypeNames); - } - - /// 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 () => ApplicationImpl.Instance.Shutdown (); - - /// - /// Gets whether the application has been initialized with and not yet shutdown with . - /// - /// - /// - /// The event is raised after the and methods have been called. - /// - /// - public static bool Initialized { get; internal set; } - - /// - /// 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; - - /// - /// Raises the event. - /// - internal static void OnInitializedChanged (object sender, EventArgs e) - { - Application.InitializedChanged?.Invoke (sender, e); - } + private static void Driver_SizeChanged (object? sender, SizeChangedEventArgs e) { OnSizeChanging (e); } } diff --git a/Terminal.Gui/App/Application.Keyboard.cs b/Terminal.Gui/App/Application.Keyboard.cs index 133f56820..4ec75af85 100644 --- a/Terminal.Gui/App/Application.Keyboard.cs +++ b/Terminal.Gui/App/Application.Keyboard.cs @@ -83,7 +83,7 @@ public static partial class Application // Keyboard handling internal static void AddKeyBindings () { - if (Keyboard is Keyboard keyboard) + if (Keyboard is KeyboardImpl keyboard) { keyboard.AddKeyBindings (); } diff --git a/Terminal.Gui/App/Application.Mouse.cs b/Terminal.Gui/App/Application.Mouse.cs index cab426f98..287cb4e78 100644 --- a/Terminal.Gui/App/Application.Mouse.cs +++ b/Terminal.Gui/App/Application.Mouse.cs @@ -5,176 +5,39 @@ namespace Terminal.Gui.App; public static partial class Application // Mouse handling { - /// - /// INTERNAL API: Holds the last mouse position. - /// - internal static Point? LastMousePosition { get; set; } - /// /// Gets the most recent position of the mouse. /// - public static Point? GetLastMousePosition () { return LastMousePosition; } + public static Point? GetLastMousePosition () { return Mouse.GetLastMousePosition (); } /// Disable or enable the mouse. The mouse is enabled by default. [ConfigurationProperty (Scope = typeof (SettingsScope))] - public static bool IsMouseDisabled { get; set; } - - /// - /// Static reference to the current . - /// - public static IMouseGrabHandler MouseGrabHandler + public static bool IsMouseDisabled { - get => ApplicationImpl.Instance.MouseGrabHandler; - set => ApplicationImpl.Instance.MouseGrabHandler = value ?? - throw new ArgumentNullException(nameof(value)); + get => Mouse.IsMouseDisabled; + set => Mouse.IsMouseDisabled = value; } /// - /// INTERNAL API: Called when a mouse event is raised by the driver. Determines the view under the mouse and - /// calls the appropriate View mouse event handlers. + /// Gets the instance that manages mouse event handling and state. /// - /// This method can be used to simulate a mouse event, e.g. in unit tests. - /// The mouse event with coordinates relative to the screen. - internal static void RaiseMouseEvent (MouseEventArgs mouseEvent) - { - if (Initialized) - { - // LastMousePosition is a static; only set if the application is initialized. - LastMousePosition = mouseEvent.ScreenPosition; - } - - if (IsMouseDisabled) - { - return; - } - - // The position of the mouse is the same as the screen position at the application level. - //Debug.Assert (mouseEvent.Position == mouseEvent.ScreenPosition); - mouseEvent.Position = mouseEvent.ScreenPosition; - - List currentViewsUnderMouse = View.GetViewsUnderLocation (mouseEvent.ScreenPosition, ViewportSettingsFlags.TransparentMouse); - - View? deepestViewUnderMouse = currentViewsUnderMouse.LastOrDefault (); - - if (deepestViewUnderMouse is { }) - { -#if DEBUG_IDISPOSABLE - if (View.EnableDebugIDisposableAsserts && deepestViewUnderMouse.WasDisposed) - { - throw new ObjectDisposedException (deepestViewUnderMouse.GetType ().FullName); - } -#endif - mouseEvent.View = deepestViewUnderMouse; - } - - MouseEvent?.Invoke (null, mouseEvent); - - if (mouseEvent.Handled) - { - return; - } - - // Dismiss the Popover if the user presses mouse outside of it - if (mouseEvent.IsPressed - && Popover?.GetActivePopover () as View is { Visible: true } visiblePopover - && View.IsInHierarchy (visiblePopover, deepestViewUnderMouse, includeAdornments: true) is false) - { - ApplicationPopover.HideWithQuitCommand (visiblePopover); - - // Recurse once so the event can be handled below the popover - RaiseMouseEvent (mouseEvent); - - return; - } - - if (HandleMouseGrab (deepestViewUnderMouse, mouseEvent)) - { - return; - } - - // May be null before the prior condition or the condition may set it as null. - // So, the checking must be outside the prior condition. - if (deepestViewUnderMouse is null) - { - return; - } - - // if the mouse is outside the Application.Top or Application.Popover hierarchy, we don't want to - // send the mouse event to the deepest view under the mouse. - if (!View.IsInHierarchy (Application.Top, deepestViewUnderMouse, true) && !View.IsInHierarchy (Popover?.GetActivePopover () as View, deepestViewUnderMouse, true)) - { - return; - } - - // Create a view-relative mouse event to send to the view that is under the mouse. - MouseEventArgs viewMouseEvent; - - if (deepestViewUnderMouse is Adornment adornment) - { - Point frameLoc = adornment.ScreenToFrame (mouseEvent.ScreenPosition); - - viewMouseEvent = new () - { - Position = frameLoc, - Flags = mouseEvent.Flags, - ScreenPosition = mouseEvent.ScreenPosition, - View = deepestViewUnderMouse - }; - } - else if (deepestViewUnderMouse.ViewportToScreen (Rectangle.Empty with { Size = deepestViewUnderMouse.Viewport.Size }).Contains (mouseEvent.ScreenPosition)) - { - Point viewportLocation = deepestViewUnderMouse.ScreenToViewport (mouseEvent.ScreenPosition); - - viewMouseEvent = new () - { - Position = viewportLocation, - Flags = mouseEvent.Flags, - ScreenPosition = mouseEvent.ScreenPosition, - View = deepestViewUnderMouse - }; - } - else - { - // The mouse was outside any View's Viewport. - // Debug.Fail ("This should never happen. If it does please file an Issue!!"); - - return; - } - - RaiseMouseEnterLeaveEvents (viewMouseEvent.ScreenPosition, currentViewsUnderMouse); - - while (deepestViewUnderMouse.NewMouseEvent (viewMouseEvent) is not true && MouseGrabHandler.MouseGrabView is not { }) - { - if (deepestViewUnderMouse is Adornment adornmentView) - { - deepestViewUnderMouse = adornmentView.Parent?.SuperView; - } - else - { - deepestViewUnderMouse = deepestViewUnderMouse.SuperView; - } - - if (deepestViewUnderMouse is null) - { - break; - } - - Point boundsPoint = deepestViewUnderMouse.ScreenToViewport (mouseEvent.ScreenPosition); - - viewMouseEvent = new () - { - Position = boundsPoint, - Flags = mouseEvent.Flags, - ScreenPosition = mouseEvent.ScreenPosition, - View = deepestViewUnderMouse - }; - } - } - + /// + /// + /// This property provides access to mouse-related functionality in a way that supports + /// parallel test execution by avoiding static state. + /// + /// + /// New code should use Application.Mouse instead of the static properties and methods + /// for better testability. Legacy static properties like and + /// are retained for backward compatibility. + /// + /// + public static IMouse Mouse => ApplicationImpl.Instance.Mouse; #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved /// - /// Raised when a mouse event occurs. Can be cancelled by setting to . + /// Raised when a mouse event occurs. Can be cancelled by setting to + /// . /// /// /// @@ -184,59 +47,34 @@ public static partial class Application // Mouse handling /// will be the deepest view under the mouse. /// /// - /// coordinates are view-relative. Only valid if is set. + /// coordinates are view-relative. Only valid if + /// is set. /// /// /// Use this even to handle mouse events at the application level, before View-specific handling. /// /// - public static event EventHandler? MouseEvent; + public static event EventHandler? MouseEvent + { + add => Mouse.MouseEvent += value; + remove => Mouse.MouseEvent -= value; + } #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved - internal static bool HandleMouseGrab (View? deepestViewUnderMouse, MouseEventArgs mouseEvent) - { - if (MouseGrabHandler.MouseGrabView is { }) - { -#if DEBUG_IDISPOSABLE - if (View.EnableDebugIDisposableAsserts && MouseGrabHandler.MouseGrabView.WasDisposed) - { - throw new ObjectDisposedException (MouseGrabHandler.MouseGrabView.GetType ().FullName); - } -#endif - - // If the mouse is grabbed, send the event to the view that grabbed it. - // The coordinates are relative to the Bounds of the view that grabbed the mouse. - Point frameLoc = MouseGrabHandler.MouseGrabView.ScreenToViewport (mouseEvent.ScreenPosition); - - var viewRelativeMouseEvent = new MouseEventArgs - { - Position = frameLoc, - Flags = mouseEvent.Flags, - ScreenPosition = mouseEvent.ScreenPosition, - View = deepestViewUnderMouse ?? MouseGrabHandler.MouseGrabView - }; - - //System.Diagnostics.Debug.WriteLine ($"{nme.Flags};{nme.X};{nme.Y};{mouseGrabView}"); - if (MouseGrabHandler.MouseGrabView?.NewMouseEvent (viewRelativeMouseEvent) is true) - { - return true; - } - - // ReSharper disable once ConditionIsAlwaysTrueOrFalse - if (MouseGrabHandler.MouseGrabView is null && deepestViewUnderMouse is Adornment) - { - // The view that grabbed the mouse has been disposed - return true; - } - } - - return false; - } + /// + /// INTERNAL: Holds the non- views that are currently under the + /// mouse. + /// + internal static List CachedViewsUnderMouse => Mouse.CachedViewsUnderMouse; /// - /// INTERNAL: Holds the non- views that are currently under the mouse. + /// INTERNAL API: Holds the last mouse position. /// - internal static List CachedViewsUnderMouse { get; } = []; + internal static Point? LastMousePosition + { + get => Mouse.LastMousePosition; + set => Mouse.LastMousePosition = value; + } /// /// INTERNAL: Raises the MouseEnter and MouseLeave events for the views that are under the mouse. @@ -245,59 +83,19 @@ public static partial class Application // Mouse handling /// The most recent result from GetViewsUnderLocation(). internal static void RaiseMouseEnterLeaveEvents (Point screenPosition, List currentViewsUnderMouse) { - // Tell any views that are no longer under the mouse that the mouse has left - List viewsToLeave = CachedViewsUnderMouse.Where (v => v is { } && !currentViewsUnderMouse.Contains (v)).ToList (); - - foreach (View? view in viewsToLeave) - { - if (view is null) - { - continue; - } - - view.NewMouseLeaveEvent (); - CachedViewsUnderMouse.Remove (view); - } - - // Tell any views that are now under the mouse that the mouse has entered and add them to the list - foreach (View? view in currentViewsUnderMouse) - { - if (view is null) - { - continue; - } - - if (CachedViewsUnderMouse.Contains (view)) - { - continue; - } - - CachedViewsUnderMouse.Add (view); - var raise = false; - - if (view is Adornment { Parent: { } } adornmentView) - { - Point superViewLoc = adornmentView.Parent.SuperView?.ScreenToViewport (screenPosition) ?? screenPosition; - raise = adornmentView.Contains (superViewLoc); - } - else - { - Point superViewLoc = view.SuperView?.ScreenToViewport (screenPosition) ?? screenPosition; - raise = view.Contains (superViewLoc); - } - - if (!raise) - { - continue; - } - - CancelEventArgs eventArgs = new (); - bool? cancelled = view.NewMouseEnterEvent (eventArgs); - - if (cancelled is true || eventArgs.Cancel) - { - break; - } - } + Mouse.RaiseMouseEnterLeaveEvents (screenPosition, currentViewsUnderMouse); } + + /// + /// INTERNAL API: Called when a mouse event is raised by the driver. Determines the view under the mouse and + /// calls the appropriate View mouse event handlers. + /// + /// This method can be used to simulate a mouse event, e.g. in unit tests. + /// The mouse event with coordinates relative to the screen. + internal static void RaiseMouseEvent (MouseEventArgs mouseEvent) { Mouse.RaiseMouseEvent (mouseEvent); } + + /// + /// INTERNAL: Clears mouse state during application reset. + /// + internal static void ResetMouseState () { Mouse.ResetState (); } } diff --git a/Terminal.Gui/App/Application.Run.cs b/Terminal.Gui/App/Application.Run.cs index 9d5059c07..19a394d9c 100644 --- a/Terminal.Gui/App/Application.Run.cs +++ b/Terminal.Gui/App/Application.Run.cs @@ -4,7 +4,7 @@ using System.Diagnostics.CodeAnalysis; namespace Terminal.Gui.App; -public static partial class Application // Run (Begin, Run, End, Stop) +public static partial class Application // Run (Begin -> Run -> Layout/Draw -> End -> Stop) { /// Gets or sets the key to quit the application. [ConfigurationProperty (Scope = typeof (SettingsScope))] @@ -71,9 +71,9 @@ public static partial class Application // Run (Begin, Run, End, Stop) //#endif // Ensure the mouse is ungrabbed. - if (MouseGrabHandler.MouseGrabView is { }) + if (Mouse.MouseGrabView is { }) { - MouseGrabHandler.UngrabMouse (); + Mouse.UngrabMouse (); } var rs = new RunState (toplevel); @@ -187,7 +187,7 @@ public static partial class Application // Run (Begin, Run, End, Stop) toplevel.OnLoaded (); - LayoutAndDraw (true); + ApplicationImpl.Instance.LayoutAndDraw (true); if (PositionCursor ()) { @@ -406,40 +406,15 @@ public static partial class Application // Run (Begin, Run, End, Stop) /// If the entire View hierarchy will be redrawn. The default is and /// should only be overriden for testing. /// - public static void LayoutAndDraw (bool forceDraw = false) + public static void LayoutAndDraw (bool forceRedraw = false) { - List tops = [.. TopLevels]; - - if (Popover?.GetActivePopover () as View is { Visible: true } visiblePopover) - { - visiblePopover.SetNeedsDraw (); - visiblePopover.SetNeedsLayout (); - tops.Insert (0, visiblePopover); - } - - bool neededLayout = View.Layout (tops.ToArray ().Reverse (), Screen.Size); - - if (ClearScreenNextIteration) - { - forceDraw = true; - ClearScreenNextIteration = false; - } - - if (forceDraw) - { - Driver?.ClearContents (); - } - - View.SetClipToScreen (); - View.Draw (tops, neededLayout || forceDraw); - View.SetClipToScreen (); - Driver?.Refresh (); + ApplicationImpl.Instance.LayoutAndDraw (forceRedraw); } /// 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; set; } diff --git a/Terminal.Gui/App/Application.Screen.cs b/Terminal.Gui/App/Application.Screen.cs index 1ceed1d7c..0d9a932c5 100644 --- a/Terminal.Gui/App/Application.Screen.cs +++ b/Terminal.Gui/App/Application.Screen.cs @@ -2,7 +2,7 @@ namespace Terminal.Gui.App; -public static partial class Application // Screen related stuff +public static partial class Application // Screen related stuff; intended to hide Driver details { private static readonly object _lockScreen = new (); private static Rectangle? _screen; diff --git a/Terminal.Gui/App/Application.cs b/Terminal.Gui/App/Application.cs index e7feaddef..10c57beed 100644 --- a/Terminal.Gui/App/Application.cs +++ b/Terminal.Gui/App/Application.cs @@ -242,16 +242,15 @@ public static partial class Application // Run State stuff NotifyNewRunState = null; NotifyStopRunState = null; - MouseGrabHandler = new MouseGrabHandler (); - // Keyboard will be lazy-initialized in ApplicationImpl on next access + // Mouse and Keyboard will be lazy-initialized in ApplicationImpl on next access Initialized = false; // Mouse - // Do not clear _lastMousePosition; Popover's require it to stay set with + // Do not clear _lastMousePosition; Popovers require it to stay set with // last mouse pos. //_lastMousePosition = null; CachedViewsUnderMouse.Clear (); - MouseEvent = null; + ResetMouseState (); // Keyboard events and bindings are now managed by the Keyboard instance diff --git a/Terminal.Gui/App/ApplicationImpl.cs b/Terminal.Gui/App/ApplicationImpl.cs index 9d0c89a62..7d2b69135 100644 --- a/Terminal.Gui/App/ApplicationImpl.cs +++ b/Terminal.Gui/App/ApplicationImpl.cs @@ -3,39 +3,52 @@ using System.Collections.Concurrent; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; -using Terminal.Gui.Drivers; namespace Terminal.Gui.App; /// -/// Implementation of core methods using the modern -/// main loop architecture with component factories for different platforms. +/// Implementation of core methods using the modern +/// main loop architecture with component factories for different platforms. /// public class ApplicationImpl : IApplication { - private readonly IComponentFactory? _componentFactory; - private IMainLoopCoordinator? _coordinator; - private string? _driverName; - private readonly ITimedEvents _timedEvents = new TimedEvents (); - // Private static readonly Lazy instance of Application private static Lazy _lazyInstance = new (() => new ApplicationImpl ()); /// - /// Gets the currently configured backend implementation of gateway methods. - /// Change to your own implementation by using (before init). + /// Creates a new instance of the Application backend. /// - public static IApplication Instance => _lazyInstance.Value; + public ApplicationImpl () { } + + internal ApplicationImpl (IComponentFactory componentFactory) + { + _componentFactory = componentFactory; + } + + private readonly IComponentFactory? _componentFactory; + private readonly ITimedEvents _timedEvents = new TimedEvents (); + private string? _driverName; /// public ITimedEvents? TimedEvents => _timedEvents; - internal IMainLoopCoordinator? Coordinator => _coordinator; + private IMouse? _mouse; /// - /// Handles which (if any) has captured the mouse + /// Handles mouse event state and processing. /// - public IMouseGrabHandler MouseGrabHandler { get; set; } = new MouseGrabHandler (); + public IMouse Mouse + { + get + { + if (_mouse is null) + { + _mouse = new MouseImpl { Application = this }; + } + return _mouse; + } + set => _mouse = value ?? throw new ArgumentNullException (nameof (value)); + } private IKeyboard? _keyboard; @@ -48,7 +61,7 @@ public class ApplicationImpl : IApplication { if (_keyboard is null) { - _keyboard = new Keyboard { Application = this }; + _keyboard = new KeyboardImpl { Application = this }; } return _keyboard; } @@ -83,6 +96,7 @@ public class ApplicationImpl : IApplication set => Application.Navigation = value; } + // TODO: Create an IViewHierarchy that encapsulates Top and TopLevels and LayoutAndDraw /// public Toplevel? Top { @@ -93,30 +107,38 @@ public class ApplicationImpl : IApplication /// public ConcurrentStack TopLevels => Application.TopLevels; - /// - public void RequestStop () => Application.RequestStop (); - - /// - /// Creates a new instance of the Application backend. - /// - public ApplicationImpl () + /// + public void LayoutAndDraw (bool forceRedraw = false) { - } + List tops = [.. TopLevels]; - internal ApplicationImpl (IComponentFactory componentFactory) - { - _componentFactory = componentFactory; - } + if (Popover?.GetActivePopover () as View is { Visible: true } visiblePopover) + { + visiblePopover.SetNeedsDraw (); + visiblePopover.SetNeedsLayout (); + tops.Insert (0, visiblePopover); + } - /// - /// Change the singleton implementation, should not be called except before application - /// startup. This method lets you provide alternative implementations of core static gateway - /// methods of . - /// - /// - public static void ChangeInstance (IApplication newApplication) - { - _lazyInstance = new Lazy (newApplication); + // BUGBUG: Application.Screen needs to be moved to IApplication + bool neededLayout = View.Layout (tops.ToArray ().Reverse (), Application.Screen.Size); + + // BUGBUG: Application.ClearScreenNextIteration needs to be moved to IApplication + if (Application.ClearScreenNextIteration) + { + forceRedraw = true; + // BUGBUG: Application.Screen needs to be moved to IApplication + Application.ClearScreenNextIteration = false; + } + + if (forceRedraw) + { + Driver?.ClearContents (); + } + + View.SetClipToScreen (); + View.Draw (tops, neededLayout || forceRedraw); + View.SetClipToScreen (); + Driver?.Refresh (); } /// @@ -141,12 +163,13 @@ public class ApplicationImpl : IApplication _driverName = Application.ForceDriver; } - Debug.Assert(Application.Navigation is null); + Debug.Assert (Application.Navigation is null); Application.Navigation = new (); Debug.Assert (Application.Popover is null); Application.Popover = new (); + // TODO: Move this into IKeyboard and Keyboard implementation // Preserve existing keyboard settings if they exist bool hasExistingKeyboard = _keyboard is not null; Key existingQuitKey = _keyboard?.QuitKey ?? Key.Esc; @@ -157,7 +180,7 @@ public class ApplicationImpl : IApplication Key existingPrevTabGroupKey = _keyboard?.PrevTabGroupKey ?? Key.F6.WithShift; // Reset keyboard to ensure fresh state with default bindings - _keyboard = new Keyboard { Application = this }; + _keyboard = new KeyboardImpl { Application = this }; // Restore previously set keys if they existed and were different from defaults if (hasExistingKeyboard) @@ -181,92 +204,6 @@ public class ApplicationImpl : IApplication Application.MainThreadId = Thread.CurrentThread.ManagedThreadId; } - private void CreateDriver (string? driverName) - { - // When running unit tests, always use FakeDriver unless explicitly specified - if (ConsoleDriver.RunningUnitTests && - string.IsNullOrEmpty (driverName) && - _componentFactory is null) - { - Logging.Logger.LogDebug ("Unit test safeguard: forcing FakeDriver (RunningUnitTests=true, driverName=null, componentFactory=null)"); - _coordinator = CreateSubcomponents (() => new FakeComponentFactory ()); - _coordinator.StartAsync ().Wait (); - - if (Application.Driver == null) - { - throw new ("Application.Driver was null even after booting MainLoopCoordinator"); - } - - return; - } - - PlatformID p = Environment.OSVersion.Platform; - - // Check component factory type first - this takes precedence over driverName - bool factoryIsWindows = _componentFactory is IComponentFactory; - bool factoryIsDotNet = _componentFactory is IComponentFactory; - bool factoryIsUnix = _componentFactory is IComponentFactory; - bool factoryIsFake = _componentFactory is IComponentFactory; - - // Then check driverName - bool nameIsWindows = driverName?.Contains ("win", StringComparison.OrdinalIgnoreCase) ?? false; - bool nameIsDotNet = (driverName?.Contains ("dotnet", StringComparison.OrdinalIgnoreCase) ?? false); - bool nameIsUnix = driverName?.Contains ("unix", StringComparison.OrdinalIgnoreCase) ?? false; - bool nameIsFake = driverName?.Contains ("fake", StringComparison.OrdinalIgnoreCase) ?? false; - - // Decide which driver to use - component factory type takes priority - if (factoryIsFake || (!factoryIsWindows && !factoryIsDotNet && !factoryIsUnix && nameIsFake)) - { - _coordinator = CreateSubcomponents (() => new FakeComponentFactory ()); - } - else if (factoryIsWindows || (!factoryIsDotNet && !factoryIsUnix && nameIsWindows)) - { - _coordinator = CreateSubcomponents (() => new WindowsComponentFactory ()); - } - else if (factoryIsDotNet || (!factoryIsWindows && !factoryIsUnix && nameIsDotNet)) - { - _coordinator = CreateSubcomponents (() => new NetComponentFactory ()); - } - else if (factoryIsUnix || (!factoryIsWindows && !factoryIsDotNet && nameIsUnix)) - { - _coordinator = CreateSubcomponents (() => new UnixComponentFactory ()); - } - else if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows) - { - _coordinator = CreateSubcomponents (() => new WindowsComponentFactory ()); - } - else - { - _coordinator = CreateSubcomponents (() => new UnixComponentFactory ()); - } - - _coordinator.StartAsync ().Wait (); - - if (Application.Driver == null) - { - throw new ("Application.Driver was null even after booting MainLoopCoordinator"); - } - } - - private IMainLoopCoordinator CreateSubcomponents (Func> fallbackFactory) - { - ConcurrentQueue inputBuffer = new (); - ApplicationMainLoop loop = new (); - - IComponentFactory cf; - - if (_componentFactory is IComponentFactory typedFactory) - { - cf = typedFactory; - } - else - { - cf = fallbackFactory (); - } - - return new MainLoopCoordinator (_timedEvents, inputBuffer, loop, cf); - } - /// /// Runs the application by creating a object and calling /// . @@ -294,11 +231,12 @@ public class ApplicationImpl : IApplication if (!Application.Initialized) { // Init() has NOT been called. Auto-initialize as per interface contract. - Init (driver, null); + Init (driver); } T top = new (); Run (top, errorHandler); + return top; } @@ -317,7 +255,7 @@ public class ApplicationImpl : IApplication if (Application.Driver == null) { - throw new InvalidOperationException ("Driver was inexplicably null when trying to Run view"); + throw new InvalidOperationException ("Driver was inexplicably null when trying to Run view"); } Application.Top = view; @@ -328,23 +266,23 @@ public class ApplicationImpl : IApplication while (Application.TopLevels.TryPeek (out Toplevel? found) && found == view && view.Running) { - if (_coordinator is null) + if (Coordinator is null) { throw new ($"{nameof (IMainLoopCoordinator)} inexplicably became null during Run"); } - _coordinator.RunIteration (); + Coordinator.RunIteration (); } - Logging.Information ($"Run - Calling End"); + Logging.Information ("Run - Calling End"); Application.End (rs); } /// Shutdown an application initialized with . public void Shutdown () { - _coordinator?.Stop (); - + Coordinator?.Stop (); + bool wasInitialized = Application.Initialized; Application.ResetState (); ConfigurationManager.PrintJsonErrors (); @@ -360,10 +298,10 @@ public class ApplicationImpl : IApplication _lazyInstance = new (() => new ApplicationImpl ()); } - /// + /// public void RequestStop (Toplevel? top) { - Logging.Logger.LogInformation ($"RequestStop '{(top is {} ? top : "null")}'"); + Logging.Logger.LogInformation ($"RequestStop '{(top is { } ? top : "null")}'"); top ??= Application.Top; @@ -383,38 +321,138 @@ public class ApplicationImpl : IApplication top.Running = false; } - /// + /// + public void RequestStop () => Application.RequestStop (); + + + /// public void Invoke (Action action) { // If we are already on the main UI thread if (Application.MainThreadId == Thread.CurrentThread.ManagedThreadId) { action (); + return; } - _timedEvents.Add (TimeSpan.Zero, - () => - { - action (); - return false; - } - ); + _timedEvents.Add ( + TimeSpan.Zero, + () => + { + action (); + + return false; + } + ); } - /// + /// public bool IsLegacy => false; - /// + /// public object AddTimeout (TimeSpan time, Func callback) { return _timedEvents.Add (time, callback); } - /// + /// public bool RemoveTimeout (object token) { return _timedEvents.Remove (token); } - /// - public void LayoutAndDraw (bool forceDraw) + /// + /// Change the singleton implementation, should not be called except before application + /// startup. This method lets you provide alternative implementations of core static gateway + /// methods of . + /// + /// + public static void ChangeInstance (IApplication newApplication) { _lazyInstance = new (newApplication); } + + /// + /// Gets the currently configured backend implementation of gateway methods. + /// Change to your own implementation by using (before init). + /// + public static IApplication Instance => _lazyInstance.Value; + + internal IMainLoopCoordinator? Coordinator { get; private set; } + + private void CreateDriver (string? driverName) { - Application.Top?.SetNeedsDraw(); - Application.Top?.SetNeedsLayout (); + // When running unit tests, always use FakeDriver unless explicitly specified + if (ConsoleDriver.RunningUnitTests && string.IsNullOrEmpty (driverName) && _componentFactory is null) + { + Logging.Logger.LogDebug ("Unit test safeguard: forcing FakeDriver (RunningUnitTests=true, driverName=null, componentFactory=null)"); + Coordinator = CreateSubcomponents (() => new FakeComponentFactory ()); + Coordinator.StartAsync ().Wait (); + + if (Application.Driver == null) + { + throw new ("Application.Driver was null even after booting MainLoopCoordinator"); + } + + return; + } + + PlatformID p = Environment.OSVersion.Platform; + + // Check component factory type first - this takes precedence over driverName + bool factoryIsWindows = _componentFactory is IComponentFactory; + bool factoryIsDotNet = _componentFactory is IComponentFactory; + bool factoryIsUnix = _componentFactory is IComponentFactory; + bool factoryIsFake = _componentFactory is IComponentFactory; + + // Then check driverName + bool nameIsWindows = driverName?.Contains ("win", StringComparison.OrdinalIgnoreCase) ?? false; + bool nameIsDotNet = driverName?.Contains ("dotnet", StringComparison.OrdinalIgnoreCase) ?? false; + bool nameIsUnix = driverName?.Contains ("unix", StringComparison.OrdinalIgnoreCase) ?? false; + bool nameIsFake = driverName?.Contains ("fake", StringComparison.OrdinalIgnoreCase) ?? false; + + // Decide which driver to use - component factory type takes priority + if (factoryIsFake || (!factoryIsWindows && !factoryIsDotNet && !factoryIsUnix && nameIsFake)) + { + Coordinator = CreateSubcomponents (() => new FakeComponentFactory ()); + } + else if (factoryIsWindows || (!factoryIsDotNet && !factoryIsUnix && nameIsWindows)) + { + Coordinator = CreateSubcomponents (() => new WindowsComponentFactory ()); + } + else if (factoryIsDotNet || (!factoryIsWindows && !factoryIsUnix && nameIsDotNet)) + { + Coordinator = CreateSubcomponents (() => new NetComponentFactory ()); + } + else if (factoryIsUnix || (!factoryIsWindows && !factoryIsDotNet && nameIsUnix)) + { + Coordinator = CreateSubcomponents (() => new UnixComponentFactory ()); + } + else if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows) + { + Coordinator = CreateSubcomponents (() => new WindowsComponentFactory ()); + } + else + { + Coordinator = CreateSubcomponents (() => new UnixComponentFactory ()); + } + + Coordinator.StartAsync ().Wait (); + + if (Application.Driver == null) + { + throw new ("Application.Driver was null even after booting MainLoopCoordinator"); + } + } + + private IMainLoopCoordinator CreateSubcomponents (Func> fallbackFactory) + { + ConcurrentQueue inputBuffer = new (); + ApplicationMainLoop loop = new (); + + IComponentFactory cf; + + if (_componentFactory is IComponentFactory typedFactory) + { + cf = typedFactory; + } + else + { + cf = fallbackFactory (); + } + + return new MainLoopCoordinator (_timedEvents, inputBuffer, loop, cf); } } diff --git a/Terminal.Gui/App/IApplication.cs b/Terminal.Gui/App/IApplication.cs index 1b343a1a8..44f058855 100644 --- a/Terminal.Gui/App/IApplication.cs +++ b/Terminal.Gui/App/IApplication.cs @@ -4,27 +4,29 @@ using System.Diagnostics.CodeAnalysis; namespace Terminal.Gui.App; /// -/// Interface for instances that provide backing functionality to static -/// gateway class . +/// Interface for instances that provide backing functionality to static +/// gateway class . /// public interface IApplication { - /// - /// Handles recurring events. These are invoked on the main UI thread - allowing for - /// safe updates to instances. - /// - ITimedEvents? TimedEvents { get; } - - /// - /// Handles grabbing the mouse (only a single can grab the mouse at once). - /// - IMouseGrabHandler MouseGrabHandler { get; set; } + /// 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 . + /// + object AddTimeout (TimeSpan time, Func callback); /// /// Handles keyboard input and key bindings at the Application level. /// IKeyboard Keyboard { get; set; } + /// + /// Handles mouse event state and processing. + /// + IMouse Mouse { get; set; } + /// Gets or sets the console driver being used. IConsoleDriver? Driver { get; set; } @@ -46,9 +48,16 @@ public interface IApplication /// Requests that the application stop running. void RequestStop (); - /// Forces all views to be laid out and drawn. - /// If true, clears the screen before drawing. - void LayoutAndDraw (bool clearScreen = false); + /// + /// Causes any Toplevels that need layout to be laid out. Then draws any Toplevels that need display. Only Views that + /// need to be laid out (see ) will be laid out. + /// Only Views that need to be drawn (see ) will be drawn. + /// + /// + /// If the entire View hierarchy will be redrawn. The default is and + /// should only be overriden for testing. + /// + public void LayoutAndDraw (bool forceRedraw = false); /// Initializes a new instance of Application. /// Call this method once per instance (or after has been called). @@ -82,6 +91,39 @@ public interface IApplication [RequiresDynamicCode ("AOT")] public void Init (IConsoleDriver? driver = null, string? driverName = null); + /// Runs on the main UI loop thread + /// the action to be invoked on the main processing thread. + void Invoke (Action action); + + /// + /// if implementation is 'old'. if implementation + /// is cutting edge. + /// + bool IsLegacy { get; } + + /// Removes a previously scheduled timeout + /// The token parameter is the value returned by . + /// + /// + /// if the timeout is successfully removed; otherwise, + /// + /// . + /// This method also returns + /// + /// if the timeout is not found. + /// + bool RemoveTimeout (object token); + + /// 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. + /// + /// + void RequestStop (Toplevel? top); /// /// Runs the application by creating a object and calling @@ -126,7 +168,7 @@ public interface IApplication [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] public T Run (Func? errorHandler = null, IConsoleDriver? driver = null) - where T : Toplevel, new (); + where T : Toplevel, new(); /// Runs the Application using the provided view. /// @@ -140,24 +182,28 @@ public interface IApplication /// /// /// Calling is equivalent to calling - /// , followed by , and then 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 handlers and then + /// method will only process any pending events, timers handlers and + /// then /// return control immediately. /// - /// When using or + /// + /// 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 + /// returns the will resume; otherwise this + /// method will /// exit. /// /// @@ -177,44 +223,9 @@ public interface IApplication /// public void Shutdown (); - /// 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. - /// - /// - void RequestStop (Toplevel? top); - - /// Runs on the main UI loop thread - /// the action to be invoked on the main processing thread. - void Invoke (Action action); - /// - /// if implementation is 'old'. if implementation - /// is cutting edge. + /// Handles recurring events. These are invoked on the main UI thread - allowing for + /// safe updates to instances. /// - bool IsLegacy { get; } - - /// 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 . - /// - object AddTimeout (TimeSpan time, Func callback); - - /// Removes a previously scheduled timeout - /// The token parameter is the value returned by . - /// - /// - /// if the timeout is successfully removed; otherwise, - /// - /// . - /// This method also returns - /// - /// if the timeout is not found. - bool RemoveTimeout (object token); -} \ No newline at end of file + ITimedEvents? TimedEvents { get; } +} diff --git a/Terminal.Gui/App/Keyboard/Keyboard.cs b/Terminal.Gui/App/Keyboard/KeyboardImpl.cs similarity index 99% rename from Terminal.Gui/App/Keyboard/Keyboard.cs rename to Terminal.Gui/App/Keyboard/KeyboardImpl.cs index ff7a5f024..05abbb066 100644 --- a/Terminal.Gui/App/Keyboard/Keyboard.cs +++ b/Terminal.Gui/App/Keyboard/KeyboardImpl.cs @@ -11,7 +11,7 @@ namespace Terminal.Gui.App; /// See for usage details. /// /// -internal class Keyboard : IKeyboard +internal class KeyboardImpl : IKeyboard { private Key _quitKey = Key.Esc; // Resources/config.json overrides private Key _arrangeKey = Key.F5.WithCtrl; // Resources/config.json overrides @@ -106,7 +106,7 @@ internal class Keyboard : IKeyboard /// /// Initializes keyboard bindings. /// - public Keyboard () + public KeyboardImpl () { AddKeyBindings (); } diff --git a/Terminal.Gui/App/MainLoop/ApplicationMainLoop.cs b/Terminal.Gui/App/MainLoop/ApplicationMainLoop.cs index 7ed0ecc26..97aff25d6 100644 --- a/Terminal.Gui/App/MainLoop/ApplicationMainLoop.cs +++ b/Terminal.Gui/App/MainLoop/ApplicationMainLoop.cs @@ -150,7 +150,7 @@ public class ApplicationMainLoop : IApplicationMainLoop { bool needsDrawOrLayout = AnySubViewsNeedDrawn (Application.Popover?.GetActivePopover () as View) || AnySubViewsNeedDrawn (Application.Top) - || (Application.MouseGrabHandler.MouseGrabView != null && AnySubViewsNeedDrawn (Application.MouseGrabHandler.MouseGrabView)); + || (Application.Mouse.MouseGrabView != null && AnySubViewsNeedDrawn (Application.Mouse.MouseGrabView)); bool sizeChanged = WindowSizeMonitor.Poll (); diff --git a/Terminal.Gui/App/Mouse/IMouse.cs b/Terminal.Gui/App/Mouse/IMouse.cs new file mode 100644 index 000000000..a4bc2c2e8 --- /dev/null +++ b/Terminal.Gui/App/Mouse/IMouse.cs @@ -0,0 +1,79 @@ +#nullable enable +using System.ComponentModel; + +namespace Terminal.Gui.App; + +/// +/// Defines a contract for mouse event handling and state management in a Terminal.Gui application. +/// +/// This interface allows for decoupling of mouse-related functionality from the static class, +/// enabling better testability and parallel test execution. +/// +/// +public interface IMouse : IMouseGrabHandler +{ + /// + /// Sets the application instance that this mouse handler is associated with. + /// This provides access to application state without coupling to static Application class. + /// + IApplication? Application { get; set; } + + /// + /// Gets or sets the last known position of the mouse. + /// + Point? LastMousePosition { get; set; } + + /// + /// Gets the most recent position of the mouse. + /// + Point? GetLastMousePosition (); + + /// + /// Gets or sets whether the mouse is disabled. The mouse is enabled by default. + /// + bool IsMouseDisabled { get; set; } + + /// + /// Gets the list of non- views that are currently under the mouse. + /// + List CachedViewsUnderMouse { get; } + + /// + /// Raised when a mouse event occurs. Can be cancelled by setting to . + /// + /// + /// + /// coordinates are screen-relative. + /// + /// + /// will be the deepest view under the mouse. + /// + /// + /// coordinates are view-relative. Only valid if is set. + /// + /// + /// Use this even to handle mouse events at the application level, before View-specific handling. + /// + /// + event EventHandler? MouseEvent; + + /// + /// INTERNAL API: Called when a mouse event is raised by the driver. Determines the view under the mouse and + /// calls the appropriate View mouse event handlers. + /// + /// This method can be used to simulate a mouse event, e.g. in unit tests. + /// The mouse event with coordinates relative to the screen. + void RaiseMouseEvent (MouseEventArgs mouseEvent); + + /// + /// INTERNAL: Raises the MouseEnter and MouseLeave events for the views that are under the mouse. + /// + /// The position of the mouse. + /// The most recent result from GetViewsUnderLocation(). + void RaiseMouseEnterLeaveEvents (Point screenPosition, List currentViewsUnderMouse); + + /// + /// INTERNAL: Resets mouse state, clearing event handlers and cached views. + /// + void ResetState (); +} diff --git a/Terminal.Gui/App/Mouse/IMouseGrabHandler.cs b/Terminal.Gui/App/Mouse/IMouseGrabHandler.cs index 026811e15..06fd0e626 100644 --- a/Terminal.Gui/App/Mouse/IMouseGrabHandler.cs +++ b/Terminal.Gui/App/Mouse/IMouseGrabHandler.cs @@ -84,4 +84,12 @@ public interface IMouseGrabHandler /// Releases the mouse grab, so mouse events will be routed to the view under the mouse pointer. /// public void UngrabMouse (); + + /// + /// Handles mouse grab logic for a mouse event. + /// + /// The deepest view under the mouse. + /// The mouse event to handle. + /// if the event was handled by the grab handler; otherwise . + bool HandleMouseGrab (View? deepestViewUnderMouse, MouseEventArgs mouseEvent); } diff --git a/Terminal.Gui/App/Mouse/MouseGrabHandler.cs b/Terminal.Gui/App/Mouse/MouseGrabHandler.cs index 65274d815..3fe7ab689 100644 --- a/Terminal.Gui/App/Mouse/MouseGrabHandler.cs +++ b/Terminal.Gui/App/Mouse/MouseGrabHandler.cs @@ -115,4 +115,45 @@ internal class MouseGrabHandler : IMouseGrabHandler UnGrabbedMouse?.Invoke (view, new (view)); } + + /// + public bool HandleMouseGrab (View? deepestViewUnderMouse, MouseEventArgs mouseEvent) + { + if (MouseGrabView is { }) + { +#if DEBUG_IDISPOSABLE + if (View.EnableDebugIDisposableAsserts && MouseGrabView.WasDisposed) + { + throw new ObjectDisposedException (MouseGrabView.GetType ().FullName); + } +#endif + + // If the mouse is grabbed, send the event to the view that grabbed it. + // The coordinates are relative to the Bounds of the view that grabbed the mouse. + Point frameLoc = MouseGrabView.ScreenToViewport (mouseEvent.ScreenPosition); + + var viewRelativeMouseEvent = new MouseEventArgs + { + Position = frameLoc, + Flags = mouseEvent.Flags, + ScreenPosition = mouseEvent.ScreenPosition, + View = deepestViewUnderMouse ?? MouseGrabView + }; + + //System.Diagnostics.Debug.WriteLine ($"{nme.Flags};{nme.X};{nme.Y};{mouseGrabView}"); + if (MouseGrabView?.NewMouseEvent (viewRelativeMouseEvent) is true) + { + return true; + } + + // ReSharper disable once ConditionIsAlwaysTrueOrFalse + if (MouseGrabView is null && deepestViewUnderMouse is Adornment) + { + // The view that grabbed the mouse has been disposed + return true; + } + } + + return false; + } } diff --git a/Terminal.Gui/App/Mouse/MouseImpl.cs b/Terminal.Gui/App/Mouse/MouseImpl.cs new file mode 100644 index 000000000..7846f0e6c --- /dev/null +++ b/Terminal.Gui/App/Mouse/MouseImpl.cs @@ -0,0 +1,393 @@ +#nullable enable +using System.ComponentModel; + +namespace Terminal.Gui.App; + +/// +/// INTERNAL: Implements to manage mouse event handling and state. +/// +/// This class holds all mouse-related state that was previously in the static class, +/// enabling better testability and parallel test execution. +/// +/// +internal class MouseImpl : IMouse +{ + /// + /// Initializes a new instance of the class. + /// + public MouseImpl () { } + + /// + public IApplication? Application { get; set; } + + /// + public Point? LastMousePosition { get; set; } + + /// + public Point? GetLastMousePosition () { return LastMousePosition; } + + /// + public bool IsMouseDisabled { get; set; } + + /// + public List CachedViewsUnderMouse { get; } = []; + + /// + public event EventHandler? MouseEvent; + + // Mouse grab functionality merged from MouseGrabHandler + + /// + public View? MouseGrabView { get; private set; } + + /// + public event EventHandler? GrabbingMouse; + + /// + public event EventHandler? UnGrabbingMouse; + + /// + public event EventHandler? GrabbedMouse; + + /// + public event EventHandler? UnGrabbedMouse; + + /// + public void RaiseMouseEvent (MouseEventArgs mouseEvent) + { + if (Application?.Initialized is true) + { + // LastMousePosition is only set if the application is initialized. + LastMousePosition = mouseEvent.ScreenPosition; + } + + if (IsMouseDisabled) + { + return; + } + + // The position of the mouse is the same as the screen position at the application level. + //Debug.Assert (mouseEvent.Position == mouseEvent.ScreenPosition); + mouseEvent.Position = mouseEvent.ScreenPosition; + + List currentViewsUnderMouse = View.GetViewsUnderLocation (mouseEvent.ScreenPosition, ViewportSettingsFlags.TransparentMouse); + + View? deepestViewUnderMouse = currentViewsUnderMouse.LastOrDefault (); + + if (deepestViewUnderMouse is { }) + { +#if DEBUG_IDISPOSABLE + if (View.EnableDebugIDisposableAsserts && deepestViewUnderMouse.WasDisposed) + { + throw new ObjectDisposedException (deepestViewUnderMouse.GetType ().FullName); + } +#endif + mouseEvent.View = deepestViewUnderMouse; + } + + MouseEvent?.Invoke (null, mouseEvent); + + if (mouseEvent.Handled) + { + return; + } + + // Dismiss the Popover if the user presses mouse outside of it + if (mouseEvent.IsPressed + && Application?.Popover?.GetActivePopover () as View is { Visible: true } visiblePopover + && View.IsInHierarchy (visiblePopover, deepestViewUnderMouse, includeAdornments: true) is false) + { + ApplicationPopover.HideWithQuitCommand (visiblePopover); + + // Recurse once so the event can be handled below the popover + RaiseMouseEvent (mouseEvent); + + return; + } + + if (HandleMouseGrab (deepestViewUnderMouse, mouseEvent)) + { + return; + } + + // May be null before the prior condition or the condition may set it as null. + // So, the checking must be outside the prior condition. + if (deepestViewUnderMouse is null) + { + return; + } + + // if the mouse is outside the Application.Top or Application.Popover hierarchy, we don't want to + // send the mouse event to the deepest view under the mouse. + if (!View.IsInHierarchy (Application?.Top, deepestViewUnderMouse, true) && !View.IsInHierarchy (Application?.Popover?.GetActivePopover () as View, deepestViewUnderMouse, true)) + { + return; + } + + // Create a view-relative mouse event to send to the view that is under the mouse. + MouseEventArgs viewMouseEvent; + + if (deepestViewUnderMouse is Adornment adornment) + { + Point frameLoc = adornment.ScreenToFrame (mouseEvent.ScreenPosition); + + viewMouseEvent = new () + { + Position = frameLoc, + Flags = mouseEvent.Flags, + ScreenPosition = mouseEvent.ScreenPosition, + View = deepestViewUnderMouse + }; + } + else if (deepestViewUnderMouse.ViewportToScreen (Rectangle.Empty with { Size = deepestViewUnderMouse.Viewport.Size }).Contains (mouseEvent.ScreenPosition)) + { + Point viewportLocation = deepestViewUnderMouse.ScreenToViewport (mouseEvent.ScreenPosition); + + viewMouseEvent = new () + { + Position = viewportLocation, + Flags = mouseEvent.Flags, + ScreenPosition = mouseEvent.ScreenPosition, + View = deepestViewUnderMouse + }; + } + else + { + // The mouse was outside any View's Viewport. + // Debug.Fail ("This should never happen. If it does please file an Issue!!"); + + return; + } + + RaiseMouseEnterLeaveEvents (viewMouseEvent.ScreenPosition, currentViewsUnderMouse); + + while (deepestViewUnderMouse.NewMouseEvent (viewMouseEvent) is not true && MouseGrabView is not { }) + { + if (deepestViewUnderMouse is Adornment adornmentView) + { + deepestViewUnderMouse = adornmentView.Parent?.SuperView; + } + else + { + deepestViewUnderMouse = deepestViewUnderMouse.SuperView; + } + + if (deepestViewUnderMouse is null) + { + break; + } + + Point boundsPoint = deepestViewUnderMouse.ScreenToViewport (mouseEvent.ScreenPosition); + + viewMouseEvent = new () + { + Position = boundsPoint, + Flags = mouseEvent.Flags, + ScreenPosition = mouseEvent.ScreenPosition, + View = deepestViewUnderMouse + }; + } + } + + /// + public void RaiseMouseEnterLeaveEvents (Point screenPosition, List currentViewsUnderMouse) + { + // Tell any views that are no longer under the mouse that the mouse has left + List viewsToLeave = CachedViewsUnderMouse.Where (v => v is { } && !currentViewsUnderMouse.Contains (v)).ToList (); + + foreach (View? view in viewsToLeave) + { + if (view is null) + { + continue; + } + + view.NewMouseLeaveEvent (); + CachedViewsUnderMouse.Remove (view); + } + + // Tell any views that are now under the mouse that the mouse has entered and add them to the list + foreach (View? view in currentViewsUnderMouse) + { + if (view is null) + { + continue; + } + + if (CachedViewsUnderMouse.Contains (view)) + { + continue; + } + + CachedViewsUnderMouse.Add (view); + var raise = false; + + if (view is Adornment { Parent: { } } adornmentView) + { + Point superViewLoc = adornmentView.Parent.SuperView?.ScreenToViewport (screenPosition) ?? screenPosition; + raise = adornmentView.Contains (superViewLoc); + } + else + { + Point superViewLoc = view.SuperView?.ScreenToViewport (screenPosition) ?? screenPosition; + raise = view.Contains (superViewLoc); + } + + if (!raise) + { + continue; + } + + CancelEventArgs eventArgs = new System.ComponentModel.CancelEventArgs (); + bool? cancelled = view.NewMouseEnterEvent (eventArgs); + + if (cancelled is true || eventArgs.Cancel) + { + break; + } + } + } + + /// + public void ResetState () + { + // Do not clear LastMousePosition; Popover's require it to stay set with last mouse pos. + CachedViewsUnderMouse.Clear (); + MouseEvent = null; + } + + // Mouse grab functionality merged from MouseGrabHandler + + /// + public void GrabMouse (View? view) + { + if (view is null || RaiseGrabbingMouseEvent (view)) + { + return; + } + + RaiseGrabbedMouseEvent (view); + + // MouseGrabView is only set if the application is initialized. + MouseGrabView = view; + } + + /// + public void UngrabMouse () + { + if (MouseGrabView is null) + { + return; + } + +#if DEBUG_IDISPOSABLE + if (View.EnableDebugIDisposableAsserts) + { + ObjectDisposedException.ThrowIf (MouseGrabView.WasDisposed, MouseGrabView); + } +#endif + + if (!RaiseUnGrabbingMouseEvent (MouseGrabView)) + { + View view = MouseGrabView; + MouseGrabView = null; + RaiseUnGrabbedMouseEvent (view); + } + } + + /// A delegate callback throws an exception. + private bool RaiseGrabbingMouseEvent (View? view) + { + if (view is null) + { + return false; + } + + GrabMouseEventArgs evArgs = new (view); + GrabbingMouse?.Invoke (view, evArgs); + + return evArgs.Cancel; + } + + /// A delegate callback throws an exception. + private bool RaiseUnGrabbingMouseEvent (View? view) + { + if (view is null) + { + return false; + } + + GrabMouseEventArgs evArgs = new (view); + UnGrabbingMouse?.Invoke (view, evArgs); + + return evArgs.Cancel; + } + + /// A delegate callback throws an exception. + private void RaiseGrabbedMouseEvent (View? view) + { + if (view is null) + { + return; + } + + GrabbedMouse?.Invoke (view, new (view)); + } + + /// A delegate callback throws an exception. + private void RaiseUnGrabbedMouseEvent (View? view) + { + if (view is null) + { + return; + } + + UnGrabbedMouse?.Invoke (view, new (view)); + } + + /// + /// Handles mouse grab logic for a mouse event. + /// + /// The deepest view under the mouse. + /// The mouse event to handle. + /// if the event was handled by the grab handler; otherwise . + public bool HandleMouseGrab (View? deepestViewUnderMouse, MouseEventArgs mouseEvent) + { + if (MouseGrabView is { }) + { +#if DEBUG_IDISPOSABLE + if (View.EnableDebugIDisposableAsserts && MouseGrabView.WasDisposed) + { + throw new ObjectDisposedException (MouseGrabView.GetType ().FullName); + } +#endif + + // If the mouse is grabbed, send the event to the view that grabbed it. + // The coordinates are relative to the Bounds of the view that grabbed the mouse. + Point frameLoc = MouseGrabView.ScreenToViewport (mouseEvent.ScreenPosition); + + MouseEventArgs viewRelativeMouseEvent = new () + { + Position = frameLoc, + Flags = mouseEvent.Flags, + ScreenPosition = mouseEvent.ScreenPosition, + View = deepestViewUnderMouse ?? MouseGrabView + }; + + //System.Diagnostics.Debug.WriteLine ($"{nme.Flags};{nme.X};{nme.Y};{mouseGrabView}"); + if (MouseGrabView?.NewMouseEvent (viewRelativeMouseEvent) is true) + { + return true; + } + + // ReSharper disable once ConditionIsAlwaysTrueOrFalse + if (MouseGrabView is null && deepestViewUnderMouse is Adornment) + { + // The view that grabbed the mouse has been disposed + return true; + } + } + + return false; + } +} diff --git a/Terminal.Gui/Drivers/IConsoleDriver.cs b/Terminal.Gui/Drivers/IConsoleDriver.cs index 39e249c1e..1c42be49b 100644 --- a/Terminal.Gui/Drivers/IConsoleDriver.cs +++ b/Terminal.Gui/Drivers/IConsoleDriver.cs @@ -4,9 +4,7 @@ namespace Terminal.Gui.Drivers; /// Base interface for Terminal.Gui ConsoleDriver implementations. /// -/// There are currently four implementations: - (for Unix and Mac) - -/// - that uses the .NET Console API - -/// for unit testing. +/// There are currently four implementations: UnixDriver, WindowsDriver, DotNetDriver, and FakeDriver /// public interface IConsoleDriver { diff --git a/Terminal.Gui/ViewBase/Adornment/Border.Arrangment.cs b/Terminal.Gui/ViewBase/Adornment/Border.Arrangment.cs index 546c871c7..999011e3e 100644 --- a/Terminal.Gui/ViewBase/Adornment/Border.Arrangment.cs +++ b/Terminal.Gui/ViewBase/Adornment/Border.Arrangment.cs @@ -431,9 +431,9 @@ public partial class Border Application.MouseEvent -= ApplicationOnMouseEvent; - if (Application.MouseGrabHandler.MouseGrabView == this && _dragPosition.HasValue) + if (Application.Mouse.MouseGrabView == this && _dragPosition.HasValue) { - Application.MouseGrabHandler.UngrabMouse (); + Application.Mouse.UngrabMouse (); } // Clean up all arrangement buttons @@ -498,7 +498,7 @@ public partial class Border // Set the start grab point to the Frame coords _startGrabPoint = new (mouseEvent.Position.X + Frame.X, mouseEvent.Position.Y + Frame.Y); _dragPosition = mouseEvent.Position; - Application.MouseGrabHandler.GrabMouse (this); + Application.Mouse.GrabMouse (this); // Determine the mode based on where the click occurred ViewArrangement arrangeMode = DetermineArrangeModeFromClick (); @@ -511,7 +511,7 @@ public partial class Border return true; } - if (mouseEvent.Flags is (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition) && Application.MouseGrabHandler.MouseGrabView == this) + if (mouseEvent.Flags is (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition) && Application.Mouse.MouseGrabView == this) { if (_dragPosition.HasValue) { @@ -523,7 +523,7 @@ public partial class Border if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Released) && _dragPosition.HasValue) { _dragPosition = null; - Application.MouseGrabHandler.UngrabMouse (); + Application.Mouse.UngrabMouse (); EndArrangeMode (); @@ -763,7 +763,7 @@ public partial class Border private void Application_GrabbingMouse (object? sender, GrabMouseEventArgs e) { - if (Application.MouseGrabHandler.MouseGrabView == this && _dragPosition.HasValue) + if (Application.Mouse.MouseGrabView == this && _dragPosition.HasValue) { e.Cancel = true; } @@ -771,7 +771,7 @@ public partial class Border private void Application_UnGrabbingMouse (object? sender, GrabMouseEventArgs e) { - if (Application.MouseGrabHandler.MouseGrabView == this && _dragPosition.HasValue) + if (Application.Mouse.MouseGrabView == this && _dragPosition.HasValue) { e.Cancel = true; } @@ -784,8 +784,8 @@ public partial class Border /// protected override void Dispose (bool disposing) { - Application.MouseGrabHandler.GrabbingMouse -= Application_GrabbingMouse; - Application.MouseGrabHandler.UnGrabbingMouse -= Application_UnGrabbingMouse; + Application.Mouse.GrabbingMouse -= Application_GrabbingMouse; + Application.Mouse.UnGrabbingMouse -= Application_UnGrabbingMouse; _dragPosition = null; base.Dispose (disposing); diff --git a/Terminal.Gui/ViewBase/Adornment/Border.cs b/Terminal.Gui/ViewBase/Adornment/Border.cs index b03645bb6..b092aae1d 100644 --- a/Terminal.Gui/ViewBase/Adornment/Border.cs +++ b/Terminal.Gui/ViewBase/Adornment/Border.cs @@ -50,8 +50,8 @@ public partial class Border : Adornment CanFocus = false; TabStop = TabBehavior.TabGroup; - Application.MouseGrabHandler.GrabbingMouse += Application_GrabbingMouse; - Application.MouseGrabHandler.UnGrabbingMouse += Application_UnGrabbingMouse; + Application.Mouse.GrabbingMouse += Application_GrabbingMouse; + Application.Mouse.UnGrabbingMouse += Application_UnGrabbingMouse; ThicknessChanged += OnThicknessChanged; } diff --git a/Terminal.Gui/ViewBase/View.Mouse.cs b/Terminal.Gui/ViewBase/View.Mouse.cs index 47de711d5..546017059 100644 --- a/Terminal.Gui/ViewBase/View.Mouse.cs +++ b/Terminal.Gui/ViewBase/View.Mouse.cs @@ -16,7 +16,7 @@ public partial class View // Mouse APIs private void SetupMouse () { - MouseHeldDown = new MouseHeldDown (this, Application.TimedEvents,Application.MouseGrabHandler); + MouseHeldDown = new MouseHeldDown (this, Application.TimedEvents,Application.Mouse); MouseBindings = new (); // TODO: Should the default really work with any button or just button1? @@ -375,7 +375,7 @@ public partial class View // Mouse APIs if (mouseEvent.IsReleased) { - if (Application.MouseGrabHandler.MouseGrabView == this) + if (Application.Mouse.MouseGrabView == this) { //Logging.Debug ($"{Id} - {MouseState}"); MouseState &= ~MouseState.Pressed; @@ -407,9 +407,9 @@ public partial class View // Mouse APIs if (mouseEvent.IsPressed) { // The first time we get pressed event, grab the mouse and set focus - if (Application.MouseGrabHandler.MouseGrabView != this) + if (Application.Mouse.MouseGrabView != this) { - Application.MouseGrabHandler.GrabMouse (this); + Application.Mouse.GrabMouse (this); if (!HasFocus && CanFocus) { @@ -541,10 +541,10 @@ public partial class View // Mouse APIs { mouseEvent.Handled = false; - if (Application.MouseGrabHandler.MouseGrabView == this && mouseEvent.IsSingleClicked) + if (Application.Mouse.MouseGrabView == this && mouseEvent.IsSingleClicked) { // We're grabbed. Clicked event comes after the last Release. This is our signal to ungrab - Application.MouseGrabHandler.UngrabMouse (); + Application.Mouse.UngrabMouse (); // TODO: Prove we need to unset MouseState.Pressed and MouseState.PressedOutside here // TODO: There may be perf gains if we don't unset these flags here diff --git a/Terminal.Gui/ViewBase/View.cs b/Terminal.Gui/ViewBase/View.cs index 27c0780f7..a86589750 100644 --- a/Terminal.Gui/ViewBase/View.cs +++ b/Terminal.Gui/ViewBase/View.cs @@ -72,9 +72,9 @@ public partial class View : IDisposable, ISupportInitializeNotification DisposeAdornments (); DisposeScrollBars (); - if (Application.MouseGrabHandler.MouseGrabView == this) + if (Application.Mouse.MouseGrabView == this) { - Application.MouseGrabHandler.UngrabMouse (); + Application.Mouse.UngrabMouse (); } for (int i = InternalSubViews.Count - 1; i >= 0; i--) diff --git a/Terminal.Gui/Views/Autocomplete/PopupAutocomplete.cs b/Terminal.Gui/Views/Autocomplete/PopupAutocomplete.cs index 2c07f1aba..602a849d7 100644 --- a/Terminal.Gui/Views/Autocomplete/PopupAutocomplete.cs +++ b/Terminal.Gui/Views/Autocomplete/PopupAutocomplete.cs @@ -125,7 +125,7 @@ public abstract partial class PopupAutocomplete : AutocompleteBase { Visible = true; HostControl?.SetNeedsDraw (); - Application.MouseGrabHandler.UngrabMouse (); + Application.Mouse.UngrabMouse (); return false; } diff --git a/Terminal.Gui/Views/ComboBox.cs b/Terminal.Gui/Views/ComboBox.cs index 6fa45e276..6da8acaea 100644 --- a/Terminal.Gui/Views/ComboBox.cs +++ b/Terminal.Gui/Views/ComboBox.cs @@ -958,7 +958,7 @@ public class ComboBox : View, IDesignable { _isFocusing = true; _highlighted = _container.SelectedItem; - Application.MouseGrabHandler.GrabMouse (this); + Application.Mouse.GrabMouse (this); } } else @@ -967,7 +967,7 @@ public class ComboBox : View, IDesignable { _isFocusing = false; _highlighted = _container.SelectedItem; - Application.MouseGrabHandler.UngrabMouse (); + Application.Mouse.UngrabMouse (); } } } diff --git a/Terminal.Gui/Views/Menuv1/Menu.cs b/Terminal.Gui/Views/Menuv1/Menu.cs index c4579aab3..a079418f1 100644 --- a/Terminal.Gui/Views/Menuv1/Menu.cs +++ b/Terminal.Gui/Views/Menuv1/Menu.cs @@ -19,7 +19,7 @@ internal sealed class Menu : View } Application.MouseEvent += Application_RootMouseEvent; - Application.MouseGrabHandler.UnGrabbedMouse += Application_UnGrabbedMouse; + Application.Mouse.UnGrabbedMouse += Application_UnGrabbedMouse; // Things this view knows how to do AddCommand (Command.Up, () => MoveUp ()); @@ -220,7 +220,7 @@ internal sealed class Menu : View return; } - Application.MouseGrabHandler.UngrabMouse (); + Application.Mouse.UngrabMouse (); _host.CloseAllMenus (); Application.LayoutAndDraw (true); @@ -238,7 +238,7 @@ internal sealed class Menu : View } Application.MouseEvent -= Application_RootMouseEvent; - Application.MouseGrabHandler.UnGrabbedMouse -= Application_UnGrabbedMouse; + Application.Mouse.UnGrabbedMouse -= Application_UnGrabbedMouse; base.Dispose (disposing); } @@ -535,7 +535,7 @@ internal sealed class Menu : View private void CloseAllMenus () { - Application.MouseGrabHandler.UngrabMouse (); + Application.Mouse.UngrabMouse (); _host.CloseAllMenus (); } diff --git a/Terminal.Gui/Views/Menuv1/MenuBar.cs b/Terminal.Gui/Views/Menuv1/MenuBar.cs index 615241e57..b08e61911 100644 --- a/Terminal.Gui/Views/Menuv1/MenuBar.cs +++ b/Terminal.Gui/Views/Menuv1/MenuBar.cs @@ -442,12 +442,12 @@ public class MenuBar : View, IDesignable if (_isContextMenuLoading) { - Application.MouseGrabHandler.GrabMouse (_openMenu); + Application.Mouse.GrabMouse (_openMenu); _isContextMenuLoading = false; } else { - Application.MouseGrabHandler.GrabMouse (this); + Application.Mouse.GrabMouse (this); } } @@ -524,16 +524,16 @@ public class MenuBar : View, IDesignable SetNeedsDraw (); - if (Application.MouseGrabHandler.MouseGrabView is { } && Application.MouseGrabHandler.MouseGrabView is MenuBar && Application.MouseGrabHandler.MouseGrabView != this) + if (Application.Mouse.MouseGrabView is { } && Application.Mouse.MouseGrabView is MenuBar && Application.Mouse.MouseGrabView != this) { - var menuBar = Application.MouseGrabHandler.MouseGrabView as MenuBar; + var menuBar = Application.Mouse.MouseGrabView as MenuBar; if (menuBar!.IsMenuOpen) { menuBar.CleanUp (); } } - Application.MouseGrabHandler.UngrabMouse (); + Application.Mouse.UngrabMouse (); _isCleaning = false; } @@ -556,7 +556,7 @@ public class MenuBar : View, IDesignable _selected = -1; } - Application.MouseGrabHandler.UngrabMouse (); + Application.Mouse.UngrabMouse (); } if (OpenCurrentMenu is { }) @@ -622,9 +622,9 @@ public class MenuBar : View, IDesignable _previousFocused.SetFocus (); } - if (Application.MouseGrabHandler.MouseGrabView == _openMenu) + if (Application.Mouse.MouseGrabView == _openMenu) { - Application.MouseGrabHandler.UngrabMouse (); + Application.Mouse.UngrabMouse (); } _openMenu?.Dispose (); _openMenu = null; @@ -652,9 +652,9 @@ public class MenuBar : View, IDesignable if (OpenCurrentMenu is { }) { SuperView?.Remove (OpenCurrentMenu); - if (Application.MouseGrabHandler.MouseGrabView == OpenCurrentMenu) + if (Application.Mouse.MouseGrabView == OpenCurrentMenu) { - Application.MouseGrabHandler.UngrabMouse (); + Application.Mouse.UngrabMouse (); } OpenCurrentMenu.Dispose (); OpenCurrentMenu = null; @@ -845,9 +845,9 @@ public class MenuBar : View, IDesignable if (_openMenu is { }) { SuperView?.Remove (_openMenu); - if (Application.MouseGrabHandler.MouseGrabView == _openMenu) + if (Application.Mouse.MouseGrabView == _openMenu) { - Application.MouseGrabHandler.UngrabMouse (); + Application.Mouse.UngrabMouse (); } _openMenu.Dispose (); _openMenu = null; @@ -935,7 +935,7 @@ public class MenuBar : View, IDesignable Host = this, X = first!.Frame.Left, Y = first.Frame.Top, BarItems = newSubMenu }; last!.Visible = false; - Application.MouseGrabHandler.GrabMouse (OpenCurrentMenu); + Application.Mouse.GrabMouse (OpenCurrentMenu); } OpenCurrentMenu._previousSubFocused = last._previousSubFocused; @@ -1029,9 +1029,9 @@ public class MenuBar : View, IDesignable foreach (Menu item in _openSubMenu) { SuperView?.Remove (item); - if (Application.MouseGrabHandler.MouseGrabView == item) + if (Application.Mouse.MouseGrabView == item) { - Application.MouseGrabHandler.UngrabMouse (); + Application.Mouse.UngrabMouse (); } item.Dispose (); } @@ -1137,7 +1137,7 @@ public class MenuBar : View, IDesignable return false; } - Application.MouseGrabHandler.UngrabMouse (); + Application.Mouse.UngrabMouse (); CloseAllMenus (); Application.LayoutAndDraw (true); _openedByAltKey = true; @@ -1209,15 +1209,15 @@ public class MenuBar : View, IDesignable Point screen = ViewportToScreen (new Point (0, i)); var menu = new Menu { Host = this, X = screen.X, Y = screen.Y, BarItems = mi }; menu.Run (mi.Action); - if (Application.MouseGrabHandler.MouseGrabView == menu) + if (Application.Mouse.MouseGrabView == menu) { - Application.MouseGrabHandler.UngrabMouse (); + Application.Mouse.UngrabMouse (); } menu.Dispose (); } else { - Application.MouseGrabHandler.GrabMouse (this); + Application.Mouse.GrabMouse (this); _selected = i; OpenMenu (i); @@ -1280,9 +1280,9 @@ public class MenuBar : View, IDesignable SuperView!.Remove (menu); _openSubMenu.Remove (menu); - if (Application.MouseGrabHandler.MouseGrabView == menu) + if (Application.Mouse.MouseGrabView == menu) { - Application.MouseGrabHandler.GrabMouse (this); + Application.Mouse.GrabMouse (this); } menu.Dispose (); @@ -1458,9 +1458,9 @@ public class MenuBar : View, IDesignable Point screen = ViewportToScreen (new Point (0, i)); var menu = new Menu { Host = this, X = screen.X, Y = screen.Y, BarItems = Menus [i] }; menu.Run (Menus [i].Action); - if (Application.MouseGrabHandler.MouseGrabView == menu) + if (Application.Mouse.MouseGrabView == menu) { - Application.MouseGrabHandler.UngrabMouse (); + Application.Mouse.UngrabMouse (); } menu.Dispose (); @@ -1535,7 +1535,7 @@ public class MenuBar : View, IDesignable internal bool HandleGrabView (MouseEventArgs me, View current) { - if (Application.MouseGrabHandler.MouseGrabView is { }) + if (Application.Mouse.MouseGrabView is { }) { if (me.View is MenuBar or Menu) { @@ -1546,7 +1546,7 @@ public class MenuBar : View, IDesignable if (me.Flags == MouseFlags.Button1Clicked) { mbar.CleanUp (); - Application.MouseGrabHandler.GrabMouse (me.View); + Application.Mouse.GrabMouse (me.View); } else { @@ -1556,10 +1556,10 @@ public class MenuBar : View, IDesignable } } - if (Application.MouseGrabHandler.MouseGrabView != me.View) + if (Application.Mouse.MouseGrabView != me.View) { View v = me.View; - Application.MouseGrabHandler.GrabMouse (v); + Application.Mouse.GrabMouse (v); return true; } @@ -1567,7 +1567,7 @@ public class MenuBar : View, IDesignable if (me.View != current) { View v = me.View; - Application.MouseGrabHandler.GrabMouse (v); + Application.Mouse.GrabMouse (v); MouseEventArgs nme; if (me.Position.Y > -1) @@ -1599,7 +1599,7 @@ public class MenuBar : View, IDesignable && me.Flags != MouseFlags.ReportMousePosition && me.Flags != 0) { - Application.MouseGrabHandler.UngrabMouse (); + Application.Mouse.UngrabMouse (); if (IsMenuOpen) { @@ -1625,11 +1625,11 @@ public class MenuBar : View, IDesignable MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition ))) { - Application.MouseGrabHandler.GrabMouse (current); + Application.Mouse.GrabMouse (current); } else if (IsMenuOpen && (me.View is MenuBar || me.View is Menu)) { - Application.MouseGrabHandler.GrabMouse (me.View); + Application.Mouse.GrabMouse (me.View); } else { @@ -1645,7 +1645,7 @@ public class MenuBar : View, IDesignable private MenuBar? GetMouseGrabViewInstance (View? view) { - if (view is null || Application.MouseGrabHandler.MouseGrabView is null) + if (view is null || Application.Mouse.MouseGrabView is null) { return null; } @@ -1661,7 +1661,7 @@ public class MenuBar : View, IDesignable hostView = ((Menu)view).Host; } - View grabView = Application.MouseGrabHandler.MouseGrabView; + View grabView = Application.Mouse.MouseGrabView; MenuBar? hostGrabView = null; if (grabView is MenuBar bar) diff --git a/Terminal.Gui/Views/ScrollBar/ScrollSlider.cs b/Terminal.Gui/Views/ScrollBar/ScrollSlider.cs index 6cb7d5433..920f88cce 100644 --- a/Terminal.Gui/Views/ScrollBar/ScrollSlider.cs +++ b/Terminal.Gui/Views/ScrollBar/ScrollSlider.cs @@ -307,9 +307,9 @@ public class ScrollSlider : View, IOrientation, IDesignable { if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed) && _lastLocation == -1) { - if (Application.MouseGrabHandler.MouseGrabView != this) + if (Application.Mouse.MouseGrabView != this) { - Application.MouseGrabHandler.GrabMouse (this); + Application.Mouse.GrabMouse (this); _lastLocation = location; } } @@ -333,9 +333,9 @@ public class ScrollSlider : View, IOrientation, IDesignable { _lastLocation = -1; - if (Application.MouseGrabHandler.MouseGrabView == this) + if (Application.Mouse.MouseGrabView == this) { - Application.MouseGrabHandler.UngrabMouse (); + Application.Mouse.UngrabMouse (); } } diff --git a/Terminal.Gui/Views/Slider/Slider.cs b/Terminal.Gui/Views/Slider/Slider.cs index 4808a939f..b8642686f 100644 --- a/Terminal.Gui/Views/Slider/Slider.cs +++ b/Terminal.Gui/Views/Slider/Slider.cs @@ -1311,7 +1311,7 @@ public class Slider : View, IOrientation { _dragPosition = mouseEvent.Position; _moveRenderPosition = ClampMovePosition ((Point)_dragPosition); - Application.MouseGrabHandler.GrabMouse (this); + Application.Mouse.GrabMouse (this); } SetNeedsDraw (); @@ -1357,7 +1357,7 @@ public class Slider : View, IOrientation || mouseEvent.Flags.HasFlag (MouseFlags.Button1Clicked)) { // End Drag - Application.MouseGrabHandler.UngrabMouse (); + Application.Mouse.UngrabMouse (); _dragPosition = null; _moveRenderPosition = null; diff --git a/Terminal.Gui/Views/TextInput/TextField.cs b/Terminal.Gui/Views/TextInput/TextField.cs index 7d171c093..73c449d81 100644 --- a/Terminal.Gui/Views/TextInput/TextField.cs +++ b/Terminal.Gui/Views/TextInput/TextField.cs @@ -855,16 +855,16 @@ public class TextField : View, IDesignable _isButtonReleased = false; PrepareSelection (x); - if (Application.MouseGrabHandler.MouseGrabView is null) + if (Application.Mouse.MouseGrabView is null) { - Application.MouseGrabHandler.GrabMouse (this); + Application.Mouse.GrabMouse (this); } } else if (ev.Flags == MouseFlags.Button1Released) { _isButtonReleased = true; _isButtonPressed = false; - Application.MouseGrabHandler.UngrabMouse (); + Application.Mouse.UngrabMouse (); } else if (ev.Flags == MouseFlags.Button1DoubleClicked) { @@ -1007,12 +1007,12 @@ public class TextField : View, IDesignable /// protected override void OnHasFocusChanged (bool newHasFocus, View previousFocusedView, View view) { - if (Application.MouseGrabHandler.MouseGrabView is { } && Application.MouseGrabHandler.MouseGrabView == this) + if (Application.Mouse.MouseGrabView is { } && Application.Mouse.MouseGrabView == this) { - Application.MouseGrabHandler.UngrabMouse (); + Application.Mouse.UngrabMouse (); } - //if (SelectedLength != 0 && !(Application.MouseGrabHandler.MouseGrabView is MenuBar)) + //if (SelectedLength != 0 && !(Application.Mouse.MouseGrabView is MenuBar)) // ClearAllSelection (); } diff --git a/Terminal.Gui/Views/TextInput/TextView.cs b/Terminal.Gui/Views/TextInput/TextView.cs index 97cf3a09a..4d12a5ed1 100644 --- a/Terminal.Gui/Views/TextInput/TextView.cs +++ b/Terminal.Gui/Views/TextInput/TextView.cs @@ -1676,15 +1676,15 @@ public class TextView : View, IDesignable _lastWasKill = false; _columnTrack = CurrentColumn; - if (Application.MouseGrabHandler.MouseGrabView is null) + if (Application.Mouse.MouseGrabView is null) { - Application.MouseGrabHandler.GrabMouse (this); + Application.Mouse.GrabMouse (this); } } else if (ev.Flags.HasFlag (MouseFlags.Button1Released)) { _isButtonReleased = true; - Application.MouseGrabHandler.UngrabMouse (); + Application.Mouse.UngrabMouse (); } else if (ev.Flags.HasFlag (MouseFlags.Button1DoubleClicked)) { @@ -1886,9 +1886,9 @@ public class TextView : View, IDesignable /// protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? view) { - if (Application.MouseGrabHandler.MouseGrabView is { } && Application.MouseGrabHandler.MouseGrabView == this) + if (Application.Mouse.MouseGrabView is { } && Application.Mouse.MouseGrabView == this) { - Application.MouseGrabHandler.UngrabMouse (); + Application.Mouse.UngrabMouse (); } } @@ -2032,7 +2032,7 @@ public class TextView : View, IDesignable return null; } - if (Application.MouseGrabHandler.MouseGrabView == this && IsSelecting) + if (Application.Mouse.MouseGrabView == this && IsSelecting) { // BUGBUG: customized rect aren't supported now because the Redraw isn't using the Intersect method. //var minRow = Math.Min (Math.Max (Math.Min (selectionStartRow, currentRow) - topRow, 0), Viewport.Height); diff --git a/Tests/UnitTests/Application/ApplicationTests.cs b/Tests/UnitTests/Application/ApplicationTests.cs index 62ac6761a..6b820f6a8 100644 --- a/Tests/UnitTests/Application/ApplicationTests.cs +++ b/Tests/UnitTests/Application/ApplicationTests.cs @@ -309,7 +309,7 @@ public class ApplicationTests // Public Properties Assert.Null (Application.Top); - Assert.Null (Application.MouseGrabHandler.MouseGrabView); + Assert.Null (Application.Mouse.MouseGrabView); // Don't check Application.ForceDriver // Assert.Empty (Application.ForceDriver); @@ -574,7 +574,7 @@ public class ApplicationTests Assert.Null (Application.Top); RunState rs = Application.Begin (new ()); Assert.Equal (Application.Top, rs.Toplevel); - Assert.Null (Application.MouseGrabHandler.MouseGrabView); // public + Assert.Null (Application.Mouse.MouseGrabView); // public Application.Top!.Dispose (); } @@ -932,7 +932,7 @@ public class ApplicationTests Assert.Equal (new (0, 0), w.Frame.Location); Application.RaiseMouseEvent (new () { Flags = MouseFlags.Button1Pressed }); - Assert.Equal (w.Border, Application.MouseGrabHandler.MouseGrabView); + Assert.Equal (w.Border, Application.Mouse.MouseGrabView); Assert.Equal (new (0, 0), w.Frame.Location); // Move down and to the right. diff --git a/Tests/UnitTests/Application/Mouse/ApplicationMouseTests.cs b/Tests/UnitTests/Application/Mouse/ApplicationMouseTests.cs index f40e617a4..77a43b1bd 100644 --- a/Tests/UnitTests/Application/Mouse/ApplicationMouseTests.cs +++ b/Tests/UnitTests/Application/Mouse/ApplicationMouseTests.cs @@ -260,39 +260,39 @@ public class ApplicationMouseTests // if (iterations == 0) // { // Assert.True (tf.HasFocus); - // Assert.Null (Application.MouseGrabHandler.MouseGrabView); + // Assert.Null (Application.Mouse.MouseGrabView); // Application.RaiseMouseEvent (new () { ScreenPosition = new (5, 5), Flags = MouseFlags.ReportMousePosition }); - // Assert.Equal (sv, Application.MouseGrabHandler.MouseGrabView); + // Assert.Equal (sv, Application.Mouse.MouseGrabView); // MessageBox.Query ("Title", "Test", "Ok"); - // Assert.Null (Application.MouseGrabHandler.MouseGrabView); + // Assert.Null (Application.Mouse.MouseGrabView); // } // else if (iterations == 1) // { - // // Application.MouseGrabHandler.MouseGrabView is null because + // // Application.Mouse.MouseGrabView is null because // // another toplevel (Dialog) was opened - // Assert.Null (Application.MouseGrabHandler.MouseGrabView); + // Assert.Null (Application.Mouse.MouseGrabView); // Application.RaiseMouseEvent (new () { ScreenPosition = new (5, 5), Flags = MouseFlags.ReportMousePosition }); - // Assert.Null (Application.MouseGrabHandler.MouseGrabView); + // Assert.Null (Application.Mouse.MouseGrabView); // Application.RaiseMouseEvent (new () { ScreenPosition = new (40, 12), Flags = MouseFlags.ReportMousePosition }); - // Assert.Null (Application.MouseGrabHandler.MouseGrabView); + // Assert.Null (Application.Mouse.MouseGrabView); // Application.RaiseMouseEvent (new () { ScreenPosition = new (0, 0), Flags = MouseFlags.Button1Pressed }); - // Assert.Null (Application.MouseGrabHandler.MouseGrabView); + // Assert.Null (Application.Mouse.MouseGrabView); // Application.RequestStop (); // } // else if (iterations == 2) // { - // Assert.Null (Application.MouseGrabHandler.MouseGrabView); + // Assert.Null (Application.Mouse.MouseGrabView); // Application.RequestStop (); // } @@ -313,33 +313,33 @@ public class ApplicationMouseTests var view2 = new View { Id = "view2" }; var view3 = new View { Id = "view3" }; - Application.MouseGrabHandler.GrabbedMouse += Application_GrabbedMouse; - Application.MouseGrabHandler.UnGrabbedMouse += Application_UnGrabbedMouse; + Application.Mouse.GrabbedMouse += Application_GrabbedMouse; + Application.Mouse.UnGrabbedMouse += Application_UnGrabbedMouse; - Application.MouseGrabHandler.GrabMouse (view1); + Application.Mouse.GrabMouse (view1); Assert.Equal (0, count); Assert.Equal (grabView, view1); - Assert.Equal (view1, Application.MouseGrabHandler.MouseGrabView); + Assert.Equal (view1, Application.Mouse.MouseGrabView); - Application.MouseGrabHandler.UngrabMouse (); + Application.Mouse.UngrabMouse (); Assert.Equal (1, count); Assert.Equal (grabView, view1); - Assert.Null (Application.MouseGrabHandler.MouseGrabView); + Assert.Null (Application.Mouse.MouseGrabView); - Application.MouseGrabHandler.GrabbedMouse += Application_GrabbedMouse; - Application.MouseGrabHandler.UnGrabbedMouse += Application_UnGrabbedMouse; + Application.Mouse.GrabbedMouse += Application_GrabbedMouse; + Application.Mouse.UnGrabbedMouse += Application_UnGrabbedMouse; - Application.MouseGrabHandler.GrabMouse (view2); + Application.Mouse.GrabMouse (view2); Assert.Equal (1, count); Assert.Equal (grabView, view2); - Assert.Equal (view2, Application.MouseGrabHandler.MouseGrabView); + Assert.Equal (view2, Application.Mouse.MouseGrabView); - Application.MouseGrabHandler.UngrabMouse (); + Application.Mouse.UngrabMouse (); Assert.Equal (2, count); Assert.Equal (grabView, view2); - Assert.Equal (view3, Application.MouseGrabHandler.MouseGrabView); - Application.MouseGrabHandler.UngrabMouse (); - Assert.Null (Application.MouseGrabHandler.MouseGrabView); + Assert.Equal (view3, Application.Mouse.MouseGrabView); + Application.Mouse.UngrabMouse (); + Assert.Null (Application.Mouse.MouseGrabView); void Application_GrabbedMouse (object sender, ViewEventArgs e) { @@ -354,7 +354,7 @@ public class ApplicationMouseTests grabView = view2; } - Application.MouseGrabHandler.GrabbedMouse -= Application_GrabbedMouse; + Application.Mouse.GrabbedMouse -= Application_GrabbedMouse; } void Application_UnGrabbedMouse (object sender, ViewEventArgs e) @@ -375,10 +375,10 @@ public class ApplicationMouseTests if (count > 1) { // It's possible to grab another view after the previous was ungrabbed - Application.MouseGrabHandler.GrabMouse (view3); + Application.Mouse.GrabMouse (view3); } - Application.MouseGrabHandler.UnGrabbedMouse -= Application_UnGrabbedMouse; + Application.Mouse.UnGrabbedMouse -= Application_UnGrabbedMouse; } } @@ -393,18 +393,18 @@ public class ApplicationMouseTests top.Add (view); Application.Begin (top); - Assert.Null (Application.MouseGrabHandler.MouseGrabView); - Application.MouseGrabHandler.GrabMouse (view); - Assert.Equal (view, Application.MouseGrabHandler.MouseGrabView); + Assert.Null (Application.Mouse.MouseGrabView); + Application.Mouse.GrabMouse (view); + Assert.Equal (view, Application.Mouse.MouseGrabView); top.Remove (view); - Application.MouseGrabHandler.UngrabMouse (); + Application.Mouse.UngrabMouse (); view.Dispose (); #if DEBUG_IDISPOSABLE Assert.True (view.WasDisposed); #endif Application.RaiseMouseEvent (new () { ScreenPosition = new (0, 0), Flags = MouseFlags.Button1Pressed }); - Assert.Null (Application.MouseGrabHandler.MouseGrabView); + Assert.Null (Application.Mouse.MouseGrabView); Assert.Equal (0, count); top.Dispose (); } diff --git a/Tests/UnitTests/View/Adornment/ShadowStyleTests.cs b/Tests/UnitTests/View/Adornment/ShadowStyleTests.cs index c488a5737..b8ff317e6 100644 --- a/Tests/UnitTests/View/Adornment/ShadowStyleTests.cs +++ b/Tests/UnitTests/View/Adornment/ShadowStyleTests.cs @@ -160,7 +160,7 @@ public class ShadowStyleTests (ITestOutputHelper output) view.NewMouseEvent (new () { Flags = MouseFlags.Button1Released, Position = new (0, 0) }); Assert.Equal (origThickness, view.Margin.Thickness); - // Button1Pressed, Button1Released cause Application.MouseGrabHandler.MouseGrabView to be set + // Button1Pressed, Button1Released cause Application.Mouse.MouseGrabView to be set Application.ResetState (true); } } diff --git a/Tests/UnitTests/View/Mouse/MouseTests.cs b/Tests/UnitTests/View/Mouse/MouseTests.cs index 0a79be069..8b9732b37 100644 --- a/Tests/UnitTests/View/Mouse/MouseTests.cs +++ b/Tests/UnitTests/View/Mouse/MouseTests.cs @@ -96,7 +96,7 @@ public class MouseTests : TestsAllViews view.Dispose (); - // Button1Pressed, Button1Released cause Application.MouseGrabHandler.MouseGrabView to be set + // Button1Pressed, Button1Released cause Application.Mouse.MouseGrabView to be set Application.ResetState (true); } @@ -126,7 +126,7 @@ public class MouseTests : TestsAllViews view.Dispose (); - // Button1Pressed, Button1Released cause Application.MouseGrabHandler.MouseGrabView to be set + // Button1Pressed, Button1Released cause Application.Mouse.MouseGrabView to be set Application.ResetState (true); } @@ -156,7 +156,7 @@ public class MouseTests : TestsAllViews view.Dispose (); - // Button1Pressed, Button1Released cause Application.MouseGrabHandler.MouseGrabView to be set + // Button1Pressed, Button1Released cause Application.Mouse.MouseGrabView to be set Application.ResetState (true); } @@ -377,7 +377,7 @@ public class MouseTests : TestsAllViews // testView.Dispose (); - // // Button1Pressed, Button1Released cause Application.MouseGrabHandler.MouseGrabView to be set + // // Button1Pressed, Button1Released cause Application.Mouse.MouseGrabView to be set // Application.ResetState (true); //} @@ -442,7 +442,7 @@ public class MouseTests : TestsAllViews testView.Dispose (); - // Button1Pressed, Button1Released cause Application.MouseGrabHandler.MouseGrabView to be set + // Button1Pressed, Button1Released cause Application.Mouse.MouseGrabView to be set Application.ResetState (true); } @@ -504,7 +504,7 @@ public class MouseTests : TestsAllViews testView.Dispose (); - // Button1Pressed, Button1Released cause Application.MouseGrabHandler.MouseGrabView to be set + // Button1Pressed, Button1Released cause Application.Mouse.MouseGrabView to be set Application.ResetState (true); } @@ -567,7 +567,7 @@ public class MouseTests : TestsAllViews testView.Dispose (); - // Button1Pressed, Button1Released cause Application.MouseGrabHandler.MouseGrabView to be set + // Button1Pressed, Button1Released cause Application.Mouse.MouseGrabView to be set Application.ResetState (true); } @@ -631,7 +631,7 @@ public class MouseTests : TestsAllViews testView.Dispose (); - // Button1Pressed, Button1Released cause Application.MouseGrabHandler.MouseGrabView to be set + // Button1Pressed, Button1Released cause Application.Mouse.MouseGrabView to be set Application.ResetState (true); } private class MouseEventTestView : View diff --git a/Tests/UnitTests/Views/Menuv1/MenuBarv1Tests.cs b/Tests/UnitTests/Views/Menuv1/MenuBarv1Tests.cs index a8b85669f..c2cc4c004 100644 --- a/Tests/UnitTests/Views/Menuv1/MenuBarv1Tests.cs +++ b/Tests/UnitTests/Views/Menuv1/MenuBarv1Tests.cs @@ -2578,11 +2578,11 @@ Edit if (i is < 0 or > 0) { - Assert.Equal (menu, Application.MouseGrabHandler.MouseGrabView); + Assert.Equal (menu, Application.Mouse.MouseGrabView); } else { - Assert.Equal (menuBar, Application.MouseGrabHandler.MouseGrabView); + Assert.Equal (menuBar, Application.Mouse.MouseGrabView); } Assert.Equal ("_Edit", miCurrent.Parent.Title); diff --git a/Tests/UnitTests/Views/ToplevelTests.cs b/Tests/UnitTests/Views/ToplevelTests.cs index 82dfa9141..3160fb314 100644 --- a/Tests/UnitTests/Views/ToplevelTests.cs +++ b/Tests/UnitTests/Views/ToplevelTests.cs @@ -305,17 +305,17 @@ public class ToplevelTests } else if (iterations == 2) { - Assert.Null (Application.MouseGrabHandler.MouseGrabView); + Assert.Null (Application.Mouse.MouseGrabView); // Grab the mouse Application.RaiseMouseEvent (new () { ScreenPosition = new (3, 2), Flags = MouseFlags.Button1Pressed }); - Assert.Equal (Application.Top!.Border, Application.MouseGrabHandler.MouseGrabView); + Assert.Equal (Application.Top!.Border, Application.Mouse.MouseGrabView); Assert.Equal (new (2, 2, 10, 3), Application.Top.Frame); } else if (iterations == 3) { - Assert.Equal (Application.Top!.Border, Application.MouseGrabHandler.MouseGrabView); + Assert.Equal (Application.Top!.Border, Application.Mouse.MouseGrabView); // Drag to left Application.RaiseMouseEvent ( @@ -326,19 +326,19 @@ public class ToplevelTests }); AutoInitShutdownAttribute.RunIteration (); - Assert.Equal (Application.Top.Border, Application.MouseGrabHandler.MouseGrabView); + Assert.Equal (Application.Top.Border, Application.Mouse.MouseGrabView); Assert.Equal (new (1, 2, 10, 3), Application.Top.Frame); } else if (iterations == 4) { - Assert.Equal (Application.Top!.Border, Application.MouseGrabHandler.MouseGrabView); + Assert.Equal (Application.Top!.Border, Application.Mouse.MouseGrabView); Assert.Equal (new (1, 2), Application.Top.Frame.Location); - Assert.Equal (Application.Top.Border, Application.MouseGrabHandler.MouseGrabView); + Assert.Equal (Application.Top.Border, Application.Mouse.MouseGrabView); } else if (iterations == 5) { - Assert.Equal (Application.Top!.Border, Application.MouseGrabHandler.MouseGrabView); + Assert.Equal (Application.Top!.Border, Application.Mouse.MouseGrabView); // Drag up Application.RaiseMouseEvent ( @@ -349,26 +349,26 @@ public class ToplevelTests }); AutoInitShutdownAttribute.RunIteration (); - Assert.Equal (Application.Top!.Border, Application.MouseGrabHandler.MouseGrabView); + Assert.Equal (Application.Top!.Border, Application.Mouse.MouseGrabView); Assert.Equal (new (1, 1, 10, 3), Application.Top.Frame); } else if (iterations == 6) { - Assert.Equal (Application.Top!.Border, Application.MouseGrabHandler.MouseGrabView); + Assert.Equal (Application.Top!.Border, Application.Mouse.MouseGrabView); Assert.Equal (new (1, 1), Application.Top.Frame.Location); - Assert.Equal (Application.Top.Border, Application.MouseGrabHandler.MouseGrabView); + Assert.Equal (Application.Top.Border, Application.Mouse.MouseGrabView); Assert.Equal (new (1, 1, 10, 3), Application.Top.Frame); } else if (iterations == 7) { - Assert.Equal (Application.Top!.Border, Application.MouseGrabHandler.MouseGrabView); + Assert.Equal (Application.Top!.Border, Application.Mouse.MouseGrabView); // Ungrab the mouse Application.RaiseMouseEvent (new () { ScreenPosition = new (2, 1), Flags = MouseFlags.Button1Released }); AutoInitShutdownAttribute.RunIteration (); - Assert.Null (Application.MouseGrabHandler.MouseGrabView); + Assert.Null (Application.Mouse.MouseGrabView); } else if (iterations == 8) { @@ -411,7 +411,7 @@ public class ToplevelTests { location = win.Frame; - Assert.Null (Application.MouseGrabHandler.MouseGrabView); + Assert.Null (Application.Mouse.MouseGrabView); // Grab the mouse Application.RaiseMouseEvent ( @@ -420,11 +420,11 @@ public class ToplevelTests ScreenPosition = new (win.Frame.X, win.Frame.Y), Flags = MouseFlags.Button1Pressed }); - Assert.Equal (win.Border, Application.MouseGrabHandler.MouseGrabView); + Assert.Equal (win.Border, Application.Mouse.MouseGrabView); } else if (iterations == 2) { - Assert.Equal (win.Border, Application.MouseGrabHandler.MouseGrabView); + Assert.Equal (win.Border, Application.Mouse.MouseGrabView); // Drag to left movex = 1; @@ -438,18 +438,18 @@ public class ToplevelTests | MouseFlags.ReportMousePosition }); - Assert.Equal (win.Border, Application.MouseGrabHandler.MouseGrabView); + Assert.Equal (win.Border, Application.Mouse.MouseGrabView); } else if (iterations == 3) { // we should have moved +1, +0 - Assert.Equal (win.Border, Application.MouseGrabHandler.MouseGrabView); - Assert.Equal (win.Border, Application.MouseGrabHandler.MouseGrabView); + Assert.Equal (win.Border, Application.Mouse.MouseGrabView); + Assert.Equal (win.Border, Application.Mouse.MouseGrabView); location.Offset (movex, movey); } else if (iterations == 4) { - Assert.Equal (win.Border, Application.MouseGrabHandler.MouseGrabView); + Assert.Equal (win.Border, Application.Mouse.MouseGrabView); // Drag up movex = 0; @@ -463,18 +463,18 @@ public class ToplevelTests | MouseFlags.ReportMousePosition }); - Assert.Equal (win.Border, Application.MouseGrabHandler.MouseGrabView); + Assert.Equal (win.Border, Application.Mouse.MouseGrabView); } else if (iterations == 5) { // we should have moved +0, -1 - Assert.Equal (win.Border, Application.MouseGrabHandler.MouseGrabView); + Assert.Equal (win.Border, Application.Mouse.MouseGrabView); location.Offset (movex, movey); Assert.Equal (location, win.Frame); } else if (iterations == 6) { - Assert.Equal (win.Border, Application.MouseGrabHandler.MouseGrabView); + Assert.Equal (win.Border, Application.Mouse.MouseGrabView); // Ungrab the mouse movex = 0; @@ -487,7 +487,7 @@ public class ToplevelTests Flags = MouseFlags.Button1Released }); - Assert.Null (Application.MouseGrabHandler.MouseGrabView); + Assert.Null (Application.Mouse.MouseGrabView); } else if (iterations == 7) { @@ -602,11 +602,11 @@ public class ToplevelTests Assert.Equal (new (0, 0, 40, 10), top.Frame); Assert.Equal (new (0, 0, 20, 3), window.Frame); - Assert.Null (Application.MouseGrabHandler.MouseGrabView); + Assert.Null (Application.Mouse.MouseGrabView); Application.RaiseMouseEvent (new () { ScreenPosition = new (0, 0), Flags = MouseFlags.Button1Pressed }); - Assert.Equal (window.Border, Application.MouseGrabHandler.MouseGrabView); + Assert.Equal (window.Border, Application.Mouse.MouseGrabView); Application.RaiseMouseEvent ( new () @@ -694,14 +694,14 @@ public class ToplevelTests RunState rs = Application.Begin (window); - Assert.Null (Application.MouseGrabHandler.MouseGrabView); + Assert.Null (Application.Mouse.MouseGrabView); Assert.Equal (new (0, 0, 10, 3), window.Frame); Application.RaiseMouseEvent (new () { ScreenPosition = new (0, 0), Flags = MouseFlags.Button1Pressed }); var firstIteration = false; AutoInitShutdownAttribute.RunIteration (); - Assert.Equal (window.Border, Application.MouseGrabHandler.MouseGrabView); + Assert.Equal (window.Border, Application.Mouse.MouseGrabView); Assert.Equal (new (0, 0, 10, 3), window.Frame); @@ -713,7 +713,7 @@ public class ToplevelTests firstIteration = false; AutoInitShutdownAttribute.RunIteration (); - Assert.Equal (window.Border, Application.MouseGrabHandler.MouseGrabView); + Assert.Equal (window.Border, Application.Mouse.MouseGrabView); Assert.Equal (new (1, 1, 10, 3), window.Frame); Application.End (rs); diff --git a/Tests/UnitTestsParallelizable/Application/KeyboardTests.cs b/Tests/UnitTestsParallelizable/Application/KeyboardTests.cs index 74c6ab54b..45b094419 100644 --- a/Tests/UnitTestsParallelizable/Application/KeyboardTests.cs +++ b/Tests/UnitTestsParallelizable/Application/KeyboardTests.cs @@ -13,7 +13,7 @@ public class KeyboardTests public void Constructor_InitializesKeyBindings () { // Arrange & Act - var keyboard = new Keyboard (); + var keyboard = new KeyboardImpl (); // Assert Assert.NotNull (keyboard.KeyBindings); @@ -25,7 +25,7 @@ public class KeyboardTests public void QuitKey_DefaultValue_IsEsc () { // Arrange - var keyboard = new Keyboard (); + var keyboard = new KeyboardImpl (); // Assert Assert.Equal (Key.Esc, keyboard.QuitKey); @@ -35,7 +35,7 @@ public class KeyboardTests public void QuitKey_SetValue_UpdatesKeyBindings () { // Arrange - var keyboard = new Keyboard (); + var keyboard = new KeyboardImpl (); Key newQuitKey = Key.Q.WithCtrl; // Act @@ -51,7 +51,7 @@ public class KeyboardTests public void ArrangeKey_DefaultValue_IsCtrlF5 () { // Arrange - var keyboard = new Keyboard (); + var keyboard = new KeyboardImpl (); // Assert Assert.Equal (Key.F5.WithCtrl, keyboard.ArrangeKey); @@ -61,7 +61,7 @@ public class KeyboardTests public void NextTabKey_DefaultValue_IsTab () { // Arrange - var keyboard = new Keyboard (); + var keyboard = new KeyboardImpl (); // Assert Assert.Equal (Key.Tab, keyboard.NextTabKey); @@ -71,7 +71,7 @@ public class KeyboardTests public void PrevTabKey_DefaultValue_IsShiftTab () { // Arrange - var keyboard = new Keyboard (); + var keyboard = new KeyboardImpl (); // Assert Assert.Equal (Key.Tab.WithShift, keyboard.PrevTabKey); @@ -81,7 +81,7 @@ public class KeyboardTests public void NextTabGroupKey_DefaultValue_IsF6 () { // Arrange - var keyboard = new Keyboard (); + var keyboard = new KeyboardImpl (); // Assert Assert.Equal (Key.F6, keyboard.NextTabGroupKey); @@ -91,7 +91,7 @@ public class KeyboardTests public void PrevTabGroupKey_DefaultValue_IsShiftF6 () { // Arrange - var keyboard = new Keyboard (); + var keyboard = new KeyboardImpl (); // Assert Assert.Equal (Key.F6.WithShift, keyboard.PrevTabGroupKey); @@ -101,7 +101,7 @@ public class KeyboardTests public void KeyBindings_Add_CanAddCustomBinding () { // Arrange - var keyboard = new Keyboard (); + var keyboard = new KeyboardImpl (); Key customKey = Key.K.WithCtrl; // Act @@ -116,7 +116,7 @@ public class KeyboardTests public void KeyBindings_Remove_CanRemoveBinding () { // Arrange - var keyboard = new Keyboard (); + var keyboard = new KeyboardImpl (); Key customKey = Key.K.WithCtrl; keyboard.KeyBindings.Add (customKey, Command.Accept); @@ -131,7 +131,7 @@ public class KeyboardTests public void KeyDown_Event_CanBeSubscribed () { // Arrange - var keyboard = new Keyboard (); + var keyboard = new KeyboardImpl (); bool eventRaised = false; // Act @@ -148,7 +148,7 @@ public class KeyboardTests public void KeyUp_Event_CanBeSubscribed () { // Arrange - var keyboard = new Keyboard (); + var keyboard = new KeyboardImpl (); bool eventRaised = false; // Act @@ -165,7 +165,7 @@ public class KeyboardTests public void InvokeCommand_WithInvalidCommand_ThrowsNotSupportedException () { // Arrange - var keyboard = new Keyboard (); + var keyboard = new KeyboardImpl (); // Pick a command that isn't registered Command invalidCommand = (Command)9999; Key testKey = Key.A; @@ -179,8 +179,8 @@ public class KeyboardTests public void Multiple_Keyboards_CanExistIndependently () { // Arrange & Act - var keyboard1 = new Keyboard (); - var keyboard2 = new Keyboard (); + var keyboard1 = new KeyboardImpl (); + var keyboard2 = new KeyboardImpl (); keyboard1.QuitKey = Key.Q.WithCtrl; keyboard2.QuitKey = Key.X.WithCtrl; @@ -195,7 +195,7 @@ public class KeyboardTests public void KeyBindings_Replace_UpdatesExistingBinding () { // Arrange - var keyboard = new Keyboard (); + var keyboard = new KeyboardImpl (); Key oldKey = Key.Esc; Key newKey = Key.Q.WithCtrl; @@ -217,7 +217,7 @@ public class KeyboardTests public void KeyBindings_Clear_RemovesAllBindings () { // Arrange - var keyboard = new Keyboard (); + var keyboard = new KeyboardImpl (); // Verify initial state has bindings Assert.True (keyboard.KeyBindings.TryGet (keyboard.QuitKey, out _)); @@ -232,7 +232,7 @@ public class KeyboardTests public void AddKeyBindings_PopulatesDefaultBindings () { // Arrange - var keyboard = new Keyboard (); + var keyboard = new KeyboardImpl (); keyboard.KeyBindings.Clear (); Assert.False (keyboard.KeyBindings.TryGet (keyboard.QuitKey, out _)); @@ -250,7 +250,7 @@ public class KeyboardTests public void KeyBindings_Add_Adds () { // Arrange - var keyboard = new Keyboard (); + var keyboard = new KeyboardImpl (); // Act keyboard.KeyBindings.Add (Key.A, Command.Accept); @@ -267,7 +267,7 @@ public class KeyboardTests public void KeyBindings_Remove_Removes () { // Arrange - var keyboard = new Keyboard (); + var keyboard = new KeyboardImpl (); keyboard.KeyBindings.Add (Key.A, Command.Accept); Assert.True (keyboard.KeyBindings.TryGet (Key.A, out _)); @@ -282,7 +282,7 @@ public class KeyboardTests public void QuitKey_Default_Is_Esc () { // Arrange & Act - var keyboard = new Keyboard (); + var keyboard = new KeyboardImpl (); // Assert Assert.Equal (Key.Esc, keyboard.QuitKey); @@ -292,7 +292,7 @@ public class KeyboardTests public void QuitKey_Setter_UpdatesBindings () { // Arrange - var keyboard = new Keyboard (); + var keyboard = new KeyboardImpl (); Key prevKey = keyboard.QuitKey; // Act - Change QuitKey @@ -309,7 +309,7 @@ public class KeyboardTests public void NextTabKey_Setter_UpdatesBindings () { // Arrange - var keyboard = new Keyboard (); + var keyboard = new KeyboardImpl (); Key prevKey = keyboard.NextTabKey; Key newKey = Key.N.WithCtrl; @@ -326,7 +326,7 @@ public class KeyboardTests public void PrevTabKey_Setter_UpdatesBindings () { // Arrange - var keyboard = new Keyboard (); + var keyboard = new KeyboardImpl (); Key newKey = Key.P.WithCtrl; // Act @@ -342,7 +342,7 @@ public class KeyboardTests public void NextTabGroupKey_Setter_UpdatesBindings () { // Arrange - var keyboard = new Keyboard (); + var keyboard = new KeyboardImpl (); Key newKey = Key.PageDown.WithCtrl; // Act @@ -359,7 +359,7 @@ public class KeyboardTests public void PrevTabGroupKey_Setter_UpdatesBindings () { // Arrange - var keyboard = new Keyboard (); + var keyboard = new KeyboardImpl (); Key newKey = Key.PageUp.WithCtrl; // Act @@ -376,7 +376,7 @@ public class KeyboardTests public void ArrangeKey_Setter_UpdatesBindings () { // Arrange - var keyboard = new Keyboard (); + var keyboard = new KeyboardImpl (); Key newKey = Key.A.WithCtrl; // Act @@ -392,7 +392,7 @@ public class KeyboardTests public void KeyBindings_AddWithTarget_StoresTarget () { // Arrange - var keyboard = new Keyboard (); + var keyboard = new KeyboardImpl (); var view = new View (); // Act @@ -410,7 +410,7 @@ public class KeyboardTests public void InvokeCommandsBoundToKey_ReturnsNull_WhenNoBindingExists () { // Arrange - var keyboard = new Keyboard (); + var keyboard = new KeyboardImpl (); Key unboundKey = Key.Z.WithAlt.WithCtrl; // Act @@ -424,7 +424,7 @@ public class KeyboardTests public void InvokeCommandsBoundToKey_InvokesCommand_WhenBindingExists () { // Arrange - var keyboard = new Keyboard (); + var keyboard = new KeyboardImpl (); // QuitKey has a bound command by default // Act @@ -440,8 +440,8 @@ public class KeyboardTests public void Multiple_Keyboards_Independent_KeyBindings () { // Arrange - var keyboard1 = new Keyboard (); - var keyboard2 = new Keyboard (); + var keyboard1 = new KeyboardImpl (); + var keyboard2 = new KeyboardImpl (); // Act keyboard1.KeyBindings.Add (Key.X, Command.Accept); @@ -459,7 +459,7 @@ public class KeyboardTests public void KeyBindings_Replace_PreservesCommandsForNewKey () { // Arrange - var keyboard = new Keyboard (); + var keyboard = new KeyboardImpl (); Key oldKey = Key.Esc; Key newKey = Key.Q.WithCtrl; diff --git a/Tests/UnitTestsParallelizable/Application/MouseInterfaceTests.cs b/Tests/UnitTestsParallelizable/Application/MouseInterfaceTests.cs new file mode 100644 index 000000000..6cdc8d378 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Application/MouseInterfaceTests.cs @@ -0,0 +1,444 @@ +using Terminal.Gui.App; +using Xunit.Abstractions; + +namespace UnitTests_Parallelizable.ApplicationTests; + +/// +/// Parallelizable tests for IMouse interface. +/// Tests the decoupled mouse handling without Application.Init or global state. +/// +[Trait ("Category", "Input")] +public class MouseInterfaceTests (ITestOutputHelper output) +{ + private readonly ITestOutputHelper _output = output; + + #region IMouse Basic Properties + + [Fact] + public void Mouse_LastMousePosition_InitiallyNull () + { + // Arrange + MouseImpl mouse = new (); + + // Act & Assert + Assert.Null (mouse.LastMousePosition); + } + + [Theory] + [InlineData (0, 0)] + [InlineData (10, 20)] + [InlineData (-5, -10)] + [InlineData (100, 200)] + public void Mouse_LastMousePosition_CanBeSetAndRetrieved (int x, int y) + { + // Arrange + MouseImpl mouse = new (); + Point testPosition = new (x, y); + + // Act + mouse.LastMousePosition = testPosition; + + // Assert + Assert.Equal (testPosition, mouse.LastMousePosition); + Assert.Equal (testPosition, mouse.GetLastMousePosition ()); + } + + [Fact] + public void Mouse_IsMouseDisabled_DefaultsFalse () + { + // Arrange + MouseImpl mouse = new (); + + // Act & Assert + Assert.False (mouse.IsMouseDisabled); + } + + [Theory] + [InlineData (true)] + [InlineData (false)] + public void Mouse_IsMouseDisabled_CanBeSetAndRetrieved (bool disabled) + { + // Arrange + MouseImpl mouse = new (); + + // Act + mouse.IsMouseDisabled = disabled; + + // Assert + Assert.Equal (disabled, mouse.IsMouseDisabled); + } + + [Fact] + public void Mouse_CachedViewsUnderMouse_InitiallyEmpty () + { + // Arrange + MouseImpl mouse = new (); + + // Act & Assert + Assert.NotNull (mouse.CachedViewsUnderMouse); + Assert.Empty (mouse.CachedViewsUnderMouse); + } + + #endregion + + #region IMouse Event Handling + + [Fact] + public void Mouse_MouseEvent_CanSubscribeAndFire () + { + // Arrange + MouseImpl mouse = new (); + var eventFired = false; + MouseEventArgs capturedArgs = null; + + mouse.MouseEvent += (sender, args) => + { + eventFired = true; + capturedArgs = args; + }; + + MouseEventArgs testEvent = new () + { + ScreenPosition = new Point (5, 10), + Flags = MouseFlags.Button1Pressed + }; + + // Act + mouse.RaiseMouseEvent (testEvent); + + // Assert + Assert.True (eventFired); + Assert.NotNull (capturedArgs); + Assert.Equal (testEvent.ScreenPosition, capturedArgs.ScreenPosition); + Assert.Equal (testEvent.Flags, capturedArgs.Flags); + } + + [Fact] + public void Mouse_MouseEvent_CanUnsubscribe () + { + // Arrange + MouseImpl mouse = new (); + var eventCount = 0; + + void Handler (object sender, MouseEventArgs args) => eventCount++; + + mouse.MouseEvent += Handler; + + MouseEventArgs testEvent = new () + { + ScreenPosition = new Point (0, 0), + Flags = MouseFlags.Button1Pressed + }; + + // Act - Fire once + mouse.RaiseMouseEvent (testEvent); + Assert.Equal (1, eventCount); + + // Unsubscribe + mouse.MouseEvent -= Handler; + + // Fire again + mouse.RaiseMouseEvent (testEvent); + + // Assert - Count should not increase + Assert.Equal (1, eventCount); + } + + [Fact] + public void Mouse_RaiseMouseEvent_WithDisabledMouse_DoesNotFireEvent () + { + // Arrange + MouseImpl mouse = new (); + var eventFired = false; + + mouse.MouseEvent += (sender, args) => { eventFired = true; }; + mouse.IsMouseDisabled = true; + + MouseEventArgs testEvent = new () + { + ScreenPosition = new Point (0, 0), + Flags = MouseFlags.Button1Pressed + }; + + // Act + mouse.RaiseMouseEvent (testEvent); + + // Assert + Assert.False (eventFired); + } + + [Theory] + [InlineData (MouseFlags.Button1Pressed)] + [InlineData (MouseFlags.Button1Released)] + [InlineData (MouseFlags.Button1Clicked)] + [InlineData (MouseFlags.Button2Pressed)] + [InlineData (MouseFlags.WheeledUp)] + [InlineData (MouseFlags.ReportMousePosition)] + public void Mouse_RaiseMouseEvent_CorrectlyPassesFlags (MouseFlags flags) + { + // Arrange + MouseImpl mouse = new (); + MouseFlags? capturedFlags = null; + + mouse.MouseEvent += (sender, args) => { capturedFlags = args.Flags; }; + + MouseEventArgs testEvent = new () + { + ScreenPosition = new Point (5, 5), + Flags = flags + }; + + // Act + mouse.RaiseMouseEvent (testEvent); + + // Assert + Assert.NotNull (capturedFlags); + Assert.Equal (flags, capturedFlags.Value); + } + + #endregion + + #region IMouse ResetState + + [Fact] + public void Mouse_ResetState_ClearsCachedViews () + { + // Arrange + MouseImpl mouse = new (); + View testView = new () { Width = 10, Height = 10 }; + + mouse.CachedViewsUnderMouse.Add (testView); + Assert.Single (mouse.CachedViewsUnderMouse); + + // Act + mouse.ResetState (); + + // Assert + Assert.Empty (mouse.CachedViewsUnderMouse); + + testView.Dispose (); + } + + [Fact] + public void Mouse_ResetState_ClearsEventHandlers () + { + // Arrange + MouseImpl mouse = new (); + var eventCount = 0; + + mouse.MouseEvent += (sender, args) => eventCount++; + + MouseEventArgs testEvent = new () + { + ScreenPosition = new Point (0, 0), + Flags = MouseFlags.Button1Pressed + }; + + // Verify event fires before reset + mouse.RaiseMouseEvent (testEvent); + Assert.Equal (1, eventCount); + + // Act + mouse.ResetState (); + + // Raise event again + mouse.RaiseMouseEvent (testEvent); + + // Assert - Event count should not increase after reset + Assert.Equal (1, eventCount); + } + + [Fact] + public void Mouse_ResetState_DoesNotClearLastMousePosition () + { + // Arrange + MouseImpl mouse = new (); + Point testPosition = new (42, 84); + + mouse.LastMousePosition = testPosition; + + // Act + mouse.ResetState (); + + // Assert - LastMousePosition should NOT be cleared (per design) + Assert.Equal (testPosition, mouse.LastMousePosition); + } + + #endregion + + #region IMouse Isolation + + [Fact] + public void Mouse_Instances_AreIndependent () + { + // Arrange + MouseImpl mouse1 = new (); + MouseImpl mouse2 = new (); + + // Act + mouse1.IsMouseDisabled = true; + mouse1.LastMousePosition = new Point (10, 10); + + // Assert - mouse2 should be unaffected + Assert.False (mouse2.IsMouseDisabled); + Assert.Null (mouse2.LastMousePosition); + } + + [Fact] + public void Mouse_Events_AreIndependent () + { + // Arrange + MouseImpl mouse1 = new (); + var mouse1EventCount = 0; + + MouseImpl mouse2 = new (); + var mouse2EventCount = 0; + + mouse1.MouseEvent += (sender, args) => mouse1EventCount++; + mouse2.MouseEvent += (sender, args) => mouse2EventCount++; + + MouseEventArgs testEvent = new () + { + ScreenPosition = new Point (0, 0), + Flags = MouseFlags.Button1Pressed + }; + + // Act + mouse1.RaiseMouseEvent (testEvent); + + // Assert + Assert.Equal (1, mouse1EventCount); + Assert.Equal (0, mouse2EventCount); + } + + [Fact] + public void Mouse_CachedViews_AreIndependent () + { + // Arrange + MouseImpl mouse1 = new (); + MouseImpl mouse2 = new (); + + View view1 = new (); + View view2 = new (); + + // Act + mouse1.CachedViewsUnderMouse.Add (view1); + mouse2.CachedViewsUnderMouse.Add (view2); + + // Assert + Assert.Single (mouse1.CachedViewsUnderMouse); + Assert.Single (mouse2.CachedViewsUnderMouse); + Assert.Contains (view1, mouse1.CachedViewsUnderMouse); + Assert.Contains (view2, mouse2.CachedViewsUnderMouse); + Assert.DoesNotContain (view2, mouse1.CachedViewsUnderMouse); + Assert.DoesNotContain (view1, mouse2.CachedViewsUnderMouse); + + view1.Dispose (); + view2.Dispose (); + } + + #endregion + + #region Mouse Grab Tests + + [Fact] + public void Mouse_GrabMouse_SetsMouseGrabView () + { + // Arrange + MouseImpl mouse = new (); + View testView = new (); + + // Act + mouse.GrabMouse (testView); + + // Assert + Assert.Equal (testView, mouse.MouseGrabView); + } + + [Fact] + public void Mouse_UngrabMouse_ClearsMouseGrabView () + { + // Arrange + MouseImpl mouse = new (); + View testView = new (); + mouse.GrabMouse (testView); + + // Act + mouse.UngrabMouse (); + + // Assert + Assert.Null (mouse.MouseGrabView); + } + + [Fact] + public void Mouse_GrabbingMouse_CanBeCanceled () + { + // Arrange + MouseImpl mouse = new (); + View testView = new (); + var eventFired = false; + + mouse.GrabbingMouse += (sender, args) => + { + eventFired = true; + args.Cancel = true; + }; + + // Act + mouse.GrabMouse (testView); + + // Assert + Assert.True (eventFired); + Assert.Null (mouse.MouseGrabView); // Should not be set because it was cancelled + } + + [Fact] + public void Mouse_GrabbedMouse_EventFired () + { + // Arrange + MouseImpl mouse = new (); + View testView = new (); + var eventFired = false; + View? eventView = null; + + mouse.GrabbedMouse += (sender, args) => + { + eventFired = true; + eventView = args.View; + }; + + // Act + mouse.GrabMouse (testView); + + // Assert + Assert.True (eventFired); + Assert.Equal (testView, eventView); + } + + [Fact] + public void Mouse_UnGrabbedMouse_EventFired () + { + // Arrange + MouseImpl mouse = new (); + View testView = new (); + mouse.GrabMouse (testView); + + var eventFired = false; + View? eventView = null; + + mouse.UnGrabbedMouse += (sender, args) => + { + eventFired = true; + eventView = args.View; + }; + + // Act + mouse.UngrabMouse (); + + // Assert + Assert.True (eventFired); + Assert.Equal (testView, eventView); + } + + #endregion +} diff --git a/Tests/UnitTestsParallelizable/Application/MouseTests.cs b/Tests/UnitTestsParallelizable/Application/MouseTests.cs new file mode 100644 index 000000000..fdd3260a4 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Application/MouseTests.cs @@ -0,0 +1,125 @@ +using Terminal.Gui.App; +using Xunit.Abstractions; + +namespace UnitTests_Parallelizable.ApplicationTests; + +/// +/// Tests for the interface and implementation. +/// These tests demonstrate the decoupled mouse handling that enables parallel test execution. +/// +public class MouseTests (ITestOutputHelper output) +{ + private readonly ITestOutputHelper _output = output; + + [Fact] + public void Mouse_Instance_CreatedSuccessfully () + { + // Arrange & Act + MouseImpl mouse = new (); + + // Assert + Assert.NotNull (mouse); + Assert.False (mouse.IsMouseDisabled); + Assert.Null (mouse.LastMousePosition); + } + + [Fact] + public void Mouse_LastMousePosition_CanBeSetAndRetrieved () + { + // Arrange + MouseImpl mouse = new (); + Point expectedPosition = new (10, 20); + + // Act + mouse.LastMousePosition = expectedPosition; + Point? actualPosition = mouse.GetLastMousePosition (); + + // Assert + Assert.Equal (expectedPosition, actualPosition); + } + + [Fact] + public void Mouse_IsMouseDisabled_CanBeSetAndRetrieved () + { + // Arrange + MouseImpl mouse = new (); + + // Act + mouse.IsMouseDisabled = true; + + // Assert + Assert.True (mouse.IsMouseDisabled); + } + + [Fact] + public void Mouse_CachedViewsUnderMouse_InitializedEmpty () + { + // Arrange + MouseImpl mouse = new (); + + // Assert + Assert.NotNull (mouse.CachedViewsUnderMouse); + Assert.Empty (mouse.CachedViewsUnderMouse); + } + + [Fact] + public void Mouse_ResetState_ClearsEventAndCachedViews () + { + // Arrange + MouseImpl mouse = new (); + var eventFired = false; + mouse.MouseEvent += (sender, args) => eventFired = true; + mouse.CachedViewsUnderMouse.Add (new View ()); + + // Act + mouse.ResetState (); + + // Assert - CachedViewsUnderMouse should be cleared + Assert.Empty (mouse.CachedViewsUnderMouse); + + // Event handlers should be cleared + MouseEventArgs mouseEvent = new () { ScreenPosition = new Point (0, 0), Flags = MouseFlags.Button1Pressed }; + mouse.RaiseMouseEvent (mouseEvent); + Assert.False (eventFired, "Event should not fire after ResetState"); + } + + [Fact] + public void Mouse_RaiseMouseEvent_DoesNotUpdateLastPositionWhenNotInitialized () + { + // Arrange + MouseImpl mouse = new (); + MouseEventArgs mouseEvent = new () { ScreenPosition = new Point (5, 10), Flags = MouseFlags.Button1Pressed }; + + // Act - Application is not initialized, so LastMousePosition should not be set + mouse.RaiseMouseEvent (mouseEvent); + + // Assert + // Since Application.Initialized is false, LastMousePosition should remain null + // This behavior matches the original implementation + Assert.Null (mouse.LastMousePosition); + } + + [Fact] + public void Mouse_MouseEvent_CanBeSubscribedAndUnsubscribed () + { + // Arrange + MouseImpl mouse = new (); + var eventCount = 0; + EventHandler handler = (sender, args) => eventCount++; + + // Act - Subscribe + mouse.MouseEvent += handler; + MouseEventArgs mouseEvent = new () { ScreenPosition = new Point (0, 0), Flags = MouseFlags.Button1Pressed }; + mouse.RaiseMouseEvent (mouseEvent); + + // Assert - Event fired once + Assert.Equal (1, eventCount); + + // Act - Unsubscribe + mouse.MouseEvent -= handler; + mouse.RaiseMouseEvent (mouseEvent); + + // Assert - Event count unchanged + Assert.Equal (1, eventCount); + } +} diff --git a/Tests/UnitTestsParallelizable/TestSetup.cs b/Tests/UnitTestsParallelizable/TestSetup.cs index bddfea00f..897548638 100644 --- a/Tests/UnitTestsParallelizable/TestSetup.cs +++ b/Tests/UnitTestsParallelizable/TestSetup.cs @@ -40,7 +40,7 @@ public class GlobalTestSetup : IDisposable // Public Properties Assert.Null (Application.Top); - Assert.Null (Application.MouseGrabHandler.MouseGrabView); + Assert.Null (Application.Mouse.MouseGrabView); // Don't check Application.ForceDriver // Assert.Empty (Application.ForceDriver); diff --git a/Tests/UnitTestsParallelizable/View/Mouse/MouseEventRoutingTests.cs b/Tests/UnitTestsParallelizable/View/Mouse/MouseEventRoutingTests.cs new file mode 100644 index 000000000..8f9453832 --- /dev/null +++ b/Tests/UnitTestsParallelizable/View/Mouse/MouseEventRoutingTests.cs @@ -0,0 +1,498 @@ +using Terminal.Gui.App; +using Xunit.Abstractions; + +namespace UnitTests_Parallelizable.ApplicationTests; + +/// +/// Parallelizable tests for mouse event routing and coordinate transformation. +/// These tests validate mouse event handling without Application.Begin or global state. +/// +[Trait ("Category", "Input")] +public class MouseEventRoutingTests (ITestOutputHelper output) +{ + private readonly ITestOutputHelper _output = output; + + #region Mouse Event Routing to Views + + [Theory] + [InlineData (5, 5, 5, 5, true)] // Click inside view + [InlineData (0, 0, 0, 0, true)] // Click at origin + [InlineData (9, 9, 9, 9, true)] // Click at far corner (view is 10x10) + [InlineData (10, 10, -1, -1, false)] // Click outside view + [InlineData (-1, -1, -1, -1, false)] // Click outside view + public void View_NewMouseEvent_ReceivesCorrectCoordinates (int screenX, int screenY, int expectedViewX, int expectedViewY, bool shouldReceive) + { + // Arrange + View view = new () + { + X = 0, + Y = 0, + Width = 10, + Height = 10 + }; + + Point? receivedPosition = null; + var eventReceived = false; + + view.MouseEvent += (sender, args) => + { + eventReceived = true; + receivedPosition = args.Position; + }; + + MouseEventArgs mouseEvent = new () + { + Position = new Point (screenX, screenY), + Flags = MouseFlags.Button1Clicked + }; + + // Act + view.NewMouseEvent (mouseEvent); + + // Assert + if (shouldReceive) + { + Assert.True (eventReceived); + Assert.NotNull (receivedPosition); + Assert.Equal (expectedViewX, receivedPosition.Value.X); + Assert.Equal (expectedViewY, receivedPosition.Value.Y); + } + + view.Dispose (); + } + + [Theory] + [InlineData (0, 0, 5, 5, 5, 5, true)] // View at origin, click at (5,5) in view + [InlineData (10, 10, 5, 5, 5, 5, true)] // View offset, but we still pass view-relative coords + [InlineData (0, 0, 0, 0, 0, 0, true)] // View at origin, click at origin + [InlineData (5, 5, 9, 9, 9, 9, true)] // View offset, click at far corner (view-relative) + [InlineData (0, 0, 10, 10, -1, -1, false)] // Click outside view bounds + [InlineData (0, 0, -1, -1, -1, -1, false)] // Click outside view bounds + public void View_WithOffset_ReceivesCorrectCoordinates ( + int viewX, + int viewY, + int viewRelativeX, + int viewRelativeY, + int expectedViewX, + int expectedViewY, + bool shouldReceive) + { + // Arrange + // Note: When testing View.NewMouseEvent directly (without Application routing), + // coordinates are already view-relative. The view's X/Y position doesn't affect + // the coordinate transformation at this level. + View view = new () + { + X = viewX, + Y = viewY, + Width = 10, + Height = 10 + }; + + Point? receivedPosition = null; + var eventReceived = false; + + view.MouseEvent += (sender, args) => + { + eventReceived = true; + receivedPosition = args.Position; + }; + + MouseEventArgs mouseEvent = new () + { + Position = new Point (viewRelativeX, viewRelativeY), + Flags = MouseFlags.Button1Clicked + }; + + // Act + view.NewMouseEvent (mouseEvent); + + // Assert + if (shouldReceive) + { + Assert.True (eventReceived, $"Event should be received at view-relative ({viewRelativeX},{viewRelativeY})"); + Assert.NotNull (receivedPosition); + Assert.Equal (expectedViewX, receivedPosition.Value.X); + Assert.Equal (expectedViewY, receivedPosition.Value.Y); + } + + view.Dispose (); + } + + #endregion + + #region View Hierarchy Mouse Event Routing + + [Fact] + public void SubView_ReceivesMouseEvent_WithCorrectRelativeCoordinates () + { + // Arrange + View superView = new () + { + X = 0, + Y = 0, + Width = 20, + Height = 20 + }; + + View subView = new () + { + X = 5, + Y = 5, + Width = 10, + Height = 10 + }; + + superView.Add (subView); + + Point? subViewReceivedPosition = null; + var subViewEventReceived = false; + + subView.MouseEvent += (sender, args) => + { + subViewEventReceived = true; + subViewReceivedPosition = args.Position; + }; + + // Click at position (2, 2) relative to subView (which is at 5,5 relative to superView) + MouseEventArgs mouseEvent = new () + { + Position = new Point (2, 2), // Relative to subView + Flags = MouseFlags.Button1Clicked + }; + + // Act + subView.NewMouseEvent (mouseEvent); + + // Assert + Assert.True (subViewEventReceived); + Assert.NotNull (subViewReceivedPosition); + Assert.Equal (2, subViewReceivedPosition.Value.X); + Assert.Equal (2, subViewReceivedPosition.Value.Y); + + subView.Dispose (); + superView.Dispose (); + } + + [Fact] + public void MouseClick_OnSubView_RaisesMouseClickEvent () + { + // Arrange + View superView = new () + { + Width = 20, + Height = 20 + }; + + View subView = new () + { + X = 5, + Y = 5, + Width = 10, + Height = 10 + }; + + superView.Add (subView); + + var clickCount = 0; + subView.MouseClick += (sender, args) => clickCount++; + + MouseEventArgs mouseEvent = new () + { + Position = new Point (5, 5), + Flags = MouseFlags.Button1Clicked + }; + + // Act + subView.NewMouseEvent (mouseEvent); + + // Assert + Assert.Equal (1, clickCount); + + subView.Dispose (); + superView.Dispose (); + } + + #endregion + + #region Mouse Event Propagation + + [Fact] + public void View_HandledEvent_StopsPropagation () + { + // Arrange + View view = new () { Width = 10, Height = 10 }; + var handlerCalled = false; + var clickHandlerCalled = false; + + view.MouseEvent += (sender, args) => + { + handlerCalled = true; + args.Handled = true; // Mark as handled + }; + + view.MouseClick += (sender, args) => { clickHandlerCalled = true; }; + + MouseEventArgs mouseEvent = new () + { + Position = new Point (5, 5), + Flags = MouseFlags.Button1Clicked + }; + + // Act + bool? result = view.NewMouseEvent (mouseEvent); + + // Assert + Assert.True (result.HasValue && result.Value); // Event was handled + Assert.True (handlerCalled); + Assert.False (clickHandlerCalled); // Click handler should not be called when event is handled + + view.Dispose (); + } + + [Fact] + public void View_UnhandledEvent_ContinuesProcessing () + { + // Arrange + View view = new () { Width = 10, Height = 10 }; + var eventHandlerCalled = false; + var clickHandlerCalled = false; + + view.MouseEvent += (sender, args) => + { + eventHandlerCalled = true; + // Don't set Handled = true + }; + + view.MouseClick += (sender, args) => { clickHandlerCalled = true; }; + + MouseEventArgs mouseEvent = new () + { + Position = new Point (5, 5), + Flags = MouseFlags.Button1Clicked + }; + + // Act + view.NewMouseEvent (mouseEvent); + + // Assert + Assert.True (eventHandlerCalled); + Assert.True (clickHandlerCalled); // Click handler should be called when event is not handled + + view.Dispose (); + } + + #endregion + + #region Mouse Button Events + + [Theory] + [InlineData (MouseFlags.Button1Pressed, 1, 0, 0)] + [InlineData (MouseFlags.Button1Released, 0, 1, 0)] + [InlineData (MouseFlags.Button1Clicked, 0, 0, 1)] + public void View_MouseButtonEvents_RaiseCorrectHandlers (MouseFlags flags, int expectedPressed, int expectedReleased, int expectedClicked) + { + // Arrange + View view = new () { Width = 10, Height = 10 }; + var pressedCount = 0; + var releasedCount = 0; + var clickedCount = 0; + + view.MouseEvent += (sender, args) => + { + if (args.Flags.HasFlag (MouseFlags.Button1Pressed)) + { + pressedCount++; + } + + if (args.Flags.HasFlag (MouseFlags.Button1Released)) + { + releasedCount++; + } + }; + + view.MouseClick += (sender, args) => { clickedCount++; }; + + MouseEventArgs mouseEvent = new () + { + Position = new Point (5, 5), + Flags = flags + }; + + // Act + view.NewMouseEvent (mouseEvent); + + // Assert + Assert.Equal (expectedPressed, pressedCount); + Assert.Equal (expectedReleased, releasedCount); + Assert.Equal (expectedClicked, clickedCount); + + view.Dispose (); + } + + [Theory] + [InlineData (MouseFlags.Button1Clicked)] + [InlineData (MouseFlags.Button2Clicked)] + [InlineData (MouseFlags.Button3Clicked)] + [InlineData (MouseFlags.Button4Clicked)] + public void View_AllMouseButtons_TriggerClickEvent (MouseFlags clickFlag) + { + // Arrange + View view = new () { Width = 10, Height = 10 }; + var clickCount = 0; + + view.MouseClick += (sender, args) => clickCount++; + + MouseEventArgs mouseEvent = new () + { + Position = new Point (5, 5), + Flags = clickFlag + }; + + // Act + view.NewMouseEvent (mouseEvent); + + // Assert + Assert.Equal (1, clickCount); + + view.Dispose (); + } + + #endregion + + #region Disabled View Tests + + [Fact] + public void View_Disabled_DoesNotRaiseMouseEvent () + { + // Arrange + View view = new () + { + Width = 10, + Height = 10, + Enabled = false + }; + + var eventCalled = false; + view.MouseEvent += (sender, args) => { eventCalled = true; }; + + MouseEventArgs mouseEvent = new () + { + Position = new Point (5, 5), + Flags = MouseFlags.Button1Clicked + }; + + // Act + view.NewMouseEvent (mouseEvent); + + // Assert + Assert.False (eventCalled); + + view.Dispose (); + } + + [Fact] + public void View_Disabled_DoesNotRaiseMouseClickEvent () + { + // Arrange + View view = new () + { + Width = 10, + Height = 10, + Enabled = false + }; + + var clickCalled = false; + view.MouseClick += (sender, args) => { clickCalled = true; }; + + MouseEventArgs mouseEvent = new () + { + Position = new Point (5, 5), + Flags = MouseFlags.Button1Clicked + }; + + // Act + view.NewMouseEvent (mouseEvent); + + // Assert + Assert.False (clickCalled); + + view.Dispose (); + } + + #endregion + + #region Focus and Selection Tests + + [Theory] + [InlineData (true, true)] + [InlineData (false, false)] + public void MouseClick_SetsFocus_BasedOnCanFocus (bool canFocus, bool expectFocus) + { + // Arrange + View superView = new () { CanFocus = true, Width = 20, Height = 20 }; + View subView = new () + { + X = 5, + Y = 5, + Width = 10, + Height = 10, + CanFocus = canFocus + }; + + superView.Add (subView); + superView.SetFocus (); // Give superView focus first + + MouseEventArgs mouseEvent = new () + { + Position = new Point (2, 2), + Flags = MouseFlags.Button1Clicked + }; + + // Act + subView.NewMouseEvent (mouseEvent); + + // Assert + Assert.Equal (expectFocus, subView.HasFocus); + + subView.Dispose (); + superView.Dispose (); + } + + [Fact] + public void MouseClick_RaisesSelecting_WhenCanFocus () + { + // Arrange + View superView = new () { CanFocus = true, Width = 20, Height = 20 }; + View view = new () + { + X = 5, + Y = 5, + Width = 10, + Height = 10, + CanFocus = true + }; + + superView.Add (view); + + var selectingCount = 0; + view.Selecting += (sender, args) => selectingCount++; + + MouseEventArgs mouseEvent = new () + { + Position = new Point (5, 5), + Flags = MouseFlags.Button1Clicked + }; + + // Act + view.NewMouseEvent (mouseEvent); + + // Assert + Assert.Equal (1, selectingCount); + + view.Dispose (); + superView.Dispose (); + } + + #endregion +} diff --git a/local_packages/Terminal.Gui.2.0.0.nupkg b/local_packages/Terminal.Gui.2.0.0.nupkg index d799399d5..b93b3273b 100644 Binary files a/local_packages/Terminal.Gui.2.0.0.nupkg and b/local_packages/Terminal.Gui.2.0.0.nupkg differ diff --git a/local_packages/Terminal.Gui.2.0.0.snupkg b/local_packages/Terminal.Gui.2.0.0.snupkg index ba67a5dbc..390f9f036 100644 Binary files a/local_packages/Terminal.Gui.2.0.0.snupkg and b/local_packages/Terminal.Gui.2.0.0.snupkg differ