#nullable enable using System.ComponentModel; namespace Terminal.Gui; public static partial class Application // Mouse handling { internal static Point? _lastMousePosition; /// /// Gets the most recent position of the mouse. /// public static Point? GetLastMousePosition () { return _lastMousePosition; } /// Disable or enable the mouse. The mouse is enabled by default. [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] public static bool IsMouseDisabled { get; set; } /// The current object that wants continuous mouse button pressed events. public static View? WantContinuousButtonPressedView { get; private set; } /// /// Gets the view that grabbed the mouse (e.g. for dragging). When this is set, all mouse events will be routed to /// this view until the view calls or the mouse is released. /// public static View? MouseGrabView { get; private set; } /// Invoked when a view wants to grab the mouse; can be canceled. public static event EventHandler? GrabbingMouse; /// Invoked when a view wants un-grab the mouse; can be canceled. public static event EventHandler? UnGrabbingMouse; /// Invoked after a view has grabbed the mouse. public static event EventHandler? GrabbedMouse; /// Invoked after a view has un-grabbed the mouse. public static event EventHandler? UnGrabbedMouse; /// /// Grabs the mouse, forcing all mouse events to be routed to the specified view until /// is called. /// /// View that will receive all mouse events until is invoked. public static void GrabMouse (View? view) { if (view is null || RaiseGrabbingMouseEvent (view)) { return; } RaiseGrabbedMouseEvent (view); MouseGrabView = view; } /// Releases the mouse grab, so mouse events will be routed to the view on which the mouse is. public static void UngrabMouse () { if (MouseGrabView is null) { return; } #if DEBUG_IDISPOSABLE ObjectDisposedException.ThrowIf (MouseGrabView.WasDisposed, MouseGrabView); #endif if (!RaiseUnGrabbingMouseEvent (MouseGrabView)) { View view = MouseGrabView; MouseGrabView = null; RaiseUnGrabbedMouseEvent (view); } } /// A delegate callback throws an exception. private static bool RaiseGrabbingMouseEvent (View? view) { if (view is null) { return false; } var evArgs = new GrabMouseEventArgs (view); GrabbingMouse?.Invoke (view, evArgs); return evArgs.Cancel; } /// A delegate callback throws an exception. private static bool RaiseUnGrabbingMouseEvent (View? view) { if (view is null) { return false; } var evArgs = new GrabMouseEventArgs (view); UnGrabbingMouse?.Invoke (view, evArgs); return evArgs.Cancel; } /// A delegate callback throws an exception. private static void RaiseGrabbedMouseEvent (View? view) { if (view is null) { return; } GrabbedMouse?.Invoke (view, new (view)); } /// A delegate callback throws an exception. private static void RaiseUnGrabbedMouseEvent (View? view) { if (view is null) { return; } UnGrabbedMouse?.Invoke (view, new (view)); } /// /// 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) { _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.GetViewsUnderMouse (mouseEvent.ScreenPosition); View? deepestViewUnderMouse = currentViewsUnderMouse.LastOrDefault (); if (deepestViewUnderMouse is { }) { #if DEBUG_IDISPOSABLE if (deepestViewUnderMouse.WasDisposed) { throw new ObjectDisposedException (deepestViewUnderMouse.GetType ().FullName); } #endif mouseEvent.View = deepestViewUnderMouse; } MouseEvent?.Invoke (null, mouseEvent); if (mouseEvent.Handled) { return; } if (HandleMouseGrab (deepestViewUnderMouse, mouseEvent)) { return; } WantContinuousButtonPressedView = deepestViewUnderMouse switch { { WantContinuousButtonPressed: true } => deepestViewUnderMouse, _ => null }; // 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; } // 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); WantContinuousButtonPressedView = deepestViewUnderMouse.WantContinuousButtonPressed ? deepestViewUnderMouse : null; 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 }; } } #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 . /// /// /// /// coordinates are screen-relative. /// /// /// will be the deepest view under the under the mouse. /// /// /// coordinates are view-relative. Only valid if is set. /// /// /// Use this evento to handle mouse events at the application level, before View-specific handling. /// /// public static event EventHandler? MouseEvent; #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved internal static bool HandleMouseGrab (View? deepestViewUnderMouse, MouseEventArgs mouseEvent) { if (MouseGrabView is { }) { #if DEBUG_IDISPOSABLE if (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; } internal static readonly List _cachedViewsUnderMouse = new (); /// /// 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 GetViewsUnderMouse(). 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; } } } }