Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 67 additions & 9 deletions Terminal.Gui/Views/TableView/TableView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,25 @@ private bool IsColumnVisible (int columnIndex)
return Style.GetColumnStyleIfAny (columnIndex)?.Visible ?? true;
}

/// <inheritdoc />
protected override bool OnActivating (CommandEventArgs args)
{
// Cancel Activate when a mouse click lands outside any cell (e.g. below the
// last row or in the header area). Without this, the Activating event would
// fire even though no cell was actually selected, surprising subscribers.
if (!TryGetMouseCellHit (args.Context, out Point? hit))
{
return false;
}

if (hit is null)
{
return true;
}

return false;
}

/// <inheritdoc />
protected override void OnActivated (ICommandContext? ctx)
{
Expand All @@ -248,26 +267,65 @@ protected override void OnActivated (ICommandContext? ctx)
return;
}

if (ctx?.Binding is not MouseBinding mouseBinding || mouseBinding.MouseEvent is null)
if (!TryGetMouseCellHit (ctx, out Point? hit) || hit is null)
{
return;
}
int boundsX = mouseBinding.MouseEvent.Position!.Value.X;
int boundsY = mouseBinding.MouseEvent.Position!.Value.Y;

MouseBinding mouseBinding = (MouseBinding)ctx!.Binding!;
SetSelection (hit.Value.X, hit.Value.Y, mouseBinding.MouseEvent!.Flags.FastHasFlags (MouseFlags.Shift));

Update ();
}

// Returns true when the binding represents a left-click on this TableView; in that case
// hit is the cell that was clicked, or null if the click was outside any cell (e.g. header,
// below the last row, or right of the last rendered column). Returns false for non-mouse or
// non-left-click bindings.
private bool TryGetMouseCellHit (ICommandContext? ctx, out Point? hit)
{
hit = null;

if (ctx?.Binding is not MouseBinding mouseBinding || mouseBinding.MouseEvent is null)
{
return false;
}

if (!mouseBinding.MouseEvent.Flags.FastHasFlags (MouseFlags.LeftButtonClicked))
{
return;
return false;
}
Point? hit = ScreenToCell (boundsX, boundsY);

if (hit is null)
int clientX = mouseBinding.MouseEvent.Position!.Value.X;
int clientY = mouseBinding.MouseEvent.Position!.Value.Y;

hit = ScreenToCell (clientX, clientY);

// ScreenToCell maps far-right X positions onto the last column (its column lookup picks
// the largest X with X <= clientX, with no upper-bound check). When ExpandLastColumn is
// false there is visible whitespace to the right of the last rendered column; reject
// hits that fall there so clicks in that whitespace don't raise Activating.
if (hit is not null && !IsXWithinAnyRenderedColumn (clientX))
{
return;
hit = null;
}
SetSelection (hit.Value.X, hit.Value.Y, mouseBinding.MouseEvent.Flags.FastHasFlags (MouseFlags.Shift));

Update ();
return true;
}

private bool IsXWithinAnyRenderedColumn (int clientX)
{
int viewportX = clientX + Viewport.X;

foreach (ColumnToRender col in NonHiddenCellInfos ())
{
if (viewportX >= col.X && viewportX < col.X + col.Width)
{
return true;
}
}

return false;
}

/// <inheritdoc/>
Expand Down
176 changes: 176 additions & 0 deletions Tests/UnitTestsParallelizable/Views/TableViewTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1141,4 +1141,180 @@ public void HeaderSeparatorLines_DoNotUseFocusAttribute_WhenTableHasFocus ()

tableView.Dispose ();
}

// Claude - Opus 4.7
// Regression test for https://github.com/gui-cs/Terminal.Gui/issues/5126
// Clicking outside the row area of a TableView (e.g. below the last row) must
// not raise the Activating event.
[Fact]
public void Click_OutsideRows_DoesNotRaise_Activating ()
{
TableView tableView = new () { Viewport = new Rectangle (0, 0, 25, 10) };
tableView.Table = BuildTable (2, 2);

var activatingFired = 0;
tableView.Activating += (_, _) => activatingFired++;

// Construct a left-click well below the last data row. With a 2-row table,
// y=9 is past the rows so ScreenToCell returns null.
Mouse mouseEvent = new ()
{
Position = new Point (1, 9),
Flags = MouseFlags.LeftButtonClicked
};

MouseBinding binding = new ([Command.Activate], mouseEvent);

tableView.InvokeCommand (Command.Activate, binding);

Assert.Equal (0, activatingFired);

tableView.Dispose ();
}

// Claude - Opus 4.7
// Companion to Click_OutsideRows_DoesNotRaise_Activating: clicking on a real
// cell still raises Activating as before.
[Fact]
public void Click_OnRow_Raises_Activating ()
{
TableView tableView = new () { Viewport = new Rectangle (0, 0, 25, 10) };
tableView.Table = BuildTable (2, 2);

var activatingFired = 0;
tableView.Activating += (_, _) => activatingFired++;

// y=3 lands on the first data row. The default header style consumes 3 lines
// (overline + header text + underline), so y=0..2 is header and y=3 is the
// first data row.
Mouse mouseEvent = new ()
{
Position = new Point (1, 3),
Flags = MouseFlags.LeftButtonClicked
};

MouseBinding binding = new ([Command.Activate], mouseEvent);

tableView.InvokeCommand (Command.Activate, binding);

Assert.Equal (1, activatingFired);

tableView.Dispose ();
}

// Claude - Opus 4.7
// Click in horizontal whitespace (right of the last rendered column) must not
// raise Activating. Disable ExpandLastColumn so the last column doesn't fill
// the viewport and there is real whitespace to the right.
[Fact]
public void Click_RightOfLastColumn_DoesNotRaise_Activating ()
{
TableView tableView = new () { Viewport = new Rectangle (0, 0, 40, 10) };
tableView.Style.ExpandLastColumn = false;
tableView.Table = BuildTable (2, 2);

var activatingFired = 0;
tableView.Activating += (_, _) => activatingFired++;

// BuildTable column "Col0" / "Col1" rendered with values like "R0C0" gives
// narrow columns. With ExpandLastColumn=false and viewport width 40, x=35 is
// well past the right edge of the last rendered column.
Mouse mouseEvent = new ()
{
Position = new Point (35, 3),
Flags = MouseFlags.LeftButtonClicked
};

MouseBinding binding = new ([Command.Activate], mouseEvent);

tableView.InvokeCommand (Command.Activate, binding);

Assert.Equal (0, activatingFired);

tableView.Dispose ();
}

// Claude - Opus 4.7
// Click on the column header area must not raise Activating — the click
// doesn't correspond to a data cell.
[Fact]
public void Click_OnHeader_DoesNotRaise_Activating ()
{
TableView tableView = new () { Viewport = new Rectangle (0, 0, 25, 10) };
tableView.Table = BuildTable (2, 2);

var activatingFired = 0;
tableView.Activating += (_, _) => activatingFired++;

// The header occupies y=0..2 (overline + header text + underline). y=1 is
// the header text line.
Mouse mouseEvent = new ()
{
Position = new Point (1, 1),
Flags = MouseFlags.LeftButtonClicked
};

MouseBinding binding = new ([Command.Activate], mouseEvent);

tableView.InvokeCommand (Command.Activate, binding);

Assert.Equal (0, activatingFired);

tableView.Dispose ();
}

// Claude - Opus 4.7
// Click on a TableView with no Table set must not raise Activating.
[Fact]
public void Click_EmptyTable_DoesNotRaise_Activating ()
{
TableView tableView = new () { Viewport = new Rectangle (0, 0, 25, 10) };

// Intentionally no Table assigned.
var activatingFired = 0;
tableView.Activating += (_, _) => activatingFired++;

Mouse mouseEvent = new ()
{
Position = new Point (5, 5),
Flags = MouseFlags.LeftButtonClicked
};

MouseBinding binding = new ([Command.Activate], mouseEvent);

tableView.InvokeCommand (Command.Activate, binding);

Assert.Equal (0, activatingFired);

tableView.Dispose ();
}

// Claude - Opus 4.7
// Click on a Table that has zero rows (header rendered but no data) must not
// raise Activating regardless of where in the data area the user clicks.
[Fact]
public void Click_TableWithZeroRows_DoesNotRaise_Activating ()
{
TableView tableView = new () { Viewport = new Rectangle (0, 0, 25, 10) };
tableView.Table = BuildTable (2, 0);

var activatingFired = 0;
tableView.Activating += (_, _) => activatingFired++;

// y=3 is just past the header, in what would be the first data row if any
// existed. With zero rows, ScreenToCell must return null.
Mouse mouseEvent = new ()
{
Position = new Point (1, 3),
Flags = MouseFlags.LeftButtonClicked
};

MouseBinding binding = new ([Command.Activate], mouseEvent);

tableView.InvokeCommand (Command.Activate, binding);

Assert.Equal (0, activatingFired);

tableView.Dispose ();
}
}
Loading