Skip to content

Commit

Permalink
Merge pull request #3372 from tig/v2_3370_continuous_button
Browse files Browse the repository at this point in the history
Fixes #3370. Adds visual highlight for press and hold (for `Button`, `Checkbox`, `RadioGroup`, and `Border`)
  • Loading branch information
tig authored Apr 8, 2024
2 parents ef6e22c + d71554d commit f1bc42a
Show file tree
Hide file tree
Showing 66 changed files with 2,260 additions and 1,552 deletions.
86 changes: 42 additions & 44 deletions Terminal.Gui/Application.cs
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ internal static void InternalInit (
private static void Driver_SizeChanged (object sender, SizeChangedEventArgs e) { OnSizeChanging (e); }
private static void Driver_KeyDown (object sender, Key e) { OnKeyDown (e); }
private static void Driver_KeyUp (object sender, Key e) { OnKeyUp (e); }
private static void Driver_MouseEvent (object sender, MouseEventEventArgs e) { OnMouseEvent (e); }
private static void Driver_MouseEvent (object sender, MouseEvent e) { OnMouseEvent (e); }

/// <summary>Gets of list of <see cref="ConsoleDriver"/> types that are available.</summary>
/// <returns></returns>
Expand Down Expand Up @@ -1441,38 +1441,29 @@ private static void OnUnGrabbedMouse (View view)
/// </para>
/// <para>The <see cref="MouseEvent.View"/> will contain the <see cref="View"/> that contains the mouse coordinates.</para>
/// </remarks>
public static event EventHandler<MouseEventEventArgs> MouseEvent;
public static event EventHandler<MouseEvent> MouseEvent;

Check warning on line 1444 in Terminal.Gui/Application.cs

View workflow job for this annotation

GitHub Actions / Build and Publish to Nuget.org

Non-nullable event 'MouseEvent' must contain a non-null value when exiting constructor. Consider declaring the event as nullable.

Check warning on line 1444 in Terminal.Gui/Application.cs

View workflow job for this annotation

GitHub Actions / build_and_test

Non-nullable event 'MouseEvent' must contain a non-null value when exiting constructor. Consider declaring the event as nullable.

/// <summary>Called when a mouse event occurs. Raises the <see cref="MouseEvent"/> event.</summary>
/// <remarks>This method can be used to simulate a mouse event, e.g. in unit tests.</remarks>
/// <param name="a">The mouse event with coordinates relative to the screen.</param>

Check warning on line 1448 in Terminal.Gui/Application.cs

View workflow job for this annotation

GitHub Actions / Build and Publish to Nuget.org

XML comment has a param tag for 'a', but there is no parameter by that name

Check warning on line 1448 in Terminal.Gui/Application.cs

View workflow job for this annotation

GitHub Actions / build_and_test

XML comment has a param tag for 'a', but there is no parameter by that name
internal static void OnMouseEvent (MouseEventEventArgs a)
internal static void OnMouseEvent (MouseEvent mouseEvent)

Check warning on line 1449 in Terminal.Gui/Application.cs

View workflow job for this annotation

GitHub Actions / Build and Publish to Nuget.org

Parameter 'mouseEvent' has no matching param tag in the XML comment for 'Application.OnMouseEvent(MouseEvent)' (but other parameters do)

Check warning on line 1449 in Terminal.Gui/Application.cs

View workflow job for this annotation

GitHub Actions / build_and_test

Parameter 'mouseEvent' has no matching param tag in the XML comment for 'Application.OnMouseEvent(MouseEvent)' (but other parameters do)
{
if (IsMouseDisabled)
{
return;
}

// TODO: In PR #3273, FindDeepestView will return adornments. Update logic below to fix adornment mouse handling
var view = View.FindDeepestView (Current, a.MouseEvent.X, a.MouseEvent.Y);

if (view is { WantContinuousButtonPressed: true })
{
WantContinuousButtonPressedView = view;
}
else
{
WantContinuousButtonPressedView = null;
}
var view = View.FindDeepestView (Current, mouseEvent.X, mouseEvent.Y);

if (view is { })
{
a.MouseEvent.View = view;
mouseEvent.View = view;
}

MouseEvent?.Invoke (null, new (a.MouseEvent));
MouseEvent?.Invoke (null, mouseEvent);

if (a.MouseEvent.Handled)
if (mouseEvent.Handled)
{
return;
}
Expand All @@ -1481,45 +1472,52 @@ internal static void OnMouseEvent (MouseEventEventArgs a)
{
// If the mouse is grabbed, send the event to the view that grabbed it.
// The coordinates are relative to the Bounds of the view that grabbed the mouse.
Point frameLoc = MouseGrabView.ScreenToFrame (a.MouseEvent.X, a.MouseEvent.Y);
Point boundsLoc = MouseGrabView.ScreenToBounds (mouseEvent.X, mouseEvent.Y);

var viewRelativeMouseEvent = new MouseEvent
{
X = frameLoc.X,
Y = frameLoc.Y,
Flags = a.MouseEvent.Flags,
ScreenPosition = new (a.MouseEvent.X, a.MouseEvent.Y),
View = view
X = boundsLoc.X,
Y = boundsLoc.Y,
Flags = mouseEvent.Flags,
ScreenPosition = new (mouseEvent.X, mouseEvent.Y),
View = MouseGrabView
};

if (MouseGrabView.Bounds.Contains (viewRelativeMouseEvent.X, viewRelativeMouseEvent.Y) is false)
{
// The mouse has moved outside the bounds of the view that
// grabbed the mouse, so we tell the view that last got
// OnMouseEnter the mouse is leaving
// BUGBUG: That sentence makes no sense. Either I'm missing something or this logic is flawed.
_mouseEnteredView?.OnMouseLeave (a.MouseEvent);
// The mouse has moved outside the bounds of the view that grabbed the mouse
_mouseEnteredView?.NewMouseLeaveEvent (mouseEvent);
}

//System.Diagnostics.Debug.WriteLine ($"{nme.Flags};{nme.X};{nme.Y};{mouseGrabView}");
if (MouseGrabView?.OnMouseEvent (viewRelativeMouseEvent) == true)
if (MouseGrabView?.NewMouseEvent (viewRelativeMouseEvent) == true)
{
return;
}
}

if (view is { WantContinuousButtonPressed: true })
{
WantContinuousButtonPressedView = view;
}
else
{
WantContinuousButtonPressedView = null;
}


if (view is not Adornment)
{
if ((view is null || view == OverlappedTop)
&& Current is { Modal: false }
&& OverlappedTop != null
&& a.MouseEvent.Flags != MouseFlags.ReportMousePosition
&& a.MouseEvent.Flags != 0)
&& mouseEvent.Flags != MouseFlags.ReportMousePosition
&& mouseEvent.Flags != 0)
{
// This occurs when there are multiple overlapped "tops"
// E.g. "Mdi" - in the Background Worker Scenario
View? top = FindDeepestTop (Top, a.MouseEvent.X, a.MouseEvent.Y);
view = View.FindDeepestView (top, a.MouseEvent.X, a.MouseEvent.Y);
View? top = FindDeepestTop (Top, mouseEvent.X, mouseEvent.Y);
view = View.FindDeepestView (top, mouseEvent.X, mouseEvent.Y);

if (view is { } && view != OverlappedTop && top != Current)
{
Expand All @@ -1537,27 +1535,27 @@ internal static void OnMouseEvent (MouseEventEventArgs a)

if (view is Adornment adornment)
{
Point frameLoc = adornment.ScreenToFrame (a.MouseEvent.X, a.MouseEvent.Y);
Point frameLoc = adornment.ScreenToFrame (mouseEvent.X, mouseEvent.Y);

me = new ()
{
X = frameLoc.X,
Y = frameLoc.Y,
Flags = a.MouseEvent.Flags,
ScreenPosition = new (a.MouseEvent.X, a.MouseEvent.Y),
Flags = mouseEvent.Flags,
ScreenPosition = new (mouseEvent.X, mouseEvent.Y),
View = view
};
}
else if (view.BoundsToScreen (view.Bounds).Contains (a.MouseEvent.X, a.MouseEvent.Y))
else if (view.BoundsToScreen (view.Bounds).Contains (mouseEvent.X, mouseEvent.Y))
{
Point boundsPoint = view.ScreenToBounds (a.MouseEvent.X, a.MouseEvent.Y);
Point boundsPoint = view.ScreenToBounds (mouseEvent.X, mouseEvent.Y);

me = new ()
{
X = boundsPoint.X,
Y = boundsPoint.Y,
Flags = a.MouseEvent.Flags,
ScreenPosition = new (a.MouseEvent.X, a.MouseEvent.Y),
Flags = mouseEvent.Flags,
ScreenPosition = new (mouseEvent.X, mouseEvent.Y),
View = view
};
}
Expand All @@ -1570,16 +1568,16 @@ internal static void OnMouseEvent (MouseEventEventArgs a)
if (_mouseEnteredView is null)
{
_mouseEnteredView = view;
view.OnMouseEnter (me);
view.NewMouseEnterEvent (me);
}
else if (_mouseEnteredView != view)
{
_mouseEnteredView.OnMouseLeave (me);
view.OnMouseEnter (me);
_mouseEnteredView.NewMouseLeaveEvent (me);
view.NewMouseEnterEvent (me);
_mouseEnteredView = view;
}

if (!view.WantMousePositionReports && a.MouseEvent.Flags == MouseFlags.ReportMousePosition)
if (!view.WantMousePositionReports && mouseEvent.Flags == MouseFlags.ReportMousePosition)
{
return;
}
Expand All @@ -1588,7 +1586,7 @@ internal static void OnMouseEvent (MouseEventEventArgs a)

//Debug.WriteLine ($"OnMouseEvent: ({a.MouseEvent.X},{a.MouseEvent.Y}) - {a.MouseEvent.Flags}");

if (view.OnMouseEvent (me))
if (view.NewMouseEvent (me) == false)
{
// Should we bubble up the event, if it is not handled?
//return;
Expand Down
4 changes: 2 additions & 2 deletions Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -538,11 +538,11 @@ public virtual Attribute MakeColor (in Color foreground, in Color background)
public void OnKeyUp (Key a) { KeyUp?.Invoke (this, a); }

/// <summary>Event fired when a mouse event occurs.</summary>
public event EventHandler<MouseEventEventArgs> MouseEvent;
public event EventHandler<MouseEvent> MouseEvent;

/// <summary>Called when a mouse event occurs. Fires the <see cref="MouseEvent"/> event.</summary>
/// <param name="a"></param>
public void OnMouseEvent (MouseEventEventArgs a) { MouseEvent?.Invoke (this, a); }
public void OnMouseEvent (MouseEvent a) { MouseEvent?.Invoke (this, a); }

/// <summary>Simulates a key press.</summary>
/// <param name="keyChar">The key character.</param>
Expand Down
8 changes: 7 additions & 1 deletion Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Driver.cs: Curses-based Driver
//

using System.Diagnostics;
using System.Runtime.InteropServices;
using Terminal.Gui.ConsoleDrivers;
using Unix.Terminal;
Expand Down Expand Up @@ -798,6 +799,9 @@ bool IsButtonClickedOrDoubleClicked (MouseFlags flag)
|| flag.HasFlag (MouseFlags.Button4DoubleClicked);
}

Debug.WriteLine ($"CursesDriver: ({pos.X},{pos.Y}) - {mouseFlag}");


if ((WasButtonReleased (mouseFlag) && IsButtonNotPressed (_lastMouseFlags)) || (IsButtonClickedOrDoubleClicked (mouseFlag) && _lastMouseFlags == 0))
{
return;
Expand All @@ -806,7 +810,9 @@ bool IsButtonClickedOrDoubleClicked (MouseFlags flag)
_lastMouseFlags = mouseFlag;

var me = new MouseEvent { Flags = mouseFlag, X = pos.X, Y = pos.Y };
OnMouseEvent (new MouseEventEventArgs (me));
Debug.WriteLine ($"CursesDriver: ({me.X},{me.Y}) - {me.Flags}");

OnMouseEvent (me);
}

#region Color Handling
Expand Down
6 changes: 3 additions & 3 deletions Terminal.Gui/ConsoleDrivers/NetDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1137,8 +1137,8 @@ private void ProcessInput (InputResult inputEvent)
break;
case EventType.Mouse:
MouseEvent me = ToDriverMouse (inputEvent.MouseEvent);
//Debug.WriteLine ($"NetDriver: ({me.X},{me.Y}) - {me.Flags}");
OnMouseEvent (new MouseEventEventArgs (me));
Debug.WriteLine ($"NetDriver: ({me.X},{me.Y}) - {me.Flags}");
OnMouseEvent (me);

break;
case EventType.WindowSize:
Expand Down Expand Up @@ -1379,7 +1379,7 @@ public void StopReportingMouseMoves ()

private MouseEvent ToDriverMouse (NetEvents.MouseEvent me)
{
// System.Diagnostics.Debug.WriteLine ($"X: {me.Position.X}; Y: {me.Position.Y}; ButtonState: {me.ButtonState}");
//System.Diagnostics.Debug.WriteLine ($"X: {me.Position.X}; Y: {me.Position.Y}; ButtonState: {me.ButtonState}");

MouseFlags mouseFlag = 0;

Expand Down
41 changes: 26 additions & 15 deletions Terminal.Gui/ConsoleDrivers/WindowsDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1419,18 +1419,16 @@ internal void ProcessInput (WindowsConsole.InputRecord inputEvent)
break;
}

OnMouseEvent (new MouseEventEventArgs (me));
OnMouseEvent (me);

if (_processButtonClick)
{
OnMouseEvent (
new MouseEventEventArgs (
new MouseEvent
{
X = me.X,
Y = me.Y,
Flags = ProcessButtonClick (inputEvent.MouseEvent)
}));
OnMouseEvent (new ()
{
X = me.X,
Y = me.Y,
Flags = ProcessButtonClick (inputEvent.MouseEvent)
});
}

break;
Expand Down Expand Up @@ -1730,27 +1728,38 @@ private async Task ProcessButtonDoubleClickedAsync ()

private async Task ProcessContinuousButtonPressedAsync (MouseFlags mouseFlag)
{
// When a user presses-and-holds, start generating pressed events every `startDelay`
// After `iterationsUntilFast` iterations, speed them up to `fastDelay` ms
const int startDelay = 500;
const int iterationsUntilFast = 4;
const int fastDelay = 50;

int iterations = 0;
int delay = startDelay;
while (_isButtonPressed)
{
await Task.Delay (100);

var me = new MouseEvent
{
X = _pointMove.X,
Y = _pointMove.Y,
Flags = mouseFlag
};

View view = Application.WantContinuousButtonPressedView;

if (view is null)
if (Application.WantContinuousButtonPressedView is null)
{
break;
}

if (iterations++ >= iterationsUntilFast)
{
delay = fastDelay;
}
await Task.Delay (delay);

//Debug.WriteLine($"ProcessContinuousButtonPressedAsync: {view}");
if (_isButtonPressed && (mouseFlag & MouseFlags.ReportMousePosition) == 0)
{
Application.Invoke (() => OnMouseEvent (new MouseEventEventArgs (me)));
Application.Invoke (() => OnMouseEvent (me));
}
}
}
Expand Down Expand Up @@ -1919,6 +1928,8 @@ private MouseEvent ToDriverMouse (WindowsConsole.MouseEventRecord mouseEvent)
{
_point = null;
}
_processButtonClick = true;

}
else if (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseMoved
&& !_isOneFingerDoubleClicked
Expand Down
22 changes: 22 additions & 0 deletions Terminal.Gui/Drawing/Color.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text.Json.Serialization;
using ColorHelper;

namespace Terminal.Gui;

Expand Down Expand Up @@ -237,6 +238,27 @@ internal static ColorName GetClosestNamedColor (Color inputColor)
[SkipLocalsInit]
private static float CalculateColorDistance (in Vector4 color1, in Vector4 color2) { return Vector4.Distance (color1, color2); }

/// <summary>
/// Gets a color that is the same hue as the current color, but with a different lightness.
/// </summary>
/// <returns></returns>
public Color GetHighlightColor ()
{
// TODO: This is a temporary implementation; just enough to show how it could work.
var hsl = ColorHelper.ColorConverter.RgbToHsl(new RGB (R, G, B));

var amount = .7;
if (hsl.L <= 5)
{
return DarkGray;
}
hsl.L = (byte)(hsl.L * amount);

var rgb = ColorHelper.ColorConverter.HslToRgb (hsl);
return new (rgb.R, rgb.G, rgb.B);

}

#region Legacy Color Names

/// <summary>The black color.</summary>
Expand Down
1 change: 1 addition & 0 deletions Terminal.Gui/Terminal.Gui.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
<!-- Dependencies -->
<!-- =================================================================== -->
<ItemGroup>
<PackageReference Include="ColorHelper" Version="1.8.1" />
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0" />
<!-- Enable Nuget Source Link for github -->
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
Expand Down
Loading

0 comments on commit f1bc42a

Please sign in to comment.