From 12e3346c0103dfff43d86810c4e2c556af6d39c2 Mon Sep 17 00:00:00 2001 From: Tig Date: Fri, 20 Sep 2024 11:53:14 -0600 Subject: [PATCH] Prototype --- Terminal.Gui/Application/Application.Mouse.cs | 153 +++++++++++++----- Terminal.Gui/Application/Application.cs | 2 +- Terminal.Gui/View/Adornment/Adornment.cs | 37 ++--- Terminal.Gui/View/View.Layout.cs | 27 +++- Terminal.Gui/View/View.Mouse.cs | 32 ++-- UICatalog/Scenarios/Mouse.cs | 15 +- 6 files changed, 187 insertions(+), 79 deletions(-) diff --git a/Terminal.Gui/Application/Application.Mouse.cs b/Terminal.Gui/Application/Application.Mouse.cs index 2b16686d3..c1d2a97a1 100644 --- a/Terminal.Gui/Application/Application.Mouse.cs +++ b/Terminal.Gui/Application/Application.Mouse.cs @@ -1,4 +1,10 @@ #nullable enable +<<<<<<< Updated upstream +======= +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.VisualBasic.Syntax; + +>>>>>>> Stashed changes namespace Terminal.Gui; public static partial class Application // Mouse handling @@ -116,8 +122,8 @@ public static partial class Application // Mouse handling UnGrabbedMouse?.Invoke (view, new (view)); } - // Used by OnMouseEvent to track the last view that was clicked on. - internal static View? MouseEnteredView { get; set; } + // Used by OnMouseEvent to suppport MouseEnter and MouseLeave events + internal static List ViewsUnderMouse { get; } = new (); /// Event fired when a mouse move or click occurs. Coordinates are screen relative. /// @@ -139,17 +145,32 @@ public static partial class Application // Mouse handling return; } +<<<<<<< Updated upstream var view = View.FindDeepestView (Current, mouseEvent.Position); +======= + Stack viewsUnderMouse = View.GetViewsUnderMouse (mouseEvent.Position); - if (view is { }) + View? deepestViewUnderMouse = viewsUnderMouse.TryPeek (out View? result) ? result : null; + + if ((mouseEvent.Flags == MouseFlags.Button1Pressed + || mouseEvent.Flags == MouseFlags.Button2Pressed + || mouseEvent.Flags == MouseFlags.Button3Pressed + || mouseEvent.Flags == MouseFlags.Button4Pressed) + && Popover is { Visible: true } && !ApplicationNavigation.IsInHierarchy (Popover, deepestViewUnderMouse, includeAdornments: true)) + { + Popover.Visible = false; + } +>>>>>>> Stashed changes + + if (deepestViewUnderMouse is { }) { #if DEBUG_IDISPOSABLE - if (view.WasDisposed) + if (deepestViewUnderMouse.WasDisposed) { - throw new ObjectDisposedException (view.GetType ().FullName); + throw new ObjectDisposedException (deepestViewUnderMouse.GetType ().FullName); } #endif - mouseEvent.View = view; + mouseEvent.View = deepestViewUnderMouse; } MouseEvent?.Invoke (null, mouseEvent); @@ -177,7 +198,7 @@ public static partial class Application // Mouse handling Position = frameLoc, Flags = mouseEvent.Flags, ScreenPosition = mouseEvent.Position, - View = view ?? MouseGrabView + View = deepestViewUnderMouse ?? MouseGrabView }; if ((MouseGrabView.Viewport with { Location = Point.Empty }).Contains (viewRelativeMouseEvent.Position) is false) @@ -193,7 +214,7 @@ public static partial class Application // Mouse handling } // ReSharper disable once ConditionIsAlwaysTrueOrFalse - if (MouseGrabView is null && view is Adornment) + if (MouseGrabView is null && deepestViewUnderMouse is Adornment) { // The view that grabbed the mouse has been disposed return; @@ -202,6 +223,7 @@ public static partial class Application // Mouse handling // We can combine this into the switch expression to reduce cognitive complexity even more and likely // avoid one or two of these checks in the process, as well. +<<<<<<< Updated upstream WantContinuousButtonPressedView = view switch { { WantContinuousButtonPressed: true } => view, @@ -224,17 +246,24 @@ public static partial class Application // Mouse handling ApplicationOverlapped.MoveCurrent ((Toplevel)top); } } +======= + WantContinuousButtonPressedView = deepestViewUnderMouse switch + { + { WantContinuousButtonPressed: true } => deepestViewUnderMouse, + _ => null + }; +>>>>>>> Stashed changes // 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 (view is null) + if (deepestViewUnderMouse is null) { return; } MouseEvent? me; - if (view is Adornment adornment) + if (deepestViewUnderMouse is Adornment adornment) { Point frameLoc = adornment.ScreenToFrame (mouseEvent.Position); @@ -243,19 +272,19 @@ public static partial class Application // Mouse handling Position = frameLoc, Flags = mouseEvent.Flags, ScreenPosition = mouseEvent.Position, - View = view + View = deepestViewUnderMouse }; } - else if (view.ViewportToScreen (Rectangle.Empty with { Size = view.Viewport.Size }).Contains (mouseEvent.Position)) + else if (deepestViewUnderMouse.ViewportToScreen (Rectangle.Empty with { Size = deepestViewUnderMouse.Viewport.Size }).Contains (mouseEvent.Position)) { - Point viewportLocation = view.ScreenToViewport (mouseEvent.Position); + Point viewportLocation = deepestViewUnderMouse.ScreenToViewport (mouseEvent.Position); me = new () { Position = viewportLocation, Flags = mouseEvent.Flags, ScreenPosition = mouseEvent.Position, - View = view + View = deepestViewUnderMouse }; } else @@ -263,51 +292,97 @@ public static partial class Application // Mouse handling return; } - if (MouseEnteredView is null) - { - MouseEnteredView = view; - view.NewMouseEnterEvent (me); - } - else if (MouseEnteredView != view) - { - MouseEnteredView.NewMouseLeaveEvent (me); - view.NewMouseEnterEvent (me); - MouseEnteredView = view; - } + // Mouse Enter/Leave events - if (!view.WantMousePositionReports && mouseEvent.Flags == MouseFlags.ReportMousePosition) + // Tell any views that are no longer under the mouse that the mouse has left and remove them from list + List viewsToLeave = ViewsUnderMouse.Where (v => v is { } && !viewsUnderMouse.Contains (v)).ToList (); + foreach (View? view in viewsToLeave) { - return; - } + if (view is null) + { + continue; + } - WantContinuousButtonPressedView = view.WantContinuousButtonPressed ? view : null; - - //Debug.WriteLine ($"OnMouseEvent: ({a.MouseEvent.X},{a.MouseEvent.Y}) - {a.MouseEvent.Flags}"); - - while (view.NewMouseEvent (me) is not true && MouseGrabView is not { }) - { if (view is Adornment adornmentView) { - view = adornmentView.Parent!.SuperView; + Point frameLoc = adornmentView.ScreenToFrame (mouseEvent.Position); + if (adornmentView.Parent is { } && !adornmentView.Contains (frameLoc)) + { + ViewsUnderMouse.Remove (view); + view.NewMouseLeaveEvent (me); + } } else { - view = view.SuperView; + Point viewportLocation = view.ScreenToViewport (mouseEvent.Position); + if (!view.Contains (viewportLocation)) + { + ViewsUnderMouse.Remove (view); + view.NewMouseLeaveEvent (me); + } + + } + } + + // Tell any views that are now under the mouse (viewsUnderMouse) that the mouse has entered and add them to the list + foreach (View? view in viewsUnderMouse) + { + if (view is null) + { + continue; } - if (view is null) + Point viewportLocation = view.ScreenToViewport (mouseEvent.Position); + + if (view is Adornment adornmentView) + { + + if (adornmentView.Parent is { } && !adornmentView.Contains (viewportLocation)) + { + ViewsUnderMouse.Add (view); + view.NewMouseEnterEvent (me); + } + } + else if (view.Contains (viewportLocation)) + { + ViewsUnderMouse.Add (view); + view.NewMouseEnterEvent (me); + } + } + + + WantContinuousButtonPressedView = deepestViewUnderMouse.WantContinuousButtonPressed ? deepestViewUnderMouse : null; + + //Debug.WriteLine ($"OnMouseEvent: ({a.MouseEvent.X},{a.MouseEvent.Y}) - {a.MouseEvent.Flags}"); + if (deepestViewUnderMouse.Id == "mouseDemo") + { + + } + + while (deepestViewUnderMouse.NewMouseEvent (me) 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 = view.ScreenToViewport (mouseEvent.Position); + Point boundsPoint = deepestViewUnderMouse.ScreenToViewport (mouseEvent.Position); me = new () { Position = boundsPoint, Flags = mouseEvent.Flags, ScreenPosition = mouseEvent.Position, - View = view + View = deepestViewUnderMouse }; } diff --git a/Terminal.Gui/Application/Application.cs b/Terminal.Gui/Application/Application.cs index d37ba3513..2d4c67295 100644 --- a/Terminal.Gui/Application/Application.cs +++ b/Terminal.Gui/Application/Application.cs @@ -198,7 +198,7 @@ public static partial class Application IsInitialized = false; // Mouse - MouseEnteredView = null; + ViewsUnderMouse.Clear (); WantContinuousButtonPressedView = null; MouseEvent = null; GrabbedMouse = null; diff --git a/Terminal.Gui/View/Adornment/Adornment.cs b/Terminal.Gui/View/Adornment/Adornment.cs index 6c575ad99..f9a9db3ef 100644 --- a/Terminal.Gui/View/Adornment/Adornment.cs +++ b/Terminal.Gui/View/Adornment/Adornment.cs @@ -205,6 +205,7 @@ public class Adornment : View #region Mouse Support + // TODO: It's stoopid that this override changes the defn of the input coords from base. /// /// Indicates whether the specified Parent's SuperView-relative coordinates are within the Adornment's Thickness. /// @@ -229,15 +230,15 @@ public class Adornment : View /// protected internal override bool? OnMouseEnter (MouseEvent mouseEvent) { - // Invert Normal - if (Diagnostics.HasFlag (ViewDiagnosticFlags.MouseEnter) && ColorScheme != null) - { - var cs = new ColorScheme (ColorScheme) - { - Normal = new (ColorScheme.Normal.Background, ColorScheme.Normal.Foreground) - }; - ColorScheme = cs; - } + //// Invert Normal + //if (Diagnostics.HasFlag (ViewDiagnosticFlags.MouseEnter) && ColorScheme != null) + //{ + // var cs = new ColorScheme (ColorScheme) + // { + // Normal = new (ColorScheme.Normal.Background, ColorScheme.Normal.Foreground) + // }; + // ColorScheme = cs; + //} return base.OnMouseEnter (mouseEvent); } @@ -245,15 +246,15 @@ public class Adornment : View /// protected internal override bool OnMouseLeave (MouseEvent mouseEvent) { - // Invert Normal - if (Diagnostics.FastHasFlags (ViewDiagnosticFlags.MouseEnter) && ColorScheme != null) - { - var cs = new ColorScheme (ColorScheme) - { - Normal = new (ColorScheme.Normal.Background, ColorScheme.Normal.Foreground) - }; - ColorScheme = cs; - } + //// Invert Normal + //if (Diagnostics.FastHasFlags (ViewDiagnosticFlags.MouseEnter) && ColorScheme != null) + //{ + // var cs = new ColorScheme (ColorScheme) + // { + // Normal = new (ColorScheme.Normal.Background, ColorScheme.Normal.Foreground) + // }; + // ColorScheme = cs; + //} return base.OnMouseLeave (mouseEvent); } diff --git a/Terminal.Gui/View/View.Layout.cs b/Terminal.Gui/View/View.Layout.cs index 99b339da3..3e36cf338 100644 --- a/Terminal.Gui/View/View.Layout.cs +++ b/Terminal.Gui/View/View.Layout.cs @@ -1,5 +1,6 @@ #nullable enable using System.Diagnostics; +using Microsoft.CodeAnalysis; namespace Terminal.Gui; @@ -28,6 +29,23 @@ public partial class View // Layout APIs // CONCURRENCY: This method is not thread-safe. Undefined behavior and likely program crashes are exposed by unsynchronized access to InternalSubviews. internal static View? FindDeepestView (View? start, in Point location) { +<<<<<<< Updated upstream +======= + return GetViewsUnderMouse (location).TryPeek (out View? result) ? result : null; + } + + internal static Stack GetViewsUnderMouse (in Point location) + { + Stack viewsUnderMouse = new (); + + View? start = Application.Top; + + if (Application.Popover?.Visible == true) + { + start = Application.Popover; + } + +>>>>>>> Stashed changes Point currentLocation = location; while (start is { Visible: true } && start.Contains (currentLocation)) @@ -51,6 +69,8 @@ public partial class View // Layout APIs if (found is { }) { + //viewsUnderMouse.Push (found); + start = found; viewportOffset = found.Parent?.Frame.Location ?? Point.Empty; } @@ -70,6 +90,7 @@ public partial class View // Layout APIs currentLocation.Y = startOffsetY + start.Viewport.Y; // start is the deepest subview under the mouse; stop searching the subviews + viewsUnderMouse.Push (start); break; } } @@ -77,14 +98,16 @@ public partial class View // Layout APIs if (subview is null) { // No subview was found that's under the mouse, so we're done - return start; + viewsUnderMouse.Push (start); + return viewsUnderMouse; } // We found a subview of start that's under the mouse, continue... start = subview; + //viewsUnderMouse.Push (subview); } - return null; + return viewsUnderMouse; } // BUGBUG: This method interferes with Dialog/MessageBox default min/max size. diff --git a/Terminal.Gui/View/View.Mouse.cs b/Terminal.Gui/View/View.Mouse.cs index b66c07fe3..49a886c86 100644 --- a/Terminal.Gui/View/View.Mouse.cs +++ b/Terminal.Gui/View/View.Mouse.cs @@ -80,6 +80,11 @@ public partial class View // Mouse APIs return false; } + if (!WantMousePositionReports && mouseEvent.Flags == MouseFlags.ReportMousePosition) + { + return false; + } + if (OnMouseEvent (mouseEvent)) { // Technically mouseEvent.Handled should already be true if implementers of OnMouseEvent @@ -328,23 +333,24 @@ public partial class View // Mouse APIs } /// - /// Called by when the mouse enters . The view will - /// then receive mouse events until is called indicating the mouse has left - /// the view. + /// Called by when the mouse enters . will + /// be raised when the mouse is no longer over the . If another View occludes the current one, the + /// that View will also receive a MouseEnter event. /// /// /// - /// A view must be both enabled and visible to receive mouse events. + /// A view must be visible to receive Enter/Leave events. /// /// - /// This method calls to fire the event. + /// This method calls to raise the event. /// /// /// See for more information. /// /// /// - /// if the event was handled, otherwise. + /// if the event was handled, otherwise. Handling the event + /// prevents Views higher in the visible hierarchy from recieving Enter/Leave events. internal bool? NewMouseEnterEvent (MouseEvent mouseEvent) { if (!Enabled) @@ -375,29 +381,23 @@ public partial class View // Mouse APIs } /// - /// Called by when the mouse leaves . The view will - /// then no longer receive mouse events. + /// Called by when the mouse leaves . /// /// /// - /// A view must be both enabled and visible to receive mouse events. + /// A view must be visible to receive Enter/Leave events. /// /// - /// This method calls to fire the event. + /// This method calls to raise the event. /// /// /// See for more information. /// /// /// - /// if the event was handled, otherwise. + /// if the event was handled, otherwise. internal bool? NewMouseLeaveEvent (MouseEvent mouseEvent) { - if (!Enabled) - { - return true; - } - if (!CanBeVisible (this)) { return false; diff --git a/UICatalog/Scenarios/Mouse.cs b/UICatalog/Scenarios/Mouse.cs index 1e921c423..bf98b2d84 100644 --- a/UICatalog/Scenarios/Mouse.cs +++ b/UICatalog/Scenarios/Mouse.cs @@ -187,13 +187,18 @@ public class Mouse : Scenario Application.Shutdown (); } - public class MouseDemo : View + public class MouseDemo : Shortcut { private bool _button1PressedOnEnter; public MouseDemo () { CanFocus = true; + Id = "mouseDemo"; + Title = "Hi"; + Key = Key.A.WithAlt; + HelpText = "Help!"; + WantMousePositionReports = true; MouseEvent += (s, e) => { @@ -214,10 +219,14 @@ public class Mouse : Scenario MouseLeave += (s, e) => { - ColorScheme = Colors.ColorSchemes ["Dialog"]; + ColorScheme = Colors.ColorSchemes ["Menu"]; _button1PressedOnEnter = false; }; - MouseEnter += (s, e) => { _button1PressedOnEnter = e.MouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed); }; + MouseEnter += (s, e) => + { + ColorScheme = Colors.ColorSchemes ["Error"]; + _button1PressedOnEnter = e.MouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed); + }; } } }