Files
Terminal.Gui/Examples/UICatalog/Scenarios/MouseTester.cs
Copilot f2d260a853 Add TryGetSource extension methods for WeakReference<View> access (#4694)
* Initial plan

* CommandContext infrastructure: WeakReference and extension methods

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

* Align CommandContext infrastructure with PR #4620

- Update ViewExtensions.cs to use Terminal.Gui.ViewBase namespace
- Update WeakReferenceExtensions.cs to match PR #4620 format
- Change CommandContext constructor to take WeakReference<View>? directly
- Add CommandContext.ToString() implementation
- Update ICommandContext.Source documentation
- Update View.Command.cs to explicitly create WeakReference
- Update Shortcut.cs to handle WeakReference Source
- Update ComboBox.cs Source comparison
- Update Dialog.cs and DialogTResult.cs pattern matching
- Update MenuBar.cs and PopoverMenu.cs to work with WeakReference
- Update ScrollSlider.cs and OptionSelector.cs

Remaining: Fix Examples code and tests

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

* Fix Examples code for WeakReference Source

- Update UICatalogRunnable.cs to use TryGetTarget for Data access
- Update Menus.cs to use ToIdentifyingString() and TryGetTarget
- Update MouseTester.cs to use ToIdentifyingString() for all Source.Id access

All Examples now build successfully

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

* Update tests for WeakReference Source

- Fix CommandContextTests.cs to wrap Source in WeakReference
- Fix InputBindingTests.cs to wrap Source in WeakReference
- Update assertions to use TryGetTarget for safe access
- All CommandContext and InputBinding tests pass

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

* Add TryGetSource extension methods and replace usage

- Add TryGetSource extension to WeakReferenceExtensions.cs
- Add CommandContextExtensions.cs with TryGetSource for ICommandContext
- Replace 14 instances of Source?.TryGetTarget pattern across codebase
- Updated Terminal.Gui files: ComboBox, Dialog, DialogTResult, MenuBar, PopoverMenu, OptionSelector, Shortcut
- Updated Examples files: UICatalogRunnable, Menus
- All files build successfully

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

* Add tests for TryGetSource extension methods

- Add 6 tests for TryGetSource extension methods
- Test WeakReference<View>.TryGetSource with valid/null references
- Test ICommandContext.TryGetSource with valid/null contexts
- Test pattern matching usage with TryGetSource
- All 23 CommandContext tests pass
- Full test suite: 15,094 passed

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

* code cleanup

---------

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>
2026-02-06 14:44:04 -07:00

504 lines
20 KiB
C#

#nullable enable
using System.Collections.ObjectModel;
namespace UICatalog.Scenarios;
[ScenarioMetadata ("Mouse Tester", "Illustrates Mouse event flow and handling")]
[ScenarioCategory ("Mouse and Keyboard")]
public class MouseTester : Scenario
{
public override void Main ()
{
ConfigurationManager.Enable (ConfigLocations.All);
using IApplication app = Application.Create ();
app.Init ();
using Runnable runnable = new () { Id = "runnable" };
MenuBar menuBar = new ();
menuBar.Add (new MenuBarItem (Strings.menuFile, [new MenuItem { Title = Strings.cmdQuit, Action = () => app.RequestStop () }]));
FlagSelector<DemoMouseFlags> mouseFlagsFilter = new () { AssignHotKeys = true, Value = DemoMouseFlags.All & ~DemoMouseFlags.PositionReport };
menuBar.Add (new MenuBarItem ("_Filter", [new MenuItem { CommandView = mouseFlagsFilter }]),
new MenuBarItem (runnable, Command.DeleteAll, "_Clear Logs"));
runnable.Add (menuBar);
View lastDriverEvent = new () { Height = 1, Width = Dim.Auto (), Y = Pos.Bottom (menuBar), Text = "Last Driver Event: " };
runnable.Add (lastDriverEvent);
View lastAppEvent = new () { Height = 1, Width = Dim.Auto (), Y = Pos.Bottom (lastDriverEvent), Text = "Last App Event: " };
runnable.Add (lastAppEvent);
View lastViewEvent = new () { Height = 1, Width = Dim.Auto (), Y = Pos.Bottom (lastAppEvent), Text = "Last View Event: " };
runnable.Add (lastViewEvent);
FlagSelector<MouseState> mouseHighlightStates = new ()
{
BorderStyle = LineStyle.Dotted, Title = "_Highlight States", Y = Pos.Bottom (lastViewEvent), Width = 20
};
runnable.Add (mouseHighlightStates);
CheckBox cbRepeatOnHold = new ()
{
X = Pos.Right (mouseHighlightStates) + 1, Y = Pos.Top (mouseHighlightStates), BorderStyle = LineStyle.Dotted, Title = "_Repeat On Hold"
};
runnable.Add (cbRepeatOnHold);
MouseEventDemoView demo = new ()
{
Id = "demo",
Y = Pos.Bottom (mouseHighlightStates),
Width = Dim.Fill (),
Height = 15,
Title = "Enter/Leave Demo"
};
MouseEventDemoView demoInPadding = new ()
{
X = 0,
Y = 0,
Width = Dim.Fill (),
Height = Dim.Func (_ => demo.Padding!.Thickness.Top),
Title = "inPadding",
Id = "inPadding"
};
demo.Padding!.Add (demoInPadding);
demo.Padding!.Initialized += DemoPaddingOnInitialized;
MouseEventDemoView sub1 = new ()
{
X = 0,
Y = 0,
Width = Dim.Percent (20),
Height = Dim.Fill (),
Title = "sub1",
Id = "sub1"
};
demo.Add (sub1);
MouseEventDemoView sub2 = new ()
{
X = Pos.Right (sub1) - 4,
Y = Pos.Top (sub1) + 1,
Width = Dim.Percent (20),
Height = Dim.Fill (1),
Title = "sub2",
Id = "sub2"
};
demo.Add (sub2);
runnable.Add (demo);
mouseHighlightStates.Value = demo.MouseHighlightStates;
mouseHighlightStates.ValueChanged += (sender, _) =>
{
if (sender is FlagSelector<MouseState> optionSelector)
{
demo.MouseHighlightStates = optionSelector.Value!.Value;
foreach (View subview in demo.SubViews)
{
subview.MouseHighlightStates = optionSelector.Value!.Value;
}
foreach (View subview in demo.Padding.SubViews)
{
subview.MouseHighlightStates = optionSelector.Value!.Value;
}
}
};
cbRepeatOnHold.ValueChanging += (_, _) =>
{
demo.MouseHoldRepeat = demo.MouseHoldRepeat is null ? MouseFlags.LeftButtonPressed : null;
foreach (View subview in demo.SubViews)
{
subview.MouseHoldRepeat = demo.MouseHoldRepeat;
}
foreach (View subview in demo.Padding.SubViews)
{
subview.MouseHoldRepeat = demo.MouseHoldRepeat;
}
};
Label label = new ()
{
Text = "Dri_ver Events:",
//X = Pos.Right (filterSlider),
Y = Pos.Bottom (demo)
};
ObservableCollection<string> driverLogList = new ();
ListView driverLog = new ()
{
X = Pos.Left (label),
Y = Pos.Bottom (label),
Width = Dim.Auto (minimumContentDim: Dim.Percent (20)),
Height = Dim.Fill (),
SchemeName = SchemeManager.SchemesToSchemeName (Schemes.Base),
Source = new ListWrapper<string> (driverLogList)
};
runnable.Add (label, driverLog);
app.Driver!.MouseEvent += (_, mouse) =>
{
if (!mouseFlagsFilter.Value.HasValue)
{
return;
}
if (mouseFlagsFilter.Value.Value.HasFlag ((DemoMouseFlags)mouse.Flags))
{
lastDriverEvent.Text = $"Last Driver Event: {mouse}";
Logging.Trace (lastDriverEvent.Text);
driverLogList.Add ($"{mouse.Position}:{mouse.Flags}");
driverLog.MoveEnd ();
}
};
label = new Label { Text = "_App Events:", X = Pos.Right (driverLog) + 1, Y = Pos.Bottom (demo) };
ObservableCollection<string> appLogList = new ();
ListView appLog = new ()
{
X = Pos.Left (label),
Y = Pos.Bottom (label),
Width = Dim.Auto (minimumContentDim: Dim.Percent (20)),
Height = Dim.Fill (),
SchemeName = SchemeManager.SchemesToSchemeName (Schemes.Base),
Source = new ListWrapper<string> (appLogList)
};
runnable.Add (label, appLog);
app.Mouse.MouseEvent += (_, mouse) =>
{
if (!mouseFlagsFilter.Value.HasValue)
{
return;
}
if (mouseFlagsFilter.Value.Value.HasFlag ((DemoMouseFlags)mouse.Flags))
{
lastAppEvent.Text = $" Last App Event: {mouse}";
appLogList.Add ($"{mouse.Position}:{mouse.Flags}");
appLog.MoveEnd ();
}
};
label = new Label { Text = "_View Events:", X = Pos.Right (appLog) + 1, Y = Pos.Top (label) };
ObservableCollection<string> viewLogList = [];
ListView viewLog = new ()
{
X = Pos.Left (label),
Y = Pos.Bottom (label),
Width = Dim.Auto (minimumContentDim: Dim.Percent (20)),
Height = Dim.Fill (),
SchemeName = SchemeManager.SchemesToSchemeName (Schemes.Base),
Source = new ListWrapper<string> (viewLogList)
};
runnable.Add (label, viewLog);
demo.MouseEvent += (_, mouse) =>
{
if (mouseFlagsFilter.Value.Value.HasFlag ((DemoMouseFlags)mouse.Flags))
{
lastViewEvent.Text = $" Last View Event: {mouse}";
viewLogList.Add ($"{mouse.Position}:{mouse.View!.Id}:{mouse.Flags}");
viewLog.MoveEnd ();
}
};
demoInPadding.MouseEvent += (_, mouse) =>
{
if (mouseFlagsFilter.Value.Value.HasFlag ((DemoMouseFlags)mouse.Flags))
{
lastViewEvent.Text = $" Last View Event: {mouse}";
viewLogList.Add ($"{mouse.Position}:{mouse.View!.Id}:{mouse.Flags}");
viewLog.MoveEnd ();
}
};
sub1.MouseEvent += (_, mouse) =>
{
if (mouseFlagsFilter.Value.Value.HasFlag ((DemoMouseFlags)mouse.Flags))
{
lastViewEvent.Text = $" Last View Event: {mouse}";
viewLogList.Add ($"{mouse.Position}:{mouse.View!.Id}:{mouse.Flags}");
viewLog.MoveEnd ();
}
};
sub2.MouseEvent += (_, mouse) =>
{
if (mouseFlagsFilter.Value.Value.HasFlag ((DemoMouseFlags)mouse.Flags))
{
lastViewEvent.Text = $" Last View Event: {mouse}";
viewLogList.Add ($"{mouse.Position}:{mouse.View!.Id}:{mouse.Flags}");
viewLog.MoveEnd ();
}
};
label = new Label { Text = "_Commands:", X = Pos.Right (viewLog) + 1, Y = Pos.Top (label) };
ObservableCollection<string> commandLogList = [];
ListView commandLog = new ()
{
X = Pos.Left (label),
Y = Pos.Bottom (label),
Width = Dim.Auto (minimumContentDim: Dim.Percent (15)),
Height = Dim.Fill (),
SchemeName = SchemeManager.SchemesToSchemeName (Schemes.Base),
Source = new ListWrapper<string> (commandLogList)
};
runnable.Add (label, commandLog);
demo.Activating += (_, args) =>
{
commandLogList.Add ($"{args.Context!.Source.ToIdentifyingString()}:{args.Context!.Command}");
commandLog.MoveEnd ();
args.Handled = true;
};
demo.Accepting += (_, args) =>
{
commandLogList.Add ($"{args.Context!.Source.ToIdentifyingString()}:{args.Context!.Command}");
commandLog.MoveEnd ();
args.Handled = true;
};
demo.CommandNotBound += (_, args) =>
{
commandLogList.Add ($"{args.Context!.Source.ToIdentifyingString()}:{args.Context!.Command}");
commandLog.MoveEnd ();
args.Handled = true;
};
demoInPadding.Activating += (_, args) =>
{
commandLogList.Add ($"{args.Context!.Source.ToIdentifyingString()}:{args.Context!.Command}");
commandLog.MoveEnd ();
args.Handled = true;
};
demoInPadding.Accepting += (_, args) =>
{
commandLogList.Add ($"{args.Context!.Source.ToIdentifyingString()}:{args.Context!.Command}");
commandLog.MoveEnd ();
args.Handled = true;
};
sub1.Activating += (_, args) =>
{
commandLogList.Add ($"{args.Context!.Source.ToIdentifyingString()}:{args.Context!.Command}");
commandLog.MoveEnd ();
args.Handled = true;
};
sub1.Accepting += (_, args) =>
{
commandLogList.Add ($"{args.Context!.Source.ToIdentifyingString()}:{args.Context!.Command}");
commandLog.MoveEnd ();
args.Handled = true;
};
sub2.Activating += (_, args) =>
{
commandLogList.Add ($"{args.Context!.Source.ToIdentifyingString()}:{args.Context!.Command}");
commandLog.MoveEnd ();
args.Handled = true;
};
sub2.Accepting += (_, args) =>
{
commandLogList.Add ($"{args.Context!.Source.ToIdentifyingString()}:{args.Context!.Command}");
commandLog.MoveEnd ();
args.Handled = true;
};
runnable.CommandNotBound += (_, args) =>
{
if (args.Context!.Command == Command.DeleteAll)
{
driverLogList.Clear ();
driverLog.SetSource (driverLogList);
appLogList.Clear ();
appLog.SetSource (appLogList);
viewLogList.Clear ();
viewLog.SetSource (viewLogList);
commandLogList.Clear ();
commandLog.SetSource (commandLogList);
args.Handled = true;
}
};
app.Run (runnable);
return;
void DemoPaddingOnInitialized (object? o, EventArgs eventArgs) => demo.Padding!.Thickness = demo.Padding.Thickness with { Top = 5 };
}
public class MouseEventDemoView : View
{
public MouseEventDemoView ()
{
CanFocus = true;
Id = "mouseEventDemoView";
MouseLeave += (_, _) => { Text = "Leave"; };
MouseEnter += (_, _) => { Text = "Enter"; };
// Bind mouse events to commands that will illustrate command handling
MouseBindings.ReplaceCommands (MouseFlags.LeftButtonPressed, Command.Down);
MouseBindings.ReplaceCommands (MouseFlags.LeftButtonReleased, Command.Up);
MouseBindings.ReplaceCommands (MouseFlags.LeftButtonClicked, Command.Accept);
MouseBindings.ReplaceCommands (MouseFlags.LeftButtonDoubleClicked, Command.Open);
}
/// <inheritdoc/>
public override void EndInit ()
{
SchemeName = SchemeManager.SchemesToSchemeName (Schemes.Base);
TextAlignment = Alignment.Center;
VerticalTextAlignment = Alignment.Center;
Padding!.Thickness = new Thickness (1, 1, 1, 1);
Padding!.SetScheme (new Scheme (new Attribute (Color.DarkGray)));
Padding.Id = $"{Id}.Padding";
Border!.Thickness = new Thickness (1);
Border.LineStyle = LineStyle.Rounded;
Border.Id = $"{Id}.Border";
base.EndInit ();
}
/// <inheritdoc/>
protected override void OnMouseStateChanged (EventArgs<MouseState> args)
{
base.OnMouseStateChanged (args);
Border!.LineStyle = args.Value.HasFlag (MouseState.PressedOutside) ? LineStyle.Dotted : LineStyle.Single;
SetNeedsDraw ();
}
/// <inheritdoc/>
protected override bool OnGettingAttributeForRole (in VisualRole role, ref Attribute currentAttribute)
{
switch (role)
{
case VisualRole.Normal when MouseState.HasFlag (MouseState.Pressed) && MouseHighlightStates.HasFlag (MouseState.Pressed):
currentAttribute = currentAttribute with { Background = currentAttribute.Foreground.GetBrighterColor () };
return true;
default:
return base.OnGettingAttributeForRole (in role, ref currentAttribute);
}
}
}
}
// All the MouseFlags we can set for filtering
[Flags]
internal enum DemoMouseFlags
{
/// <summary>
/// No mouse event. This is the default value for <see cref="Mouse.Flags"/> when no mouse event is being
/// No mouse event. This is the default value for <see cref="Mouse.Flags"/> when no mouse event is being
/// reported.
/// </summary>
None = 0,
/// <summary>The first mouse button was pressed.</summary>
LeftButtonPressed = 0x2,
/// <summary>The first mouse button was released.</summary>
LeftButtonReleased = 0x1,
/// <summary>The first mouse button was clicked (press+release).</summary>
LeftButtonClicked = 0x4,
/// <summary>The first mouse button was double-clicked.</summary>
LeftButtonDoubleClicked = 0x8,
/// <summary>The first mouse button was triple-clicked.</summary>
LeftButtonTripleClicked = 0x10,
/// <summary>The second mouse button was pressed.</summary>
MiddleButtonPressed = 0x80,
/// <summary>The second mouse button was released.</summary>
MiddleButtonReleased = 0x40,
/// <summary>The second mouse button was clicked (press+release).</summary>
MiddleButtonClicked = 0x100,
/// <summary>The second mouse button was double-clicked.</summary>
MiddleButtonDoubleClicked = 0x200,
/// <summary>The second mouse button was triple-clicked.</summary>
MiddleButtonTripleClicked = 0x400,
/// <summary>The third mouse button was pressed.</summary>
RightButtonPressed = 0x2000,
/// <summary>The third mouse button was released.</summary>
RightButtonReleased = 0x1000,
/// <summary>The third mouse button was clicked (press+release).</summary>
RightButtonClicked = 0x4000,
/// <summary>The third mouse button was double-clicked.</summary>
RightButtonDoubleClicked = 0x8000,
/// <summary>The third mouse button was triple-clicked.</summary>
RightButtonTripleClicked = 0x10000,
/// <summary>The fourth mouse button was pressed.</summary>
Button4Pressed = 0x80000,
/// <summary>The fourth mouse button was released.</summary>
Button4Released = 0x40000,
/// <summary>The fourth mouse button was clicked.</summary>
Button4Clicked = 0x100000,
/// <summary>The fourth mouse button was double-clicked.</summary>
Button4DoubleClicked = 0x200000,
/// <summary>The fourth mouse button was triple-clicked.</summary>
Button4TripleClicked = 0x400000,
/// <summary>The mouse position is being reported in this event.</summary>
PositionReport = 0x8000000,
/// <summary>Vertical button wheeled up.</summary>
WheeledUp = 0x10000000,
/// <summary>Vertical button wheeled down.</summary>
WheeledDown = 0x20000000,
/// <summary>Vertical button wheeled up while pressing Ctrl.</summary>
WheeledLeft = 0x1000000 | WheeledUp,
/// <summary>Vertical button wheeled down while pressing Ctrl.</summary>
WheeledRight = 0x1000000 | WheeledDown,
All = -1
}