#nullable enable using System.ComponentModel; namespace Terminal.Gui; public partial class View // Mouse APIs { /// Gets the mouse bindings for this view. public MouseBindings MouseBindings { get; internal set; } = null!; private void SetupMouse () { MouseBindings = new (); // TODO: Should the default really work with any button or just button1? MouseBindings.Add (MouseFlags.Button1Clicked, Command.Select); MouseBindings.Add (MouseFlags.Button2Clicked, Command.Select); MouseBindings.Add (MouseFlags.Button3Clicked, Command.Select); MouseBindings.Add (MouseFlags.Button4Clicked, Command.Select); MouseBindings.Add (MouseFlags.Button1Clicked | MouseFlags.ButtonCtrl, Command.Select); } /// /// Invokes the Commands bound to the MouseFlags specified by . /// See for an overview of Terminal.Gui mouse APIs. /// /// The mouse event passed. /// /// if no command was invoked; input processing should continue. /// if at least one command was invoked and was not handled (or cancelled); input processing /// should continue. /// if at least one command was invoked and handled (or cancelled); input processing should /// stop. /// protected bool? InvokeCommandsBoundToMouse (MouseEventArgs mouseEventArgs) { if (!MouseBindings.TryGet (mouseEventArgs.Flags, out MouseBinding binding)) { return null; } binding.MouseEventArgs = mouseEventArgs; return InvokeCommands (binding.Commands, binding); } #region MouseEnterLeave private bool _hovering; private ColorScheme? _savedNonHoverColorScheme; /// /// INTERNAL Called by when the mouse moves over the View's /// . /// will /// be raised when the mouse is no longer over the . If another View occludes this View, the /// that View will also receive MouseEnter/Leave events. /// /// /// /// if the event was canceled, if not, if the /// view is not visible. Cancelling the event /// prevents Views higher in the visible hierarchy from receiving Enter/Leave events. /// internal bool? NewMouseEnterEvent (CancelEventArgs eventArgs) { // Pre-conditions if (!CanBeVisible (this)) { return null; } // Cancellable event if (OnMouseEnter (eventArgs)) { return true; } MouseEnter?.Invoke (this, eventArgs); _hovering = !eventArgs.Cancel; if (eventArgs.Cancel) { return true; } // Post-conditions if (HighlightStyle.HasFlag (HighlightStyle.Hover) || Diagnostics.HasFlag (ViewDiagnosticFlags.Hover)) { HighlightStyle copy = HighlightStyle; var hover = HighlightStyle.Hover; CancelEventArgs args = new (ref copy, ref hover); if (RaiseHighlight (args) || args.Cancel) { return args.Cancel; } ColorScheme? cs = ColorScheme; if (cs is null) { cs = new (); } _savedNonHoverColorScheme = cs; ColorScheme = ColorScheme?.GetHighlightColorScheme (); } return false; } /// /// Called when the mouse moves over the View's and no other non-SubView occludes it. /// will /// be raised when the mouse is no longer over the . /// /// /// /// A view must be visible to receive Enter events (Leave events are always received). /// /// /// If the event is cancelled, the mouse event will not be propagated to other views and /// will not be raised. /// /// /// Adornments receive MouseEnter/Leave events when the mouse is over the Adornment's . /// /// /// See for more information. /// /// /// /// /// if the event was canceled, if not. Cancelling the event /// prevents Views higher in the visible hierarchy from receiving Enter/Leave events. /// protected virtual bool OnMouseEnter (CancelEventArgs eventArgs) { return false; } /// /// Raised when the mouse moves over the View's . will /// be raised when the mouse is no longer over the . If another View occludes this View, the /// that View will also receive MouseEnter/Leave events. /// /// /// /// A view must be visible to receive Enter events (Leave events are always received). /// /// /// If the event is cancelled, the mouse event will not be propagated to other views. /// /// /// Adornments receive MouseEnter/Leave events when the mouse is over the Adornment's . /// /// /// Set to if the event was canceled, /// if not. Cancelling the event /// prevents Views higher in the visible hierarchy from receiving Enter/Leave events. /// /// /// See for more information. /// /// public event EventHandler? MouseEnter; /// /// INTERNAL Called by when the mouse leaves , or is /// occluded /// by another non-SubView. /// /// /// /// This method calls and raises the event. /// /// /// Adornments receive MouseEnter/Leave events when the mouse is over the Adornment's . /// /// /// See for more information. /// /// internal void NewMouseLeaveEvent () { // Pre-conditions // Non-cancellable event OnMouseLeave (); MouseLeave?.Invoke (this, EventArgs.Empty); // Post-conditions _hovering = false; if (HighlightStyle.HasFlag (HighlightStyle.Hover) || Diagnostics.HasFlag (ViewDiagnosticFlags.Hover)) { HighlightStyle copy = HighlightStyle; var hover = HighlightStyle.None; RaiseHighlight (new (ref copy, ref hover)); if (_savedNonHoverColorScheme is { }) { ColorScheme = _savedNonHoverColorScheme; _savedNonHoverColorScheme = null; } } } /// /// Called when the mouse moves outside View's , or is occluded by another non-SubView. /// /// /// /// Adornments receive MouseEnter/Leave events when the mouse is over the Adornment's . /// /// /// See for more information. /// /// protected virtual void OnMouseLeave () { } /// /// Raised when the mouse moves outside View's , or is occluded by another non-SubView. /// /// /// /// Adornments receive MouseEnter/Leave events when the mouse is over the Adornment's . /// /// /// See for more information. /// /// public event EventHandler? MouseLeave; #endregion MouseEnterLeave #region Low Level Mouse Events /// Gets or sets whether the wants continuous button pressed events. public virtual bool WantContinuousButtonPressed { get; set; } /// Gets or sets whether the wants mouse position reports. /// if mouse position reports are wanted; otherwise, . public bool WantMousePositionReports { get; set; } /// /// Processes a new . This method is called by when a /// mouse /// event occurs. /// /// /// /// A view must be both enabled and visible to receive mouse events. /// /// /// This method raises /; if not handled, and one of the /// mouse buttons was clicked, the / event will be raised /// /// /// See for more information. /// /// /// If is , the / /// event /// will be raised on any new mouse event where indicates a button /// is pressed. /// /// /// /// if the event was handled, otherwise. public bool? NewMouseEvent (MouseEventArgs mouseEvent) { // Pre-conditions if (!Enabled) { // A disabled view should not eat mouse events return false; } if (!CanBeVisible (this)) { return false; } if (!WantMousePositionReports && mouseEvent.Flags == MouseFlags.ReportMousePosition) { return false; } // Cancellable event if (RaiseMouseEvent (mouseEvent) || mouseEvent.Handled) { return true; } // Post-Conditions if (HighlightStyle != HighlightStyle.None || WantContinuousButtonPressed) { if (WhenGrabbedHandlePressed (mouseEvent)) { return mouseEvent.Handled; } if (WhenGrabbedHandleReleased (mouseEvent)) { return mouseEvent.Handled; } if (WhenGrabbedHandleClicked (mouseEvent)) { return mouseEvent.Handled; } } // 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 if (mouseEvent.IsSingleDoubleOrTripleClicked) { return RaiseMouseClickEvent (mouseEvent); } if (mouseEvent.IsWheel) { return RaiseMouseWheelEvent (mouseEvent); } return false; } /// /// Raises the / event. /// /// /// , if the event was handled, otherwise. public bool RaiseMouseEvent (MouseEventArgs mouseEvent) { if (OnMouseEvent (mouseEvent) || mouseEvent.Handled) { return true; } MouseEvent?.Invoke (this, mouseEvent); return mouseEvent.Handled; } /// Called when a mouse event occurs within the view's . /// /// /// The coordinates are relative to . /// /// /// /// , if the event was handled, otherwise. protected virtual bool OnMouseEvent (MouseEventArgs mouseEvent) { return false; } /// Raised when a mouse event occurs. /// /// /// The coordinates are relative to . /// /// public event EventHandler? MouseEvent; #endregion Low Level Mouse Events #region Mouse Pressed Events /// /// INTERNAL For cases where the view is grabbed and the mouse is clicked, this method handles the released event /// (typically /// when or are set). /// /// /// Marked internal just to support unit tests /// /// /// , if the event was handled, otherwise. internal bool WhenGrabbedHandleReleased (MouseEventArgs mouseEvent) { mouseEvent.Handled = false; if (mouseEvent.IsReleased) { if (Application.MouseGrabView == this) { SetPressedHighlight (HighlightStyle.None); } return mouseEvent.Handled = true; } return false; } /// /// INTERNAL For cases where the view is grabbed and the mouse is clicked, this method handles the released event /// (typically /// when or are set). /// /// /// /// Marked internal just to support unit tests /// /// /// /// , if the event was handled, otherwise. private bool WhenGrabbedHandlePressed (MouseEventArgs mouseEvent) { mouseEvent.Handled = false; if (mouseEvent.IsPressed) { // The first time we get pressed event, grab the mouse and set focus if (Application.MouseGrabView != this) { Application.GrabMouse (this); if (!HasFocus && CanFocus) { // Set the focus, but don't invoke Accept SetFocus (); } mouseEvent.Handled = true; } if (Viewport.Contains (mouseEvent.Position)) { if (this is not Adornment && SetPressedHighlight (HighlightStyle.HasFlag (HighlightStyle.Pressed) ? HighlightStyle.Pressed : HighlightStyle.None)) { return true; } } else { if (this is not Adornment && SetPressedHighlight (HighlightStyle.HasFlag (HighlightStyle.PressedOutside) ? HighlightStyle.PressedOutside : HighlightStyle.None)) { return true; } } if (WantContinuousButtonPressed && Application.MouseGrabView == this) { return RaiseMouseClickEvent (mouseEvent); } return mouseEvent.Handled = true; } return false; } #endregion Mouse Pressed Events #region Mouse Click Events /// Raises the / event. /// /// /// Called when the mouse is either clicked or double-clicked. /// /// /// If is , will be invoked on every mouse event /// where /// the mouse button is pressed. /// /// /// , if the event was handled, otherwise. protected bool RaiseMouseClickEvent (MouseEventArgs args) { // Pre-conditions if (!Enabled) { // QUESTION: Is this right? Should a disabled view eat mouse clicks? return args.Handled = false; } // Cancellable event if (OnMouseClick (args) || args.Handled) { return args.Handled; } MouseClick?.Invoke (this, args); if (args.Handled) { return true; } // Post-conditions // By default, this will raise Selecting/OnSelecting - Subclasses can override this via AddCommand (Command.Select ...). args.Handled = InvokeCommandsBoundToMouse (args) == true; return args.Handled; } /// /// Called when a mouse click occurs. Check to see which button was clicked. /// /// /// /// Called when the mouse is either clicked or double-clicked. /// /// /// If is , will be called on every mouse event /// where /// the mouse button is pressed. /// /// /// /// , if the event was handled, otherwise. protected virtual bool OnMouseClick (MouseEventArgs args) { return false; } /// Raised when a mouse click occurs. /// /// /// Raised when the mouse is either clicked or double-clicked. /// /// /// If is , will be raised on every mouse event /// where /// the mouse button is pressed. /// /// public event EventHandler? MouseClick; /// /// INTERNAL For cases where the view is grabbed and the mouse is clicked, this method handles the click event /// (typically /// when or are set). /// /// /// Marked internal just to support unit tests /// /// /// , if the event was handled, otherwise. internal bool WhenGrabbedHandleClicked (MouseEventArgs mouseEvent) { mouseEvent.Handled = false; if (Application.MouseGrabView == this && mouseEvent.IsSingleClicked) { // We're grabbed. Clicked event comes after the last Release. This is our signal to ungrab Application.UngrabMouse (); if (SetPressedHighlight (HighlightStyle.None)) { return true; } // If mouse is still in bounds, generate a click if (!WantMousePositionReports && Viewport.Contains (mouseEvent.Position)) { return RaiseMouseClickEvent (mouseEvent); } return mouseEvent.Handled = true; } return false; } #endregion Mouse Clicked Events #region Mouse Wheel Events /// Raises the / event. /// /// /// , if the event was handled, otherwise. protected bool RaiseMouseWheelEvent (MouseEventArgs args) { // Pre-conditions if (!Enabled) { // QUESTION: Is this right? Should a disabled view eat mouse? return args.Handled = false; } // Cancellable event if (OnMouseWheel (args) || args.Handled) { return args.Handled; } MouseWheel?.Invoke (this, args); if (args.Handled) { return true; } args.Handled = InvokeCommandsBoundToMouse (args) == true; return args.Handled; } /// /// Called when a mouse wheel event occurs. Check to see which wheel was moved was /// clicked. /// /// /// /// /// , if the event was handled, otherwise. protected virtual bool OnMouseWheel (MouseEventArgs args) { return false; } /// Raised when a mouse wheel event occurs. /// /// public event EventHandler? MouseWheel; #endregion Mouse Wheel Events #region Highlight Handling // Used for Pressed highlighting private ColorScheme? _savedHighlightColorScheme; /// /// Gets or sets whether the will be highlighted visually by mouse interaction. /// public HighlightStyle HighlightStyle { get; set; } /// /// INTERNAL Raises the event. Returns if the event was handled, /// otherwise. /// /// /// private bool RaiseHighlight (CancelEventArgs args) { if (OnHighlight (args)) { return true; } Highlight?.Invoke (this, args); //if (args.Cancel) //{ // return true; //} //args.Cancel = InvokeCommandsBoundToMouse (args) == true; return args.Cancel; } /// /// Called when the view is to be highlighted. The passed in the event indicates the /// highlight style that will be applied. The view can modify the highlight style by setting the /// property. /// /// /// Set the property to , to cancel, indicating custom /// highlighting. /// /// , to cancel, indicating custom highlighting. protected virtual bool OnHighlight (CancelEventArgs args) { return false; } /// /// Raised when the view is to be highlighted. The passed in the event indicates the /// highlight style that will be applied. The view can modify the highlight style by setting the /// property. /// Set to , to cancel, indicating custom highlighting. /// public event EventHandler>? Highlight; /// /// INTERNAL Enables the highlight for the view when the mouse is pressed. Called from OnMouseEvent. /// /// /// /// Set to and/or /// to enable. /// /// /// Calls and raises the event. /// /// /// Marked internal just to support unit tests /// /// /// , if the Highlight event was handled, otherwise. internal bool SetPressedHighlight (HighlightStyle newHighlightStyle) { // TODO: Make the highlight colors configurable if (!CanFocus) { return false; } HighlightStyle copy = HighlightStyle; CancelEventArgs args = new (ref copy, ref newHighlightStyle); if (RaiseHighlight (args) || args.Cancel) { return true; } // For 3D Pressed Style - Note we don't care about canceling the event here Margin?.RaiseHighlight (args); args.Cancel = false; // Just in case if (args.NewValue.HasFlag (HighlightStyle.Pressed) || args.NewValue.HasFlag (HighlightStyle.PressedOutside)) { if (_savedHighlightColorScheme is null && ColorScheme is { }) { _savedHighlightColorScheme ??= ColorScheme; if (CanFocus) { var cs = new ColorScheme (ColorScheme) { // Highlight the foreground focus color Focus = new (ColorScheme.Focus.Foreground.GetHighlightColor (), ColorScheme.Focus.Background.GetHighlightColor ()) }; ColorScheme = cs; } else { 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) }; ColorScheme = cs; } } // Return false since we don't want to eat the event return false; } if (args.NewValue == HighlightStyle.None) { // Unhighlight if (_savedHighlightColorScheme is { }) { ColorScheme = _savedHighlightColorScheme; _savedHighlightColorScheme = null; } } return false; } #endregion Highlight Handling /// /// INTERNAL: Gets the Views that are under the mouse at , including Adornments. /// /// /// If any transparent views will be ignored. /// internal static List GetViewsUnderMouse (in Point location, bool ignoreTransparent = false) { List viewsUnderMouse = new (); View? start = Application.Top; Point currentLocation = location; while (start is { Visible: true } && start.Contains (currentLocation)) { viewsUnderMouse.Add (start); Adornment? found = null; if (start is not Adornment) { if (start.Margin is { } && start.Margin.Contains (currentLocation)) { found = start.Margin; } else if (start.Border is { } && start.Border.Contains (currentLocation)) { found = start.Border; } else if (start.Padding is { } && start.Padding.Contains (currentLocation)) { found = start.Padding; } } Point viewportOffset = start.GetViewportOffsetFromFrame (); if (found is { }) { start = found; viewsUnderMouse.Add (start); viewportOffset = found.Parent?.Frame.Location ?? Point.Empty; } int startOffsetX = currentLocation.X - (start.Frame.X + viewportOffset.X); int startOffsetY = currentLocation.Y - (start.Frame.Y + viewportOffset.Y); View? subview = null; for (int i = start.InternalSubViews.Count - 1; i >= 0; i--) { if (start.InternalSubViews [i].Visible && start.InternalSubViews [i].Contains (new (startOffsetX + start.Viewport.X, startOffsetY + start.Viewport.Y)) && (!ignoreTransparent || !start.InternalSubViews [i].ViewportSettings.HasFlag (ViewportSettings.TransparentMouse))) { subview = start.InternalSubViews [i]; currentLocation.X = startOffsetX + start.Viewport.X; currentLocation.Y = startOffsetY + start.Viewport.Y; // start is the deepest subview under the mouse; stop searching the subviews break; } } if (subview is null) { if (start.ViewportSettings.HasFlag (ViewportSettings.TransparentMouse)) { viewsUnderMouse.AddRange (View.GetViewsUnderMouse (location, true)); // De-dupe viewsUnderMouse HashSet dedupe = [..viewsUnderMouse]; viewsUnderMouse = [..dedupe]; } // No subview was found that's under the mouse, so we're done return viewsUnderMouse; } // We found a subview of start that's under the mouse, continue... start = subview; } return viewsUnderMouse; } private void DisposeMouse () { } }