using System.ComponentModel; 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.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 /// /// 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 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 /// /// /// If is , 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. /// /// /// /// 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 (HighlightStates != MouseState.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) { // 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 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 (App?.Mouse.MouseGrabView == this) { //Logging.Debug ($"{Id} - {MouseState}"); MouseState &= ~MouseState.Pressed; MouseState &= ~MouseState.PressedOutside; } 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 (App?.Mouse.MouseGrabView != this) { App?.Mouse.GrabMouse (this); if (!HasFocus && CanFocus) { // Set the focus, but don't invoke Accept SetFocus (); } 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; } if (!Viewport.Contains (mouseEvent.Position)) { // 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; } } 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 (App?.Mouse.MouseGrabView == this && mouseEvent.IsSingleClicked) { // 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, 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 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) { } /// /// RaisedCalled 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 () { } }