From 82bf8f05468bc16cb7634af18a4bd15f102b532e Mon Sep 17 00:00:00 2001 From: BDisp Date: Mon, 13 Mar 2023 21:41:33 +0000 Subject: [PATCH] Fixes https://github.com/gui-cs/Terminal.Gui/issues/2413. GrabMouse ungrab other view that was grabbed and still need it. --- Terminal.Gui/Core/Application.cs | 58 +++++-- .../Core/EventArgs/GrabMouseEventArgs.cs | 29 ++++ Terminal.Gui/Core/Toplevel.cs | 23 ++- UICatalog/Scenarios/Clipping.cs | 9 +- UnitTests/TopLevels/ToplevelTests.cs | 154 ++++++++++++++++-- 5 files changed, 243 insertions(+), 30 deletions(-) create mode 100644 Terminal.Gui/Core/EventArgs/GrabMouseEventArgs.cs diff --git a/Terminal.Gui/Core/Application.cs b/Terminal.Gui/Core/Application.cs index f38e31b21..0f847c97f 100644 --- a/Terminal.Gui/Core/Application.cs +++ b/Terminal.Gui/Core/Application.cs @@ -162,7 +162,7 @@ namespace Terminal.Gui { if (alternateForwardKey != value) { var oldKey = alternateForwardKey; alternateForwardKey = value; - OnAlternateForwardKeyChanged (new KeyChangedEventArgs(oldKey,value)); + OnAlternateForwardKeyChanged (new KeyChangedEventArgs (oldKey, value)); } } } @@ -186,7 +186,7 @@ namespace Terminal.Gui { if (alternateBackwardKey != value) { var oldKey = alternateBackwardKey; alternateBackwardKey = value; - OnAlternateBackwardKeyChanged (new KeyChangedEventArgs(oldKey,value)); + OnAlternateBackwardKeyChanged (new KeyChangedEventArgs (oldKey, value)); } } } @@ -210,7 +210,7 @@ namespace Terminal.Gui { if (quitKey != value) { var oldKey = quitKey; quitKey = value; - OnQuitKeyChanged (new KeyChangedEventArgs(oldKey,value)); + OnQuitKeyChanged (new KeyChangedEventArgs (oldKey, value)); } } } @@ -720,6 +720,16 @@ namespace Terminal.Gui { /// public static View MouseGrabView => mouseGrabView; + /// + /// Event to be invoked when a view want grab the mouse which can be canceled. + /// + public static event EventHandler GrabbingMouse; + + /// + /// Event to be invoked when a view want ungrab the mouse which can be canceled. + /// + public static event EventHandler UnGrabbingMouse; + /// /// Event to be invoked when a view grab the mouse. /// @@ -739,9 +749,11 @@ namespace Terminal.Gui { { if (view == null) return; - OnGrabbedMouse (view); - mouseGrabView = view; - Driver.UncookMouse (); + if (!OnGrabbingMouse (view)) { + OnGrabbedMouse (view); + mouseGrabView = view; + Driver.UncookMouse (); + } } /// @@ -751,16 +763,36 @@ namespace Terminal.Gui { { if (mouseGrabView == null) return; - OnUnGrabbedMouse (mouseGrabView); - mouseGrabView = null; - Driver.CookMouse (); + if (!OnUnGrabbingMouse (mouseGrabView)) { + OnUnGrabbedMouse (mouseGrabView); + mouseGrabView = null; + Driver.CookMouse (); + } + } + + static bool OnGrabbingMouse (View view) + { + if (view == null) + return false; + var evArgs = new GrabMouseEventArgs (view); + GrabbingMouse?.Invoke (view, evArgs); + return evArgs.Cancel; + } + + static bool OnUnGrabbingMouse (View view) + { + if (view == null) + return false; + var evArgs = new GrabMouseEventArgs (view); + UnGrabbingMouse?.Invoke (view, evArgs); + return evArgs.Cancel; } static void OnGrabbedMouse (View view) { if (view == null) return; - GrabbedMouse?.Invoke (view, new ViewEventArgs(view)); + GrabbedMouse?.Invoke (view, new ViewEventArgs (view)); } static void OnUnGrabbedMouse (View view) @@ -1029,7 +1061,7 @@ namespace Terminal.Gui { Driver.Refresh (); } - NotifyNewRunState?.Invoke (toplevel, new RunStateEventArgs(rs)); + NotifyNewRunState?.Invoke (toplevel, new RunStateEventArgs (rs)); return rs; } @@ -1497,7 +1529,7 @@ namespace Terminal.Gui { static void OnNotifyStopRunState (Toplevel top) { if (ExitRunLoopAfterFirstIteration) { - NotifyStopRunState?.Invoke (top, new ToplevelEventArgs(top)); + NotifyStopRunState?.Invoke (top, new ToplevelEventArgs (top)); } } @@ -1530,7 +1562,7 @@ namespace Terminal.Gui { t.SetRelativeLayout (full); t.LayoutSubviews (); t.PositionToplevels (); - t.OnResized (new SizeChangedEventArgs(full.Size)); + t.OnResized (new SizeChangedEventArgs (full.Size)); } Refresh (); } diff --git a/Terminal.Gui/Core/EventArgs/GrabMouseEventArgs.cs b/Terminal.Gui/Core/EventArgs/GrabMouseEventArgs.cs new file mode 100644 index 000000000..952dc5ac6 --- /dev/null +++ b/Terminal.Gui/Core/EventArgs/GrabMouseEventArgs.cs @@ -0,0 +1,29 @@ +using System; + +namespace Terminal.Gui { + /// + /// Args for events that relate to specific + /// + public class GrabMouseEventArgs : EventArgs{ + + /// + /// Creates a new instance of the class. + /// + /// The view that the event is about. + public GrabMouseEventArgs (View view) + { + View = view; + } + + /// + /// The view that the event is about. + /// + public View View { get; } + + /// + /// Flag that allows the cancellation of the event. If set to in the + /// event handler, the event will be canceled. + /// + public bool Cancel { get; set; } + } +} diff --git a/Terminal.Gui/Core/Toplevel.cs b/Terminal.Gui/Core/Toplevel.cs index ca53bb8ac..efabbea3b 100644 --- a/Terminal.Gui/Core/Toplevel.cs +++ b/Terminal.Gui/Core/Toplevel.cs @@ -120,7 +120,7 @@ namespace Terminal.Gui { internal virtual void OnChildUnloaded (Toplevel top) { - ChildUnloaded?.Invoke (this, new ToplevelEventArgs(top)); + ChildUnloaded?.Invoke (this, new ToplevelEventArgs (top)); } internal virtual void OnChildLoaded (Toplevel top) @@ -159,7 +159,7 @@ namespace Terminal.Gui { internal virtual void OnActivate (Toplevel deactivated) { - Activate?.Invoke (this, new ToplevelEventArgs(deactivated)); + Activate?.Invoke (this, new ToplevelEventArgs (deactivated)); } /// @@ -221,6 +221,9 @@ namespace Terminal.Gui { { ColorScheme = Colors.TopLevel; + Application.GrabbingMouse += Application_GrabbingMouse; + Application.UnGrabbingMouse += Application_UnGrabbingMouse; + // Things this view knows how to do AddCommand (Command.QuitToplevel, () => { QuitToplevel (); return true; }); AddCommand (Command.Suspend, () => { Driver.Suspend (); ; return true; }); @@ -256,6 +259,20 @@ namespace Terminal.Gui { AddKeyBinding (Key.L | Key.CtrlMask, Command.Refresh); } + private void Application_UnGrabbingMouse (object sender, GrabMouseEventArgs e) + { + if (Application.MouseGrabView == this && dragPosition.HasValue) { + e.Cancel = true; + } + } + + private void Application_GrabbingMouse (object sender, GrabMouseEventArgs e) + { + if (Application.MouseGrabView == this && dragPosition.HasValue) { + e.Cancel = true; + } + } + /// /// Invoked when the is changed. /// @@ -824,8 +841,8 @@ namespace Terminal.Gui { } if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Released) && dragPosition.HasValue) { - Application.UngrabMouse (); dragPosition = null; + Application.UngrabMouse (); } //System.Diagnostics.Debug.WriteLine ($"dragPosition after: {dragPosition.HasValue}"); diff --git a/UICatalog/Scenarios/Clipping.cs b/UICatalog/Scenarios/Clipping.cs index 606902bd4..58f89b7a3 100644 --- a/UICatalog/Scenarios/Clipping.cs +++ b/UICatalog/Scenarios/Clipping.cs @@ -37,7 +37,8 @@ namespace UICatalog.Scenarios { Y = 3, Width = Dim.Fill (3), Height = Dim.Fill (3), - ColorScheme = Colors.Dialog + ColorScheme = Colors.Dialog, + Id = "1" }; var embedded2 = new Window ("2") { @@ -45,7 +46,8 @@ namespace UICatalog.Scenarios { Y = 3, Width = Dim.Fill (3), Height = Dim.Fill (3), - ColorScheme = Colors.Error + ColorScheme = Colors.Error, + Id = "2" }; embedded1.Add (embedded2); @@ -54,7 +56,8 @@ namespace UICatalog.Scenarios { Y = 3, Width = Dim.Fill (3), Height = Dim.Fill (3), - ColorScheme = Colors.TopLevel + ColorScheme = Colors.TopLevel, + Id = "3" }; var testButton = new Button (2, 2, "click me"); diff --git a/UnitTests/TopLevels/ToplevelTests.cs b/UnitTests/TopLevels/ToplevelTests.cs index 1da5891e5..12763a163 100644 --- a/UnitTests/TopLevels/ToplevelTests.cs +++ b/UnitTests/TopLevels/ToplevelTests.cs @@ -154,28 +154,28 @@ namespace Terminal.Gui.TopLevelTests { var eventInvoked = ""; - top.ChildUnloaded += (s,e) => eventInvoked = "ChildUnloaded"; + top.ChildUnloaded += (s, e) => eventInvoked = "ChildUnloaded"; top.OnChildUnloaded (top); Assert.Equal ("ChildUnloaded", eventInvoked); - top.ChildLoaded += (s,e) => eventInvoked = "ChildLoaded"; + top.ChildLoaded += (s, e) => eventInvoked = "ChildLoaded"; top.OnChildLoaded (top); Assert.Equal ("ChildLoaded", eventInvoked); top.Closed += (s, e) => eventInvoked = "Closed"; top.OnClosed (top); Assert.Equal ("Closed", eventInvoked); - top.Closing += (s,e) => eventInvoked = "Closing"; + top.Closing += (s, e) => eventInvoked = "Closing"; top.OnClosing (new ToplevelClosingEventArgs (top)); Assert.Equal ("Closing", eventInvoked); top.AllChildClosed += (s, e) => eventInvoked = "AllChildClosed"; top.OnAllChildClosed (); Assert.Equal ("AllChildClosed", eventInvoked); - top.ChildClosed += (s,e) => eventInvoked = "ChildClosed"; + top.ChildClosed += (s, e) => eventInvoked = "ChildClosed"; top.OnChildClosed (top); Assert.Equal ("ChildClosed", eventInvoked); - top.Deactivate += (s,e) => eventInvoked = "Deactivate"; + top.Deactivate += (s, e) => eventInvoked = "Deactivate"; top.OnDeactivate (top); Assert.Equal ("Deactivate", eventInvoked); - top.Activate += (s,e) => eventInvoked = "Activate"; + top.Activate += (s, e) => eventInvoked = "Activate"; top.OnActivate (top); Assert.Equal ("Activate", eventInvoked); top.Loaded += (s, e) => eventInvoked = "Loaded"; @@ -356,7 +356,7 @@ namespace Terminal.Gui.TopLevelTests { var top = Application.Top; top.Add (win1, win2); top.Loaded += (s, e) => isRunning = true; - top.Closing += (s,e) => isRunning = false; + top.Closing += (s, e) => isRunning = false; Application.Begin (top); top.Running = true; @@ -589,9 +589,9 @@ namespace Terminal.Gui.TopLevelTests { void View_Added (object sender, SuperViewChangedEventArgs e) { - Assert.Throws (() => Application.Top.AlternateForwardKeyChanged += (s,e) => alternateForwardKey = e.OldKey); - Assert.Throws (() => Application.Top.AlternateBackwardKeyChanged += (s,e) => alternateBackwardKey = e.OldKey); - Assert.Throws (() => Application.Top.QuitKeyChanged += (s,e) => quitKey = e.OldKey); + Assert.Throws (() => Application.Top.AlternateForwardKeyChanged += (s, e) => alternateForwardKey = e.OldKey); + Assert.Throws (() => Application.Top.AlternateBackwardKeyChanged += (s, e) => alternateBackwardKey = e.OldKey); + Assert.Throws (() => Application.Top.QuitKeyChanged += (s, e) => quitKey = e.OldKey); Assert.False (wasAdded); wasAdded = true; view.Added -= View_Added; @@ -621,7 +621,7 @@ namespace Terminal.Gui.TopLevelTests { void View_Initialized (object sender, EventArgs e) { - Application.Top.AlternateForwardKeyChanged += (s,e) => alternateForwardKey = e.OldKey; + Application.Top.AlternateForwardKeyChanged += (s, e) => alternateForwardKey = e.OldKey; Application.Top.AlternateBackwardKeyChanged += (s, e) => alternateBackwardKey = e.OldKey; Application.Top.QuitKeyChanged += (s, e) => quitKey = e.OldKey; } @@ -1081,5 +1081,137 @@ namespace Terminal.Gui.TopLevelTests { Assert.Equal (new Rect (1, 3, 10, 5), view.Frame); Assert.Equal (new Rect (0, 0, 10, 5), view.NeedDisplay); } + + [Fact, AutoInitShutdown] + public void Toplevel_Inside_ScrollView_MouseGrabView () + { + var scrollView = new ScrollView () { + X = 3, + Y = 3, + Width = 40, + Height = 16, + ContentSize = new Size (200, 100) + }; + var win = new Window ("Window") { X = 3, Y = 3, Width = Dim.Fill (3), Height = Dim.Fill (3) }; + scrollView.Add (win); + var top = Application.Top; + top.Add (scrollView); + Application.Begin (top); + + Assert.Equal (new Rect (0, 0, 80, 25), top.Frame); + Assert.Equal (new Rect (3, 3, 40, 16), scrollView.Frame); + Assert.Equal (new Rect (0, 0, 200, 100), scrollView.Subviews [0].Frame); + Assert.Equal (new Rect (3, 3, 194, 94), win.Frame); + TestHelpers.AssertDriverContentsWithFrameAre (@" + ▲ + ┬ + │ + ┌ Window ───────────────────────────┴ + │ ░ + │ ░ + │ ░ + │ ░ + │ ░ + │ ░ + │ ░ + │ ░ + │ ░ + │ ░ + │ ▼ + ◄├──────┤░░░░░░░░░░░░░░░░░░░░░░░░░░░░░► ", output); + + ReflectionTools.InvokePrivate ( + typeof (Application), + "ProcessMouseEvent", + new MouseEvent () { + X = 6, + Y = 6, + Flags = MouseFlags.Button1Pressed + }); + Assert.Equal (win, Application.MouseGrabView); + Assert.Equal (new Rect (3, 3, 194, 94), win.Frame); + + ReflectionTools.InvokePrivate ( + typeof (Application), + "ProcessMouseEvent", + new MouseEvent () { + X = 9, + Y = 9, + Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition + }); + Assert.Equal (win, Application.MouseGrabView); + top.SetNeedsLayout (); + top.LayoutSubviews (); + Assert.Equal (new Rect (6, 6, 191, 91), win.Frame); + Application.Refresh (); + TestHelpers.AssertDriverContentsWithFrameAre (@" + ▲ + ┬ + │ + ┴ + ░ + ░ + ┌ Window ────────────────────────░ + │ ░ + │ ░ + │ ░ + │ ░ + │ ░ + │ ░ + │ ░ + │ ▼ + ◄├──────┤░░░░░░░░░░░░░░░░░░░░░░░░░░░░░► ", output); + + ReflectionTools.InvokePrivate ( + typeof (Application), + "ProcessMouseEvent", + new MouseEvent () { + X = 5, + Y = 5, + Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition + }); + Assert.Equal (win, Application.MouseGrabView); + top.SetNeedsLayout (); + top.LayoutSubviews (); + Assert.Equal (new Rect (2, 2, 195, 95), win.Frame); + Application.Refresh (); + TestHelpers.AssertDriverContentsWithFrameAre (@" + ▲ + ┬ + ┌ Window ────────────────────────────│ + │ ┴ + │ ░ + │ ░ + │ ░ + │ ░ + │ ░ + │ ░ + │ ░ + │ ░ + │ ░ + │ ░ + │ ▼ + ◄├──────┤░░░░░░░░░░░░░░░░░░░░░░░░░░░░░► ", output); + + ReflectionTools.InvokePrivate ( + typeof (Application), + "ProcessMouseEvent", + new MouseEvent () { + X = 5, + Y = 5, + Flags = MouseFlags.Button1Released + }); + Assert.Null (Application.MouseGrabView); + + ReflectionTools.InvokePrivate ( + typeof (Application), + "ProcessMouseEvent", + new MouseEvent () { + X = 4, + Y = 4, + Flags = MouseFlags.ReportMousePosition + }); + Assert.Equal (scrollView, Application.MouseGrabView); + } } } \ No newline at end of file