using System.ComponentModel; using System.Diagnostics; namespace Terminal.Gui.ViewBase; public partial class View // Mouse APIs { /// /// Handles , we have detected a button /// down in the view and have grabbed the mouse. /// public IMouseHeldDown? MouseHeldDown { get; set; } /// Gets the mouse bindings for this view. public MouseBindings MouseBindings { get; internal set; } = null!; private void SetupMouse () { MouseHeldDown = new MouseHeldDown (this, App?.TimedEvents, App?.Mouse); MouseBindings = new (); // TODO: Should the default really work with any button or just button1? MouseBindings.Add (MouseFlags.Button1Clicked, Command.Activate); MouseBindings.Add (MouseFlags.Button2Clicked, Command.Activate); MouseBindings.Add (MouseFlags.Button3Clicked, Command.Activate); MouseBindings.Add (MouseFlags.Button4Clicked, Command.Activate); MouseBindings.Add (MouseFlags.Button1Clicked | MouseFlags.ButtonCtrl, Command.Activate); } /// /// 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 /// /// 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); if (eventArgs.Cancel) { return true; } MouseState |= MouseState.In; if (HighlightStates != MouseState.None) { SetNeedsDraw (); } 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); MouseState &= ~MouseState.In; // TODO: Should we also MouseState &= ~MouseState.Pressed; ?? if (HighlightStates != MouseState.None) { SetNeedsDraw (); } } /// /// 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. When set to /// , /// and the user presses and holds the mouse button, will be /// repeatedly called with the same for as long as the mouse button remains pressed. /// public 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 mouse event for this view. This is the main entry point for mouse input handling, /// called by when the mouse interacts with this view. /// /// /// /// This method orchestrates the complete mouse event handling pipeline: /// /// /// /// /// Validates pre-conditions (view must be enabled and visible) /// /// /// /// /// Raises for low-level handling via /// and event subscribers /// /// /// /// /// Handles mouse grab scenarios when or /// are set (press/release/click) /// /// /// /// /// Invokes commands bound to mouse clicks via /// (default: event) /// /// /// /// /// Handles mouse wheel events via and /// /// /// /// /// Continuous Button Press: When is /// and the user holds a mouse button down, this method is repeatedly called /// with (or Button2-4) events, enabling repeating button /// behavior (e.g., scroll buttons). /// /// /// Mouse Grab: Views with or /// enabled automatically grab the mouse on button press, /// receiving all subsequent mouse events until the button is released, even if the mouse moves /// outside the view's . /// /// /// Most views should handle mouse clicks by subscribing to the event or /// overriding rather than overriding this method. Override this method /// only for custom low-level mouse handling (e.g., drag-and-drop). /// /// /// /// The mouse event to process. Coordinates in are relative /// to the view's . /// /// /// if the event was handled and should not be propagated; /// if the event was not handled and should continue propagating; /// if the view declined to handle the event (e.g., disabled or not visible). /// /// /// /// /// /// /// 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 (HighlightStates != MouseState.None || WantContinuousButtonPressed) { if (WhenGrabbedHandlePressed (mouseEvent)) { // If we raised Clicked/Activated on the grabbed view, we are done // regardless of whether the event was handled. return true; } WhenGrabbedHandleReleased (mouseEvent); 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) { // Logging.Debug ($"{mouseEvent.Flags};{mouseEvent.Position}"); return RaiseCommandsBoundToMouse (mouseEvent); } if (mouseEvent.IsWheel) { return RaiseMouseWheelEvent (mouseEvent); } return false; } /// /// Raises the / event. /// /// /// , if the event was handled, otherwise. public bool RaiseMouseEvent (MouseEventArgs mouseEvent) { // TODO: probably this should be moved elsewhere, please advise if (WantContinuousButtonPressed && MouseHeldDown != null) { if (mouseEvent.IsPressed) { MouseHeldDown.Start (); } else { MouseHeldDown.Stop (); } } 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 WhenGrabbed Handlers /// /// INTERNAL: For cases where the view is grabbed and the mouse is pressed, this method handles the pressed events from /// the driver. /// When is set, this method will raise the Clicked/Selecting event /// via each time it is called (after the first time the mouse is pressed). /// /// /// , if processing should stop, otherwise. private bool WhenGrabbedHandlePressed (MouseEventArgs mouseEvent) { if (!mouseEvent.IsPressed) { return false; } Debug.Assert (!mouseEvent.Handled); mouseEvent.Handled = false; // If the user has just pressed the mouse, grab the mouse and set focus if (App is null || App.Mouse.MouseGrabView != this) { App?.Mouse.GrabMouse (this); if (!HasFocus && CanFocus) { // Set the focus, but don't invoke Accept SetFocus (); } // This prevents raising Clicked/Selecting the first time the mouse is pressed. mouseEvent.Handled = true; } if (Viewport.Contains (mouseEvent.Position)) { //Logging.Debug ($"{Id} - Inside Viewport: {MouseState}"); // The mouse is inside. if (HighlightStates.HasFlag (MouseState.Pressed)) { MouseState |= MouseState.Pressed; } // Always clear PressedOutside when the mouse is pressed inside the Viewport MouseState &= ~MouseState.PressedOutside; } else { // Logging.Debug ($"{Id} - Outside Viewport: {MouseState}"); // The mouse is outside. // When WantContinuousButtonPressed is set we want to keep the mouse state as pressed (e.g. a repeating button). // This shows the user that the button is doing something, even if the mouse is outside the Viewport. if (HighlightStates.HasFlag (MouseState.PressedOutside) && !WantContinuousButtonPressed) { MouseState |= MouseState.PressedOutside; } } if (!mouseEvent.Handled && WantContinuousButtonPressed && App?.Mouse.MouseGrabView == this) { // Ignore the return value here, because the semantics of WhenGrabbedHandlePressed is the return // value indicates whether processing should stop or not. RaiseCommandsBoundToMouse (mouseEvent); return true; } return mouseEvent.Handled = true; } /// /// INTERNAL: For cases where the view is grabbed, this method handles the released events from the driver /// (typically /// when or are set). /// /// internal void WhenGrabbedHandleReleased (MouseEventArgs mouseEvent) { if (App is { } && App.Mouse.MouseGrabView == this) { //Logging.Debug ($"{Id} - {MouseState}"); MouseState &= ~MouseState.Pressed; MouseState &= ~MouseState.PressedOutside; } } /// /// INTERNAL: For cases where the view is grabbed, this method handles the click events from the driver /// (typically /// when or are set). /// /// /// , if processing should stop; otherwise. internal bool WhenGrabbedHandleClicked (MouseEventArgs mouseEvent) { if (App is null || App.Mouse.MouseGrabView != this || !mouseEvent.IsSingleClicked) { return false; } // Logging.Debug ($"{mouseEvent.Flags};{mouseEvent.Position}"); // We're grabbed. Clicked event comes after the last Release. This is our signal to ungrab App?.Mouse.UngrabMouse (); // TODO: Prove we need to unset MouseState.Pressed and MouseState.PressedOutside here // TODO: There may be perf gains if we don't unset these flags here MouseState &= ~MouseState.Pressed; MouseState &= ~MouseState.PressedOutside; // If mouse is still in bounds, return false to indicate a click should be raised. return WantMousePositionReports || !Viewport.Contains (mouseEvent.Position); } #endregion WhenGrabbed Handlers #region Mouse Click Events /// /// INTERNAL API: Converts mouse click events into s by invoking the commands bound /// to the mouse button via . By default, all mouse clicks are bound to /// which raises the event. /// protected bool RaiseCommandsBoundToMouse (MouseEventArgs args) { // Pre-conditions if (!Enabled) { // QUESTION: Is this right? Should a disabled view eat mouse clicks? return args.Handled = false; } Debug.Assert (!args.Handled); // Logging.Debug ($"{args.Flags};{args.Position}"); MouseEventArgs clickedArgs = new (); clickedArgs.Flags = args.IsPressed ? args.Flags switch { MouseFlags.Button1Pressed => MouseFlags.Button1Clicked, MouseFlags.Button2Pressed => MouseFlags.Button2Clicked, MouseFlags.Button3Pressed => MouseFlags.Button3Clicked, MouseFlags.Button4Pressed => MouseFlags.Button4Clicked, _ => clickedArgs.Flags } : args.Flags; clickedArgs.Position = args.Position; clickedArgs.ScreenPosition = args.ScreenPosition; clickedArgs.View = args.View; // By default, this will raise Activating/OnActivating - Subclasses can override this via // ReplaceCommand (Command.Activate ...). args.Handled = InvokeCommandsBoundToMouse (clickedArgs) == true; return args.Handled; } #endregion Mouse Click 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 MouseState Handling private MouseState _mouseState; /// /// Gets the state of the mouse relative to the View. When changed, the / /// /// event will be raised. /// public MouseState MouseState { get => _mouseState; internal set { if (_mouseState == value) { return; } EventArgs args = new (value); RaiseMouseStateChanged (args); _mouseState = value; } } /// /// Gets or sets which changes should cause the View to change its appearance. /// /// /// /// is set by default, which means the View will be highlighted when the /// mouse is over it. The default behavior of /// is to use the role for the highlight Attribute. /// /// /// means the View will be highlighted when the mouse is pressed over it. /// 's default behavior is to use /// the role when the Border is pressed for Arrangement. /// 's default behavior, when shadows are enabled, is to move the shadow providing /// a pressed effect. /// /// /// means the View will be highlighted when the mouse was pressed /// inside it and then moved outside of it, unless is set to /// , in which case the flag has no effect. /// /// public MouseState HighlightStates { get; set; } /// /// INTERNAL Raises the event. /// /// private void RaiseMouseStateChanged (EventArgs args) { //Logging.Debug ($"{Id} - {args.Value} -> {args.Value}"); OnMouseStateChanged (args); MouseStateChanged?.Invoke (this, args); } /// /// Called when has changed, indicating the View should be highlighted or not. The /// passed in the event /// indicates the highlight style that will be applied. /// protected virtual void OnMouseStateChanged (EventArgs args) { } /// /// Raised when has changed, indicating the View should be highlighted or not. The /// passed in the event /// indicates the highlight style that will be applied. /// public event EventHandler>? MouseStateChanged; #endregion MouseState Handling private void DisposeMouse () { if (App?.Mouse.MouseGrabView == this) { App.Mouse.UngrabMouse (); } } }