#nullable enable
using System.ComponentModel;
using System.Diagnostics;
namespace Terminal.Gui;
public static partial class Application // Mouse handling
{
/// 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 || OnGrabbingMouse (view))
{
return;
}
OnGrabbedMouse (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 (!OnUnGrabbingMouse (MouseGrabView))
{
View view = MouseGrabView;
MouseGrabView = null;
OnUnGrabbedMouse (view);
}
}
/// A delegate callback throws an exception.
private static bool OnGrabbingMouse (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 OnUnGrabbingMouse (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 OnGrabbedMouse (View? view)
{
if (view is null)
{
return;
}
GrabbedMouse?.Invoke (view, new (view));
}
/// A delegate callback throws an exception.
private static void OnUnGrabbedMouse (View? view)
{
if (view is null)
{
return;
}
UnGrabbedMouse?.Invoke (view, new (view));
}
/// Event fired when a mouse move or click occurs. Coordinates are screen relative.
///
///
/// Use this event to receive mouse events in screen coordinates. Use to
/// receive mouse events relative to a .
///
/// The will contain the that contains the mouse coordinates.
///
public static event EventHandler? MouseEvent;
/// Called when a mouse event is raised by the driver.
/// 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 OnMouseEvent (MouseEvent mouseEvent)
{
if (IsMouseDisabled)
{
return;
}
List currentViewsUnderMouse = View.GetViewsUnderMouse (mouseEvent.Position);
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 (GrabMouse (deepestViewUnderMouse, mouseEvent))
{
return;
}
// 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.
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;
}
// TODO: Move this after call to RaiseMouseEnterLeaveEvents once MouseEnter/Leave don't use MouseEvent anymore.
MouseEvent? me;
if (deepestViewUnderMouse is Adornment adornment)
{
Point frameLoc = adornment.ScreenToFrame (mouseEvent.Position);
me = new ()
{
Position = frameLoc,
Flags = mouseEvent.Flags,
ScreenPosition = mouseEvent.Position,
View = deepestViewUnderMouse
};
}
else if (deepestViewUnderMouse.ViewportToScreen (Rectangle.Empty with { Size = deepestViewUnderMouse.Viewport.Size }).Contains (mouseEvent.Position))
{
Point viewportLocation = deepestViewUnderMouse.ScreenToViewport (mouseEvent.Position);
me = new ()
{
Position = viewportLocation,
Flags = mouseEvent.Flags,
ScreenPosition = mouseEvent.Position,
View = deepestViewUnderMouse
};
}
else
{
Debug.Fail ("This should never happen");
return;
}
RaiseMouseEnterLeaveEvents (me.ScreenPosition, currentViewsUnderMouse);
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 = deepestViewUnderMouse.ScreenToViewport (mouseEvent.Position);
me = new ()
{
Position = boundsPoint,
Flags = mouseEvent.Flags,
ScreenPosition = mouseEvent.Position,
View = deepestViewUnderMouse
};
}
}
internal static bool GrabMouse (View? deepestViewUnderMouse, MouseEvent 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.Position);
var viewRelativeMouseEvent = new MouseEvent
{
Position = frameLoc,
Flags = mouseEvent.Flags,
ScreenPosition = mouseEvent.Position,
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 ();
// TODO: Refactor MouseEnter/LeaveEvents to not take MouseEvent param.
///
/// 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);
bool 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;
}
}
}
}