diff --git a/Terminal.Gui/Application/Application.cs b/Terminal.Gui/Application/Application.cs index 947cab271b..47d0090e00 100644 --- a/Terminal.Gui/Application/Application.cs +++ b/Terminal.Gui/Application/Application.cs @@ -137,6 +137,9 @@ internal static void ResetState (bool ignoreDisposed = false) // Mouse _mouseEnteredView = null; + _lastViewButtonPressed = null; + _canProcessClickedEvent = true; + _isMouseDown = false; WantContinuousButtonPressedView = null; MouseEvent = null; GrabbedMouse = null; diff --git a/Terminal.Gui/Application/ApplicationMouse.cs b/Terminal.Gui/Application/ApplicationMouse.cs index 9f2a953390..ee5782f4e4 100644 --- a/Terminal.Gui/Application/ApplicationMouse.cs +++ b/Terminal.Gui/Application/ApplicationMouse.cs @@ -114,6 +114,9 @@ private static void OnUnGrabbedMouse (View view) // Used by OnMouseEvent to track the last view that was clicked on. internal static View? _mouseEnteredView; + internal static View? _lastViewButtonPressed; + internal static bool _canProcessClickedEvent = true; + internal static bool? _isMouseDown; /// Event fired when a mouse move or click occurs. Coordinates are screen relative. /// @@ -142,6 +145,38 @@ internal static void OnMouseEvent (MouseEvent mouseEvent) mouseEvent.View = view; } + if (_lastViewButtonPressed is null && mouseEvent.Flags is MouseFlags.Button1Pressed or MouseFlags.Button2Pressed or MouseFlags.Button3Pressed or MouseFlags.Button4Pressed) + { + _lastViewButtonPressed = view; + _isMouseDown = true; + } + else if (_lastViewButtonPressed is { } && mouseEvent.Flags is MouseFlags.Button1Released or MouseFlags.Button2Released or MouseFlags.Button3Released or MouseFlags.Button4Released) + { + if (_lastViewButtonPressed != view) + { + _canProcessClickedEvent = false; + } + + _lastViewButtonPressed = null; + _isMouseDown = false; + } + else if (!_canProcessClickedEvent && mouseEvent.Flags is MouseFlags.Button1Clicked or MouseFlags.Button2Clicked or MouseFlags.Button3Clicked or MouseFlags.Button4Clicked) + { + _canProcessClickedEvent = true; + _isMouseDown = null; + + return; + } + else if (!mouseEvent.Flags.HasFlag (MouseFlags.ReportMousePosition)) + { + _lastViewButtonPressed = null; + _isMouseDown = null; + } + else + { + _isMouseDown = null; + } + MouseEvent?.Invoke (null, mouseEvent); if (mouseEvent.Handled) @@ -160,7 +195,8 @@ internal static void OnMouseEvent (MouseEvent mouseEvent) Position = frameLoc, Flags = mouseEvent.Flags, ScreenPosition = mouseEvent.Position, - View = MouseGrabView + View = MouseGrabView, + IsMouseDown = _isMouseDown }; if ((MouseGrabView.Viewport with { Location = Point.Empty }).Contains (viewRelativeMouseEvent.Position) is false) @@ -170,7 +206,8 @@ internal static void OnMouseEvent (MouseEvent mouseEvent) } //System.Diagnostics.Debug.WriteLine ($"{nme.Flags};{nme.X};{nme.Y};{mouseGrabView}"); - if (MouseGrabView?.NewMouseEvent (viewRelativeMouseEvent) == true) + if ((MouseGrabView?.WantMousePositionReports == true || MouseGrabView?.WantContinuousButtonPressed == true) + && MouseGrabView?.NewMouseEvent (viewRelativeMouseEvent) == true) { return; } @@ -221,7 +258,8 @@ internal static void OnMouseEvent (MouseEvent mouseEvent) Position = frameLoc, Flags = mouseEvent.Flags, ScreenPosition = mouseEvent.Position, - View = view + View = view, + IsMouseDown = _isMouseDown }; } else if (view.ViewportToScreen (Rectangle.Empty with { Size = view.Viewport.Size }).Contains (mouseEvent.Position)) @@ -233,7 +271,8 @@ internal static void OnMouseEvent (MouseEvent mouseEvent) Position = viewportLocation, Flags = mouseEvent.Flags, ScreenPosition = mouseEvent.Position, - View = view + View = view, + IsMouseDown = _isMouseDown }; } diff --git a/Terminal.Gui/Input/Mouse.cs b/Terminal.Gui/Input/Mouse.cs index 3c8c98e91f..67a18f3169 100644 --- a/Terminal.Gui/Input/Mouse.cs +++ b/Terminal.Gui/Input/Mouse.cs @@ -134,6 +134,11 @@ public class MouseEvent /// public Point ScreenPosition { get; set; } + /// + /// Indicates if the current mouse event has first pressed , latest released or none . + /// + public bool? IsMouseDown { get; set; } + /// /// Indicates if the current mouse event has been processed. Set this value to to indicate the mouse /// event was handled. diff --git a/Terminal.Gui/View/Adornment/Border.cs b/Terminal.Gui/View/Adornment/Border.cs index 2931cd9ad2..560cf916d6 100644 --- a/Terminal.Gui/View/Adornment/Border.cs +++ b/Terminal.Gui/View/Adornment/Border.cs @@ -59,6 +59,7 @@ public Border (View parent) : base (parent) HighlightStyle |= HighlightStyle.Pressed; Highlight += Border_Highlight; + WantMousePositionReports = true; } #if SUBVIEW_BASED_BORDER diff --git a/Terminal.Gui/View/ViewMouse.cs b/Terminal.Gui/View/ViewMouse.cs index 24314f583c..aa190fcf0f 100644 --- a/Terminal.Gui/View/ViewMouse.cs +++ b/Terminal.Gui/View/ViewMouse.cs @@ -31,6 +31,25 @@ public partial class View /// public event EventHandler MouseClick; + /// + /// Event fired when the user presses and releases the mouse button twice in quick succession without + /// moving the mouse outside the view. + /// + /// + /// + /// The coordinates are relative to . + /// + /// + public event EventHandler MouseDoubleClick; + + /// Event fired when the user first presses the button down over a view. + /// + /// + /// The coordinates are relative to . + /// + /// + public event EventHandler MouseDown; + /// Event fired when the mouse moves into the View's . public event EventHandler MouseEnter; @@ -45,6 +64,39 @@ public partial class View /// Event fired when the mouse leaves the View's . public event EventHandler MouseLeave; + /// + /// Event fired when the user moves the mouse over a view, or if mouse was grabbed by the view. + /// Flags will indicate if a button is down. + /// + /// + /// + /// The coordinates are relative to . + /// + /// + public event EventHandler MouseMove; + + /// + /// Event fired when the user presses and releases the mouse button thrice in quick succession without + /// moving the mouse outside the view. + /// + /// + /// + /// The coordinates are relative to . + /// + /// + public event EventHandler MouseTripleClick; + + /// + /// Event fired when the user lets go of the mouse button. Only received if the mouse is over the view, + /// or it was grabbed by the view. + /// + /// + /// + /// The coordinates are relative to . + /// + /// + public event EventHandler MouseUp; + /// /// Processes a . This method is called by when a mouse /// event occurs. @@ -87,6 +139,30 @@ public partial class View return mouseEvent.Handled = true; } + if (mouseEvent.IsMouseDown == true) + { + if (OnMouseDown (new (mouseEvent))) + { + return true; + } + } + + if (mouseEvent.Flags.HasFlag (MouseFlags.ReportMousePosition)) + { + if (OnMouseMove (new (mouseEvent))) + { + return true; + } + } + + if (mouseEvent.IsMouseDown == false) + { + if (OnMouseUp (new (mouseEvent))) + { + return true; + } + } + if (HighlightStyle != HighlightStyle.None || WantContinuousButtonPressed) { if (HandlePressed (mouseEvent)) @@ -109,20 +185,30 @@ public partial class View || mouseEvent.Flags.HasFlag (MouseFlags.Button2Clicked) || mouseEvent.Flags.HasFlag (MouseFlags.Button3Clicked) || mouseEvent.Flags.HasFlag (MouseFlags.Button4Clicked) - || mouseEvent.Flags.HasFlag (MouseFlags.Button1DoubleClicked) + ) + { + // If it's a click, and we didn't handle it, then we'll call OnMouseClick + // We get here if the view did not handle the mouse event via OnMouseEvent/MouseEvent and + // it did not handle the press/release/clicked events via HandlePress/HandleRelease/HandleClicked + return OnMouseClick (new (mouseEvent)); + } + + if (mouseEvent.Flags.HasFlag (MouseFlags.Button1DoubleClicked) || mouseEvent.Flags.HasFlag (MouseFlags.Button2DoubleClicked) || mouseEvent.Flags.HasFlag (MouseFlags.Button3DoubleClicked) || mouseEvent.Flags.HasFlag (MouseFlags.Button4DoubleClicked) - || mouseEvent.Flags.HasFlag (MouseFlags.Button1TripleClicked) + ) + { + return OnMouseDoubleClick (new (mouseEvent)); + } + + if (mouseEvent.Flags.HasFlag (MouseFlags.Button1TripleClicked) || mouseEvent.Flags.HasFlag (MouseFlags.Button2TripleClicked) || mouseEvent.Flags.HasFlag (MouseFlags.Button3TripleClicked) || mouseEvent.Flags.HasFlag (MouseFlags.Button4TripleClicked) ) { - // If it's a click, and we didn't handle it, then we'll call OnMouseClick - // We get here if the view did not handle the mouse event via OnMouseEvent/MouseEvent and - // it did not handle the press/release/clicked events via HandlePress/HandleRelease/HandleClicked - return OnMouseClick (new (mouseEvent)); + return OnMouseTripleClick (new (mouseEvent)); } return false; @@ -135,6 +221,39 @@ public partial class View /// if mouse position reports are wanted; otherwise, . public virtual bool WantMousePositionReports { get; set; } + /// + /// Called when the user presses and releases the mouse button twice in quick succession without + /// moving the mouse outside the view. + /// + /// + /// + /// The coordinates are relative to . + /// + /// + /// + /// , if the event was handled, otherwise. + protected internal virtual bool OnMouseDoubleClick (MouseEventEventArgs args) + { + MouseDoubleClick?.Invoke (this, args); + + return args.Handled; + } + + /// Called when the user first presses the button down over a view's . + /// + /// + /// The coordinates are relative to . + /// + /// + /// + /// , if the event was handled, otherwise. + protected internal virtual bool OnMouseDown (MouseEventEventArgs args) + { + MouseDown?.Invoke (this, args); + + return args.Handled; + } + /// /// Called by when the mouse enters . The view will /// then receive mouse events until is called indicating the mouse has left @@ -207,6 +326,60 @@ protected internal virtual bool OnMouseLeave (MouseEvent mouseEvent) return args.Handled; } + /// + /// Called when the user moves the mouse over a view, or if mouse was grabbed by the view. + /// Flags will indicate if a button is down. + /// + /// + /// + /// The coordinates are relative to . + /// + /// + /// + /// , if the event was handled, otherwise. + protected internal virtual bool OnMouseMove (MouseEventEventArgs args) + { + MouseMove?.Invoke (this, args); + + return args.Handled; + } + + /// + /// Called when the user presses and releases the mouse button thrice in quick succession without + /// moving the mouse outside the view. + /// + /// + /// + /// The coordinates are relative to . + /// + /// + /// + /// , if the event was handled, otherwise. + protected internal virtual bool OnMouseTripleClick (MouseEventEventArgs args) + { + MouseTripleClick?.Invoke (this, args); + + return args.Handled; + } + + /// + /// Called when the user lets go of the mouse button. Only received if the mouse is over the view, + /// or it was grabbed by the view. + /// + /// + /// + /// The coordinates are relative to . + /// + /// + /// + /// , if the event was handled, otherwise. + protected internal virtual bool OnMouseUp (MouseEventEventArgs args) + { + MouseUp?.Invoke (this, args); + + return args.Handled; + } + /// /// Called when the view is to be highlighted. /// @@ -231,11 +404,13 @@ protected internal virtual bool OnMouseLeave (MouseEvent mouseEvent) return args.Cancel; } - /// Invokes the MouseClick event. + /// + /// Called when the user presses down and then releases the mouse over a view (they could move off in between). + /// If they press and release multiple times in quick succession this event will be called for each up action. + /// /// /// - /// Called when the mouse is either clicked or double-clicked. Check - /// to see which button was clicked. + /// The coordinates are relative to . /// /// /// , if the event was handled, otherwise. @@ -442,7 +617,7 @@ internal bool SetHighlight (HighlightStyle newHighlightStyle) // Enable override via virtual method and/or event HighlightStyle copy = HighlightStyle; - var args = new CancelEventArgs (ref copy, ref newHighlightStyle); + CancelEventArgs args = new CancelEventArgs (ref copy, ref newHighlightStyle); if (OnHighlight (args) == true) { @@ -477,7 +652,8 @@ internal bool SetHighlight (HighlightStyle newHighlightStyle) var cs = new ColorScheme (ColorScheme) { // Highlight the foreground focus color - Focus = new (ColorScheme.Focus.Foreground.GetHighlightColor (), ColorScheme.Focus.Background.GetHighlightColor ()) + Focus = new (ColorScheme.Focus.Foreground.GetHighlightColor (), ColorScheme.Focus.Background.GetHighlightColor ()), + HotFocus = new (ColorScheme.HotFocus.Foreground.GetHighlightColor (), ColorScheme.HotFocus.Background.GetHighlightColor ()) }; ColorScheme = cs; } @@ -486,7 +662,8 @@ internal bool SetHighlight (HighlightStyle newHighlightStyle) var cs = new ColorScheme (ColorScheme) { // Invert Focus color foreground/background. We can do this because we know the view is not going to be focused. - Normal = new (ColorScheme.Focus.Background, ColorScheme.Normal.Foreground) + Normal = new (ColorScheme.Focus.Background, ColorScheme.Normal.Foreground), + HotNormal = new (ColorScheme.HotFocus.Background, ColorScheme.HotNormal.Foreground) }; ColorScheme = cs; } diff --git a/UnitTests/Application/MouseTests.cs b/UnitTests/Application/MouseTests.cs index a3cf2d584d..2cb9e0836e 100644 --- a/UnitTests/Application/MouseTests.cs +++ b/UnitTests/Application/MouseTests.cs @@ -401,5 +401,118 @@ public void View_Is_Responsible_For_Calling_UnGrabMouse_Before_Being_Disposed () Assert.Equal (0, count); top.Dispose (); } + [Fact] + [AutoInitShutdown] + public void Clicked_Event_Only_Occurs_In_The_Same_View_That_Pressed_The_Mouse () + { + var view1Clicked = false; + var view2Clicked = false; + var view1 = new View { Id = "view1", CanFocus = true, Width = 10, Height = 1 }; + view1.MouseClick += (s, e) => view1Clicked = true; + var view2 = new View { Id = "view1", CanFocus = true, Y = 2, Width = 10, Height = 1 }; + view2.MouseClick += (s, e) => view2Clicked = true; + var top = new Toplevel (); + top.Add (view1, view2); + Application.Begin (top); + + Application.OnMouseEvent (new () { Position = new (0, 0), Flags = MouseFlags.Button1Pressed }); + Assert.Equal (Application._mouseEnteredView, view1); + Assert.Equal (Application._lastViewButtonPressed, view1); + Assert.True (Application._canProcessClickedEvent); + Assert.True (Application._isMouseDown); + Assert.False (view1Clicked); + Assert.False (view2Clicked); + + Application.OnMouseEvent (new () { Position = new (0, 2), Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition }); + Assert.Equal (Application._mouseEnteredView, view2); + Assert.Equal (Application._lastViewButtonPressed, view1); + Assert.True (Application._canProcessClickedEvent); + Assert.Null (Application._isMouseDown); + Assert.False (view1Clicked); + Assert.False (view2Clicked); + + Application.OnMouseEvent (new () { Position = new (0, 2), Flags = MouseFlags.Button1Released }); + Assert.Equal (Application._mouseEnteredView, view2); + Assert.Null (Application._lastViewButtonPressed); + Assert.False (Application._canProcessClickedEvent); + Assert.False (Application._isMouseDown); + Assert.False (view1Clicked); + Assert.False (view2Clicked); + + Application.OnMouseEvent (new () { Position = new (0, 2), Flags = MouseFlags.Button1Clicked }); + Assert.Equal (Application._mouseEnteredView, view2); + Assert.Null (Application._lastViewButtonPressed); + Assert.True (Application._canProcessClickedEvent); + Assert.Null (Application._isMouseDown); + Assert.False (view1Clicked); + Assert.False (view2Clicked); + + Application.OnMouseEvent (new () { Position = new (0, 2), Flags = MouseFlags.Button1Pressed }); + Assert.Equal (Application._mouseEnteredView, view2); + Assert.Equal (Application._lastViewButtonPressed, view2); + Assert.True (Application._canProcessClickedEvent); + Assert.True (Application._isMouseDown); + Assert.False (view1Clicked); + Assert.False (view2Clicked); + + Application.OnMouseEvent (new () { Position = new (0, 0), Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition }); + Assert.Equal (Application._mouseEnteredView, view1); + Assert.Equal (Application._lastViewButtonPressed, view2); + Assert.True (Application._canProcessClickedEvent); + Assert.Null (Application._isMouseDown); + Assert.False (view1Clicked); + Assert.False (view2Clicked); + + Application.OnMouseEvent (new () { Position = new (0, 2), Flags = MouseFlags.Button1Released }); + Assert.Equal (Application._mouseEnteredView, view2); + Assert.Null (Application._lastViewButtonPressed); + Assert.True (Application._canProcessClickedEvent); + Assert.False (Application._isMouseDown); + Assert.False (view1Clicked); + Assert.False (view2Clicked); + + Application.OnMouseEvent (new () { Position = new (0, 2), Flags = MouseFlags.Button1Clicked }); + Assert.Equal (Application._mouseEnteredView, view2); + Assert.Null (Application._lastViewButtonPressed); + Assert.True (Application._canProcessClickedEvent); + Assert.Null (Application._isMouseDown); + Assert.False (view1Clicked); + Assert.True (view2Clicked); + + Application.OnMouseEvent (new () { Position = new (0, 0), Flags = MouseFlags.Button1Pressed }); + Assert.Equal (Application._mouseEnteredView, view1); + Assert.Equal (Application._lastViewButtonPressed, view1); + Assert.True (Application._canProcessClickedEvent); + Assert.True (Application._isMouseDown); + Assert.False (view1Clicked); + Assert.True (view2Clicked); + + Application.OnMouseEvent (new () { Position = new (0, 2), Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition }); + Assert.Equal (Application._mouseEnteredView, view2); + Assert.Equal (Application._lastViewButtonPressed, view1); + Assert.True (Application._canProcessClickedEvent); + Assert.Null (Application._isMouseDown); + Assert.False (view1Clicked); + Assert.True (view2Clicked); + + Application.OnMouseEvent (new () { Position = new (0, 0), Flags = MouseFlags.Button1Released }); + Assert.Equal (Application._mouseEnteredView, view1); + Assert.Null (Application._lastViewButtonPressed); + Assert.True (Application._canProcessClickedEvent); + Assert.False (Application._isMouseDown); + Assert.False (view1Clicked); + Assert.True (view2Clicked); + + Application.OnMouseEvent (new () { Position = new (0, 0), Flags = MouseFlags.Button1Clicked }); + Assert.Equal (Application._mouseEnteredView, view1); + Assert.Null (Application._lastViewButtonPressed); + Assert.True (Application._canProcessClickedEvent); + Assert.Null (Application._isMouseDown); + Assert.True (view1Clicked); + Assert.True (view2Clicked); + + top.Dispose (); + } + #endregion } diff --git a/UnitTests/View/MouseTests.cs b/UnitTests/View/MouseTests.cs index a0bef94f58..2e43c9bc37 100644 --- a/UnitTests/View/MouseTests.cs +++ b/UnitTests/View/MouseTests.cs @@ -646,4 +646,53 @@ void View_Highlight (object sender, CancelEventArgs e) } } } + + [Fact] + [AutoInitShutdown] + public void Test_All_Mouse_Events () + { + var onMouseEvent = false; + var onMouseDown = false; + var onMouseMove = false; + var onMouseUp = false; + var onMouseClick = false; + var onMouseDoubleClick = false; + var onMouseTripleClick = false; + + var view = new View { CanFocus = true, Width = 10, Height = 1}; + view.MouseEvent += (s, e) => onMouseEvent = true; + view.MouseDown += (s, e) => onMouseDown = true; + view.MouseMove += (s, e) => onMouseMove = true; + view.MouseUp += (s, e) => onMouseUp = true; + view.MouseClick += (s, e) => onMouseClick = true; + view.MouseDoubleClick += (s, e) => onMouseDoubleClick = true; + view.MouseTripleClick += (s, e) => onMouseTripleClick = true; + + var top = new Toplevel (); + top.Add (view); + Application.Begin (top); + + Application.OnMouseEvent (new () { Position = new (0, 0), Flags = MouseFlags.WheeledDown }); + Assert.True (onMouseEvent); + + Application.OnMouseEvent (new () { Position = new (0, 0), Flags = MouseFlags.Button1Pressed }); + Assert.True (onMouseDown); + + Application.OnMouseEvent (new () { Position = new (0, 0), Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition }); + Assert.True (onMouseMove); + + Application.OnMouseEvent (new () { Position = new (0, 0), Flags = MouseFlags.Button1Released }); + Assert.True (onMouseUp); + + Application.OnMouseEvent (new () { Position = new (0, 0), Flags = MouseFlags.Button1Clicked }); + Assert.True (onMouseClick); + + Application.OnMouseEvent (new () { Position = new (0, 0), Flags = MouseFlags.Button1DoubleClicked }); + Assert.True (onMouseDoubleClick); + + Application.OnMouseEvent (new () { Position = new (0, 0), Flags = MouseFlags.Button1TripleClicked }); + Assert.True (onMouseTripleClick); + + top.Dispose (); + } } diff --git a/UnitTests/Views/ButtonTests.cs b/UnitTests/Views/ButtonTests.cs index 4ef86d3875..c97ff8439e 100644 --- a/UnitTests/Views/ButtonTests.cs +++ b/UnitTests/Views/ButtonTests.cs @@ -684,4 +684,45 @@ public void WantContinuousButtonPressed_True_ButtonPressRelease_Accepts (MouseFl button.Dispose (); } + [Fact] + [AutoInitShutdown] + public void WantMousePositionReports_False_And_WantContinuousButtonPressed_False_Focus_The_Another_Button_On_Mouse_Pressed_Released () + { + var button1 = new Button { Text = "Button1" }; + var acceptCount = 0; + button1.Accept += (s, e) => acceptCount++; + var button2 = new Button { Text = "Button2", X = Pos.Right (button1) + 1 }; + button2.Accept += (s, e) => acceptCount++; + var top = new Toplevel (); + top.Add (button1, button2); + Application.Begin (top); + + Assert.True (button1.HasFocus); + Application.OnMouseEvent (new () { Position = new (0, 0), Flags = MouseFlags.Button1Pressed }); + Assert.Equal (button1, Application.MouseGrabView); + Assert.Equal (0, acceptCount); + Assert.True (button1.HasFocus); + + Application.OnMouseEvent (new () { Position = new (13, 0), Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition}); + Assert.Equal (button2, Application.MouseGrabView); + Assert.Equal (0, acceptCount); + Assert.True (button2.HasFocus); + + Application.OnMouseEvent (new () { Position = new (13, 0), Flags = MouseFlags.Button1Released }); + Assert.Equal (button2, Application.MouseGrabView); + Assert.Equal (0, acceptCount); + Assert.True (button2.HasFocus); + + Application.OnMouseEvent (new () { Position = new (13, 0), Flags = MouseFlags.Button1Clicked }); + Assert.Equal (button2, Application.MouseGrabView); + Assert.Equal (0, acceptCount); + Assert.True (button2.HasFocus); + + Application.OnMouseEvent (new () { Position = new (13, 0), Flags = MouseFlags.Button1Clicked }); + Assert.Null (Application.MouseGrabView); + Assert.Equal (1, acceptCount); + Assert.True (button2.HasFocus); + + top.Dispose (); + } } \ No newline at end of file