Fixes #4176. Removes View.MouseClick (#4450)

* Initial plan

* Add comprehensive MouseClick removal analysis

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Fix code review feedback on analysis document

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Add actual line numbers to analysis document

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Final polish on analysis document - clarify TextView

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Phase 3: Port 6 simple Views from MouseClick to Selecting/MouseBindings

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Phase 2: Port 10 Scenarios from MouseClick to Selecting/remove

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Update analysis document - Phases 2 & 3 complete

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Fix failing tests - Button multi-button support and Label focus handling

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Phase 4: Port ScrollBar from OnMouseClick to OnSelecting; remove analysis doc

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Phase 5: Update/remove tests that explicitly test MouseClick API

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Code cleanup

* Remove MouseClick event

Consolidated mouse event handling by removing the `MouseClick`
event and integrating its functionality into the `MouseEvent`
event. Simplified `MouseEventArgs` documentation and added
support for invoking commands bound to mouse events.

Reorganized code by removing `Mouse Pressed Events` and
`Mouse Click Events` regions, introducing a new `WhenGrabbed
Handlers` region. Updated tests to replace `MouseClick` with
`MouseEvent`, adjusted test logic, and improved variable
naming for clarity.

Removed redundant assertions and unused code related to
`MouseClick`. Improved event propagation logic to ensure
proper handling of unhandled events. Performed general code
cleanup to enhance readability and maintainability.

* Updated deep dives.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tig <585482+tig@users.noreply.github.com>
Co-authored-by: Tig <tig@users.noreply.github.com>
This commit is contained in:
Copilot
2025-12-06 14:41:20 -07:00
committed by GitHub
parent 0eafb5947e
commit dd12df7fb7
31 changed files with 811 additions and 670 deletions

View File

@@ -148,7 +148,7 @@ See the [Mouse Deep Dive](mouse.md).
- [View.WantContinuousButtonPresses](~/api/Terminal.Gui.ViewBase.View.yml#Terminal_Gui_ViewBase_View_WantContinuousButtonPresses) - Enables continuous button press events
- [View.Highlight](~/api/Terminal.Gui.ViewBase.View.yml) - Event for visual feedback on mouse hover/click
- [View.HighlightStyle](~/api/Terminal.Gui.ViewBase.View.yml#Terminal_Gui_ViewBase_View_HighlightStyle) - Visual style when highlighted
- Events: `MouseEnter`, `MouseLeave`, `MouseClick`, `MouseEvent`
- Events: `MouseEnter`, `MouseLeave`, `MouseEvent`
### Layout and Arrangement
@@ -368,7 +368,6 @@ The mouse subsystem processes mouse events through:
3. [View.MouseEnter](~/api/Terminal.Gui.ViewBase.View.yml) / [View.MouseLeave](~/api/Terminal.Gui.ViewBase.View.yml) events
4. [View.MouseBindings](~/api/Terminal.Gui.ViewBase.View.yml) - Converts mouse actions to commands
5. Command handlers
6. [View.MouseClick](~/api/Terminal.Gui.ViewBase.View.yml) event (high-level)
### Layout

View File

@@ -555,7 +555,7 @@ void HandleMouse(object? sender, MouseEventArgs args) { }
```csharp
// v2 - Viewport-relative coordinates
view.MouseClick += (s, e) =>
view.MouseEvent += (s, e) =>
{
// e.Position is relative to view's Viewport
var x = e.Position.X; // 0 = left edge of viewport
@@ -563,16 +563,120 @@ view.MouseClick += (s, e) =>
};
```
### Highlight Event
### Mouse Click Handling
v2 adds a `Highlight` event for visual feedback:
**v1:**
```csharp
// v1 - MouseClick event
view.MouseClick += (mouseEvent) =>
{
// Handle click
DoSomething();
};
```
**v2:**
```csharp
// v2 - Use MouseBindings + Commands + Selecting event
view.MouseBindings.Add(MouseFlags.Button1Clicked, Command.Select);
view.Selecting += (s, e) =>
{
// Handle selection (called when Button1Clicked)
DoSomething();
};
// Alternative: Use MouseEvent for low-level handling
view.MouseEvent += (s, e) =>
{
if (e.Flags.HasFlag(MouseFlags.Button1Clicked))
{
DoSomething();
e.Handled = true;
}
};
```
**Key Changes:**
- `View.MouseClick` event has been **removed**
- Use `MouseBindings` to map mouse events to `Command`s
- Default mouse bindings invoke `Command.Select` which raises the `Selecting` event
- For custom behavior, override `OnSelecting` or subscribe to the `Selecting` event
- For low-level mouse handling, use `MouseEvent` directly
**Migration Pattern:**
```csharp
// ❌ v1 - OnMouseClick override
protected override bool OnMouseClick(MouseEventArgs mouseEvent)
{
if (mouseEvent.Flags.HasFlag(MouseFlags.Button1Clicked))
{
PerformAction();
return true;
}
return base.OnMouseClick(mouseEvent);
}
// ✅ v2 - OnSelecting override
protected override bool OnSelecting(CommandEventArgs args)
{
if (args.Context is CommandContext<MouseBinding> { Binding.MouseEventArgs: { } mouseArgs })
{
// Access mouse position and flags via context
if (mouseArgs.Flags.HasFlag(MouseFlags.Button1Clicked))
{
PerformAction();
return true;
}
}
return base.OnSelecting(args);
}
// ✅ v2 - Selecting event (simpler)
view.Selecting += (s, e) =>
{
PerformAction();
e.Handled = true;
};
```
**Accessing Mouse Position in Selecting Event:**
```csharp
view.Selecting += (s, e) =>
{
// Extract mouse event args from command context
if (e.Context is CommandContext<MouseBinding> { Binding.MouseEventArgs: { } mouseArgs })
{
Point position = mouseArgs.Position;
MouseFlags flags = mouseArgs.Flags;
// Use position and flags for custom logic
HandleClick(position, flags);
e.Handled = true;
}
};
```
### Mouse State and Highlighting
v2 adds enhanced mouse state tracking:
```csharp
view.Highlight += (s, e) =>
// Configure which mouse states trigger highlighting
view.HighlightStates = MouseState.In | MouseState.Pressed;
// React to mouse state changes
view.MouseStateChanged += (s, e) =>
{
// Provide visual feedback on mouse hover
switch (e.Value)
{
case MouseState.In:
// Mouse entered view
break;
case MouseState.Pressed:
// Mouse button pressed in view
break;
}
};
view.HighlightStyle = HighlightStyle.Hover;
```
See [Mouse Deep Dive](mouse.md) for complete details.

View File

@@ -44,8 +44,12 @@ public class MyView : View
AddCommand (Command.ScrollDown, () => ScrollVertical (1));
MouseBindings.Add (MouseFlags.WheelDown, Command.ScrollDown);
AddCommand (Command.Select, () => SelectItem());
MouseBindings.Add (MouseFlags.Button1Clicked, Command.Select);
// Mouse clicks invoke Command.Select by default
// Override to customize click behavior
AddCommand (Command.Select, () => {
SelectItem();
return true;
});
}
}
```
@@ -56,17 +60,65 @@ The @Terminal.Gui.Input.Command enum lists generic operations that are implement
Here are some common mouse binding patterns used throughout Terminal.Gui:
* **Click Events**: `MouseFlags.Button1Clicked` for primary selection/activation
* **Click Events**: `MouseFlags.Button1Clicked` for primary selection/activation - maps to `Command.Select` by default
* **Double-Click Events**: `MouseFlags.Button1DoubleClicked` for default actions (like opening/accepting)
* **Right-Click Events**: `MouseFlags.Button3Clicked` for context menus
* **Scroll Events**: `MouseFlags.WheelUp` and `MouseFlags.WheelDown` for scrolling content
* **Drag Events**: `MouseFlags.Button1Pressed` combined with mouse move tracking for drag operations
### Default Mouse Bindings
By default, all views have the following mouse bindings configured:
```cs
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);
```
When a mouse click occurs, the `Command.Select` is invoked, which raises the `Selecting` event. Views can override `OnSelecting` or subscribe to the `Selecting` event to handle clicks:
```cs
public class MyView : View
{
public MyView()
{
// Option 1: Subscribe to Selecting event
Selecting += (s, e) =>
{
if (e.Context is CommandContext<MouseBinding> { Binding.MouseEventArgs: { } mouseArgs })
{
// Access mouse position and flags
HandleSelection(mouseArgs.Position, mouseArgs.Flags);
e.Handled = true;
}
};
}
// Option 2: Override OnSelecting
protected override bool OnSelecting(CommandEventArgs args)
{
if (args.Context is CommandContext<MouseBinding> { Binding.MouseEventArgs: { } mouseArgs })
{
// Custom selection logic with mouse position
if (mouseArgs.Position.Y == 0)
{
HandleHeaderClick();
return true;
}
}
return base.OnSelecting(args);
}
}
```
## Mouse Events
At the core of *Terminal.Gui*'s mouse API is the @Terminal.Gui.Input.MouseEventArgs class. The @Terminal.Gui.Input.MouseEventArgs class provides a platform-independent abstraction for common mouse events. Every mouse event can be fully described in a @Terminal.Gui.Input.MouseEventArgs instance, and most of the mouse-related APIs are simply helper functions for decoding a @Terminal.Gui.Input.MouseEventArgs.
When the user does something with the mouse, the driver maps the platform-specific mouse event into a `MouseEventArgs` and calls `IApplication.Mouse.RaiseMouseEvent`. Then, `IApplication.Mouse.RaiseMouseEvent` determines which `View` the event should go to. The `View.OnMouseEvent` method can be overridden or the `View.MouseEvent` event can be subscribed to, to handle the low-level mouse event. If the low-level event is not handled by a view, `IApplication` will then call the appropriate high-level helper APIs. For example, if the user double-clicks the mouse, `View.OnMouseClick` will be called/`View.MouseClick` will be raised with the event arguments indicating which mouse button was double-clicked.
When the user does something with the mouse, the driver maps the platform-specific mouse event into a `MouseEventArgs` and calls `IApplication.Mouse.RaiseMouseEvent`. Then, `IApplication.Mouse.RaiseMouseEvent` determines which `View` the event should go to. The `View.OnMouseEvent` method can be overridden or the `View.MouseEvent` event can be subscribed to, to handle the low-level mouse event. If the low-level event is not handled by a view, `IApplication` will then call the appropriate high-level helper APIs.
### Mouse Event Processing Flow
@@ -77,8 +129,9 @@ Mouse events are processed through the following workflow using the [Cancellable
3. **View Level**: The target view processes the event through:
- `OnMouseEvent` (virtual method that can be overridden)
- `MouseEvent` event (for event subscribers)
- Mouse bindings (if the event wasn't handled)
- High-level events like `OnMouseClick`, `MouseEnter`, `MouseLeave`
- Mouse bindings (if the event wasn't handled) which invoke commands
- Command handlers (e.g., `OnSelecting` for `Command.Select`)
- High-level events like `MouseEnter`, `MouseLeave`
### Handling Mouse Events Directly
@@ -114,15 +167,76 @@ public class CustomView : View
}
```
### Handling Mouse Clicks
The recommended pattern for handling mouse clicks is to use the `Selecting` event or override `OnSelecting`. This integrates with the command system and provides access to mouse event details through the command context:
```cs
public class ClickableView : View
{
public ClickableView()
{
Selecting += OnSelecting;
}
private void OnSelecting(object sender, CommandEventArgs e)
{
// Extract mouse event information from command context
if (e.Context is CommandContext<MouseBinding> { Binding.MouseEventArgs: { } mouseArgs })
{
// Access mouse position (viewport-relative)
Point clickPosition = mouseArgs.Position;
// Check which button was clicked
if (mouseArgs.Flags.HasFlag(MouseFlags.Button1Clicked))
{
HandleLeftClick(clickPosition);
}
else if (mouseArgs.Flags.HasFlag(MouseFlags.Button3Clicked))
{
ShowContextMenu(clickPosition);
}
e.Handled = true;
}
}
}
```
For views that need different behavior for different mouse buttons, configure custom mouse bindings:
```cs
public class MultiButtonView : View
{
public MultiButtonView()
{
// Clear default bindings
MouseBindings.Clear();
// Map different buttons to different commands
MouseBindings.Add(MouseFlags.Button1Clicked, Command.Select);
MouseBindings.Add(MouseFlags.Button3Clicked, Command.ContextMenu);
AddCommand(Command.ContextMenu, HandleContextMenu);
}
private bool HandleContextMenu()
{
// Show context menu
return true;
}
}
```
## Mouse State
The @Terminal.Gui.ViewBase.View.MouseState property provides an abstraction for the current state of the mouse, enabling views to do interesting things like change their appearance based on the mouse state.
Mouse states include:
* **Normal** - Default state when mouse is not interacting with the view
* **Over** - Mouse is positioned over the view
* **In** - Mouse is positioned over the view (inside the viewport)
* **Pressed** - Mouse button is pressed down while over the view
* **Clicked** - Mouse was clicked on the view
* **PressedOutside** - Mouse was pressed inside but moved outside the view
It works in conjunction with the @Terminal.Gui.ViewBase.View.HighlightStates which is a list of mouse states that will cause a view to become highlighted.
@@ -131,9 +245,9 @@ Subscribe to the @Terminal.Gui.ViewBase.View.MouseStateChanged event to be notif
```cs
view.MouseStateChanged += (sender, e) =>
{
switch (e.NewState)
switch (e.Value)
{
case MouseState.Over:
case MouseState.In:
// Change appearance when mouse hovers
break;
case MouseState.Pressed:
@@ -143,6 +257,13 @@ view.MouseStateChanged += (sender, e) =>
};
```
Configure which states should cause highlighting:
```cs
// Highlight when mouse is over the view or when pressed
view.HighlightStates = MouseState.In | MouseState.Pressed;
```
## Mouse Button and Movement Concepts
* **Down** - Indicates the user pushed a mouse button down.
@@ -150,6 +271,7 @@ view.MouseStateChanged += (sender, e) =>
* **Released** - Indicates the user released a mouse button.
* **Clicked** - Indicates the user pressed then released the mouse button while over a particular View.
* **Double-Clicked** - Indicates the user clicked twice in rapid succession.
* **Triple-Clicked** - Indicates the user clicked three times in rapid succession.
* **Moved** - Indicates the mouse moved to a new location since the last mouse event.
* **Wheel** - Indicates the mouse wheel was scrolled up or down.
@@ -179,7 +301,7 @@ public class MyView : View
if (mouseEvent.Flags.HasFlag(MouseFlags.Button3Clicked))
{
// Access application mouse functionality through View.App
App?.MouseEvent?.Invoke(this, mouseEvent);
App?.Mouse?.RaiseMouseEvent(mouseEvent);
return true;
}
return base.OnMouseEvent(mouseEvent);
@@ -209,16 +331,31 @@ view.MouseLeave += (sender, e) =>
Mouse coordinates in Terminal.Gui are provided in multiple coordinate systems:
* **Screen Coordinates** - Relative to the entire terminal screen (0,0 is top-left of terminal)
* **View Coordinates** - Relative to the view's content area (0,0 is top-left of view's viewport)
* **Screen Coordinates** - Relative to the entire terminal screen (0,0 is top-left of terminal) - available via `MouseEventArgs.ScreenPosition`
* **View Coordinates** - Relative to the view's viewport (0,0 is top-left of view's viewport) - available via `MouseEventArgs.Position`
The `MouseEventArgs` provides both coordinate systems:
* `MouseEventArgs.Position` - Screen coordinates
* `MouseEventArgs.ViewPosition` - View-relative coordinates (when available)
* `MouseEventArgs.ScreenPosition` - Screen coordinates (absolute position on screen)
* `MouseEventArgs.Position` - Viewport-relative coordinates (position within the view's content area)
When handling mouse events in views, use `Position` for viewport-relative coordinates:
```cs
view.MouseEvent += (s, e) =>
{
// e.Position is viewport-relative
if (e.Position.X < 10 && e.Position.Y < 5)
{
// Click in top-left corner of viewport
}
};
```
## Best Practices
* **Use Mouse Bindings** when possible for simple mouse interactions - they integrate well with the Command system
* **Use Mouse Bindings and Commands** for simple mouse interactions - they integrate well with the Command system and work alongside keyboard bindings
* **Use the `Selecting` event** to handle mouse clicks - it's raised by the default `Command.Select` binding for all mouse buttons
* **Access mouse details via CommandContext** when you need position or flags in `Selecting` handlers
* **Handle Mouse Events directly** for complex interactions like drag-and-drop or custom gestures
* **Respect platform conventions** - use right-click for context menus, double-click for default actions
* **Provide keyboard alternatives** - ensure all mouse functionality has keyboard equivalents
@@ -231,9 +368,4 @@ The `MouseEventArgs` provides both coordinate systems:
* Mouse wheel support may vary between platforms and terminals
* Some terminals may not support all mouse buttons or modifier keys
* Mouse coordinates are limited to character cell boundaries - sub-character precision is not available
* Performance can be impacted by excessive mouse move event handling - use mouse enter/leave events when appropriate rather than tracking all mouse moves
* Performance can be impacted by excessive mouse move event handling - use mouse enter/leave events when appropriate rather than tracking all mouse moves

View File

@@ -7,7 +7,7 @@ This document covers Terminal.Gui's navigation system, which determines:
- What are the visual cues that help the user know what keystrokes will change the focus?
- What are the visual cues that help the user know what keystrokes will cause action in elements of the application that don't currently have focus?
- What is the order in which UI elements are traversed when using keyboard navigation?
- What are the default actions for standard key/mouse input (e.g. Hotkey, `Space`, `Enter`, `MouseClick`)?
- What are the default actions for standard key/mouse input (e.g. Hotkey, `Space`, `Enter`, or a mouse click)?
## See Also