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, IDisposable { /// /// Initializes a new instance of the class and subscribes to Application configuration property events. /// public MouseImpl () { // Subscribe to Application static property change events Application.IsMouseDisabledChanged += OnIsMouseDisabledChanged; } /// public IApplication? App { get; set; } /// public Point? LastMousePosition { get; set; } /// 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) { //Debug.Assert (App.Application.MainThreadId == Thread.CurrentThread.ManagedThreadId); if (App?.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 = App?.TopRunnableView?.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 (this, mouseEvent); if (mouseEvent.Handled) { return; } // Dismiss the Popover if the user presses mouse outside of it if (mouseEvent.IsPressed && App?.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.TopRunnable or Popover hierarchy, we don't want to // send the mouse event to the deepest view under the mouse. if (!View.IsInHierarchy (App?.TopRunnableView, deepestViewUnderMouse, true) && !View.IsInHierarchy (App?.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; } if (currentViewsUnderMouse is { }) { 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 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; MouseGrabView = null; } // Mouse grab functionality merged from MouseGrabHandler /// public void GrabMouse (View? view) { if (RaiseGrabbingMouseEvent (view)) { return; } if (view is null) { UngrabMouse(); return; } RaiseGrabbedMouseEvent (view); // MouseGrabView is only set if the application is initialized. MouseGrabView = view; } /// public void UngrabMouse () { if (MouseGrabView is null) { return; } 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 = MouseGrabView // Always set to the grab view. See Issue #4370 }; //System.Diagnostics.Debug.WriteLine ($"{nme.Flags};{nme.X};{nme.Y};{mouseGrabView}"); if (MouseGrabView?.NewMouseEvent (viewRelativeMouseEvent) is true || viewRelativeMouseEvent.IsSingleClicked) { 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; } // Event handler for Application static property changes private void OnIsMouseDisabledChanged (object? sender, ValueChangedEventArgs e) { IsMouseDisabled = e.NewValue; } /// public void Dispose () { // Unsubscribe from Application static property change events Application.IsMouseDisabledChanged -= OnIsMouseDisabledChanged; } }