Skip to content

Fixes #4172 Timeout revamp and remove continuous mouse #4173

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 68 commits into from
Jul 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
6402f3a
Remove continous press code from Application
tznind Jun 3, 2025
a7a5d1a
WIP prototype code to handle continuous press as subcomponent of View
tznind Jun 3, 2025
8f43909
Prototype with Button
tznind Jun 3, 2025
cbb20ff
Implement CWP
tznind Jun 3, 2025
b91a9ca
Move to seperate classes and prevent double entry to Start
tznind Jun 4, 2025
a19145a
Fix repeat clicking when moving mouse by removing phantom click code …
tznind Jun 4, 2025
d8335c4
Remove initial tick because it results in double activation e.g. butt…
tznind Jun 4, 2025
98947ba
Refactor DatePicker lamdas
tznind Jun 14, 2025
e946638
Merge branch 'v2_develop' into 4101_continuous_press
tznind Jun 14, 2025
550ef03
WIP investigate subcomponents instead of statics
tznind Jun 14, 2025
a28cc6c
Add IMouseGrabHandler to IApplication
tznind Jun 14, 2025
6d794f2
Make mouse grabbing non static activity
tznind Jun 14, 2025
ee7ea86
Make MouseHeldDown suppress when null fields e.g. app not initialized…
tznind Jun 14, 2025
887631d
Update test and remove dependency on Application
tznind Jun 14, 2025
0d75ef0
Fix other mouse click and hold tests
tznind Jun 14, 2025
c3c6c70
Code cleanup
tznind Jun 14, 2025
f717be5
Update class diagram
tznind Jun 14, 2025
32d747a
Fix bad xml doc references
tznind Jun 14, 2025
e9a33cb
Fix timed events not getting passed through in v2 applications
tznind Jun 14, 2025
48e80f3
Make timed events nullable for tests that dont create an Application
tznind Jun 14, 2025
06e45e1
Remove strange blocking test
tznind Jun 15, 2025
c106ff0
WIP remove all idles and replace with zero timeouts
tznind Jun 19, 2025
6f11fd6
Fix build of tests
tznind Jun 19, 2025
c597454
Fix unit tests
tznind Jun 20, 2025
aba2e43
Add wakeup call back in
tznind Jun 20, 2025
2dfe6b3
Comment out incredibly complicated test and fix others
tznind Jun 20, 2025
f18b752
Fix test
tznind Jun 20, 2025
34bc316
test fix
tznind Jun 20, 2025
0016a18
Make Post execute immediately if already on UI thread
tznind Jun 20, 2025
e85cccd
Re enable test and simplify Invoke to just execute if in UI thread (u…
tznind Jun 20, 2025
e4c7f0f
Remove xml doc references to idles
tznind Jun 21, 2025
e13ed63
Remove more references to idles
tznind Jun 21, 2025
c588e04
Make Screen initialization threadsafe
tznind Jun 21, 2025
c2390ad
merge 4101_continuous_press into branch
tznind Jun 21, 2025
49dc897
Add more exciting timeouts
tznind Jun 21, 2025
394794a
WIP add tests
tznind Jun 21, 2025
308b26f
fix log
tznind Jun 21, 2025
e20b489
fix test
tznind Jun 21, 2025
78c5d0d
make continuous key press use smoth acceleration
tznind Jun 21, 2025
966178e
Rename _lock to _lockScreen
tznind Jun 21, 2025
8721ea3
Remove section on idles, they are not a thing anymore - and they kind…
tznind Jun 21, 2025
53de140
Add nullable enable
tznind Jun 21, 2025
7fb15fa
Add xml comment
tznind Jun 21, 2025
889e071
Fix namings and cleanup code
tznind Jun 21, 2025
77e8675
Merge branch 'v2_develop' into logarithmic-timeout
tig Jun 24, 2025
144b027
Merge branch 'v2_develop' into logarithmic-timeout
tig Jun 27, 2025
de953f7
Merge branch 'v2_develop' into logarithmic-timeout
tznind Jul 5, 2025
f6b9fe7
xmldoc fix
tznind Jul 5, 2025
65207ad
Rename LockAndRunTimers to just RunTimers
tznind Jul 5, 2025
079a4c5
Rename AddTimeout and RemoveTimeout (and event) to just Add/Remove
tznind Jul 5, 2025
768e5bd
Update description of MainLoop
tznind Jul 5, 2025
fb19bfa
Merge branch 'v2_develop' into logarithmic-timeout
tig Jul 7, 2025
87fe518
Merge branch 'v2_develop' of tig:tig/Terminal.Gui into v2_develop
tig Jul 7, 2025
62fd462
Merge branch 'v2_develop' of tig:tig/Terminal.Gui into v2_develop
tig Jul 7, 2025
1efaf49
Merge branch 'v2_develop' into logarithmic-timeout
tig Jul 7, 2025
837298a
Merge branch 'logarithmic-timeout' of github.com:tznind/gui.cs into t…
tig Jul 7, 2025
a1ea545
Commented out Run_T_Call_Init_ForceDriver_Should_Pick_Correct_Driver
tig Jul 7, 2025
8abb13c
Again? Commented out Run_T_Call_Init_ForceDriver_Should_Pick_Correct_…
tig Jul 7, 2025
6515d88
Revert Commented out Run_T_Call_Init_ForceDriver_Should_Pick_Correct_…
tig Jul 7, 2025
a938fc1
When mouse is released from MouseHeldDown reset host MouseState
tznind Jul 9, 2025
ad8248a
Fix namespaces in class diagram
tznind Jul 10, 2025
ee22ef8
Apply @BDisp suggested fix
tznind Jul 10, 2025
d47defc
Fix class diagrams
tznind Jul 10, 2025
c477cce
Add lock
tznind Jul 10, 2025
28787e8
Make TimeSpan.Zero definetly run
tznind Jul 10, 2025
5fc2e48
Fix duplicate entry in package props
tznind Jul 10, 2025
47aa0cd
Code cleanup and better API docs
tig Jul 10, 2025
6894b58
Merge pull request #178 from tig/tznind-logarithmic-timeout
tznind Jul 10, 2025
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
3 changes: 0 additions & 3 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@
<PackageVersion Include="Microsoft.CodeAnalysis" Version="4.11.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.11.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" />

<PackageVersion Include="Microsoft.Net.Compilers.Toolset" Version="4.11.0" />

<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="[9.0.2,10)" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.6" />
<PackageVersion Include="System.IO.Abstractions" Version="[22.0.11,23)" />
Expand Down
88 changes: 88 additions & 0 deletions Examples/UICatalog/Scenarios/Threading.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ public class Threading : Scenario
private ListView _logJob;
private Action _sync;

private LogarithmicTimeout _logarithmicTimeout;
private NumericUpDown _numberLog;
private Button _btnLogarithmic;
private object _timeoutObj;

private SmoothAcceleratingTimeout _smoothTimeout;
private NumericUpDown _numberSmooth;
private Button _btnSmooth;
private object _timeoutObjSmooth;

public override void Main ()
{
Application.Init ();
Expand Down Expand Up @@ -82,6 +92,35 @@ public override void Main ()

var text = new TextField { X = 1, Y = 3, Width = 100, Text = "Type anything after press the button" };

_btnLogarithmic = new Button ()
{
X = 50,
Y = 4,
Text = "Start Log Counter"
};
_btnLogarithmic.Accepting += StartStopLogTimeout;

_numberLog = new NumericUpDown ()
{
X = Pos.Right (_btnLogarithmic),
Y = 4,
};

_btnSmooth = new Button ()
{
X = Pos.Right (_numberLog),
Y = 4,
Text = "Start Smooth Counter"
};
_btnSmooth.Accepting += StartStopSmoothTimeout;

_numberSmooth = new NumericUpDown ()
{
X = Pos.Right (_btnSmooth),
Y = 4,
};


var btnAction = new Button { X = 80, Y = 10, Text = "Load Data Action" };
btnAction.Accepting += (s, e) => _action.Invoke ();
var btnLambda = new Button { X = 80, Y = 12, Text = "Load Data Lambda" };
Expand All @@ -107,6 +146,10 @@ public override void Main ()
_btnActionCancel,
_logJob,
text,
_btnLogarithmic,
_numberLog,
_btnSmooth,
_numberSmooth,
btnAction,
btnLambda,
btnHandler,
Expand All @@ -129,6 +172,51 @@ void Win_Loaded (object sender, EventArgs args)
Application.Shutdown ();
}

private bool LogTimeout ()
{
_numberLog.Value++;
_logarithmicTimeout.AdvanceStage ();
return true;
}
private bool SmoothTimeout ()
{
_numberSmooth.Value++;
_smoothTimeout.AdvanceStage ();
return true;
}

private void StartStopLogTimeout (object sender, CommandEventArgs e)
{
if (_timeoutObj != null)
{
_btnLogarithmic.Text = "Start Log Counter";
Application.TimedEvents.Remove (_timeoutObj);
_timeoutObj = null;
}
else
{
_btnLogarithmic.Text = "Stop Log Counter";
_logarithmicTimeout = new LogarithmicTimeout (TimeSpan.FromMilliseconds (500), LogTimeout);
_timeoutObj = Application.TimedEvents.Add (_logarithmicTimeout);
}
}

private void StartStopSmoothTimeout (object sender, CommandEventArgs e)
{
if (_timeoutObjSmooth != null)
{
_btnSmooth.Text = "Start Smooth Counter";
Application.TimedEvents.Remove (_timeoutObjSmooth);
_timeoutObjSmooth = null;
}
else
{
_btnSmooth.Text = "Stop Smooth Counter";
_smoothTimeout = new SmoothAcceleratingTimeout (TimeSpan.FromMilliseconds (500), TimeSpan.FromMilliseconds (50), 0.5, SmoothTimeout);
_timeoutObjSmooth = Application.TimedEvents.Add (_smoothTimeout);
}
}

private async void CallLoadItemsAsync ()
{
_cancellationTokenSource = new CancellationTokenSource ();
Expand Down
146 changes: 13 additions & 133 deletions Terminal.Gui/App/Application.Mouse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,122 +19,16 @@ public static partial class Application // Mouse handling
[ConfigurationProperty (Scope = typeof (SettingsScope))]
public static bool IsMouseDisabled { get; set; }

/// <summary>Gets <see cref="View"/> that has registered to get continuous mouse button pressed events.</summary>
public static View? WantContinuousButtonPressedView { get; internal set; }

/// <summary>
/// Gets the view that grabbed the mouse (e.g. for dragging). When this is set, all mouse events will be routed to
/// this view until the view calls <see cref="UngrabMouse"/> or the mouse is released.
/// Static reference to the current <see cref="IApplication"/> <see cref="IMouseGrabHandler"/>.
/// </summary>
public static View? MouseGrabView { get; private set; }

/// <summary>Invoked when a view wants to grab the mouse; can be canceled.</summary>
public static event EventHandler<GrabMouseEventArgs>? GrabbingMouse;

/// <summary>Invoked when a view wants un-grab the mouse; can be canceled.</summary>
public static event EventHandler<GrabMouseEventArgs>? UnGrabbingMouse;

/// <summary>Invoked after a view has grabbed the mouse.</summary>
public static event EventHandler<ViewEventArgs>? GrabbedMouse;

/// <summary>Invoked after a view has un-grabbed the mouse.</summary>
public static event EventHandler<ViewEventArgs>? UnGrabbedMouse;

/// <summary>
/// Grabs the mouse, forcing all mouse events to be routed to the specified view until <see cref="UngrabMouse"/>
/// is called.
/// </summary>
/// <param name="view">View that will receive all mouse events until <see cref="UngrabMouse"/> is invoked.</param>
public static void GrabMouse (View? view)
public static IMouseGrabHandler MouseGrabHandler
{
if (view is null || RaiseGrabbingMouseEvent (view))
{
return;
}

RaiseGrabbedMouseEvent (view);

if (Initialized)
{
// MouseGrabView is a static; only set if the application is initialized.
MouseGrabView = view;
}
get => ApplicationImpl.Instance.MouseGrabHandler;
set => ApplicationImpl.Instance.MouseGrabHandler = value ??
throw new ArgumentNullException(nameof(value));
}

/// <summary>Releases the mouse grab, so mouse events will be routed to the view on which the mouse is.</summary>
public static void UngrabMouse ()
{
if (MouseGrabView is null)
{
return;
}

#if DEBUG_IDISPOSABLE
if (View.EnableDebugIDisposableAsserts)
{
ObjectDisposedException.ThrowIf (MouseGrabView.WasDisposed, MouseGrabView);
}
#endif

if (!RaiseUnGrabbingMouseEvent (MouseGrabView))
{
View view = MouseGrabView;
MouseGrabView = null;
RaiseUnGrabbedMouseEvent (view);
}
}

/// <exception cref="Exception">A delegate callback throws an exception.</exception>
private static bool RaiseGrabbingMouseEvent (View? view)
{
if (view is null)
{
return false;
}

var evArgs = new GrabMouseEventArgs (view);
GrabbingMouse?.Invoke (view, evArgs);

return evArgs.Cancel;
}

/// <exception cref="Exception">A delegate callback throws an exception.</exception>
private static bool RaiseUnGrabbingMouseEvent (View? view)
{
if (view is null)
{
return false;
}

var evArgs = new GrabMouseEventArgs (view);
UnGrabbingMouse?.Invoke (view, evArgs);

return evArgs.Cancel;
}

/// <exception cref="Exception">A delegate callback throws an exception.</exception>
private static void RaiseGrabbedMouseEvent (View? view)
{
if (view is null)
{
return;
}

GrabbedMouse?.Invoke (view, new (view));
}

/// <exception cref="Exception">A delegate callback throws an exception.</exception>
private static void RaiseUnGrabbedMouseEvent (View? view)
{
if (view is null)
{
return;
}

UnGrabbedMouse?.Invoke (view, new (view));
}


/// <summary>
/// INTERNAL API: Called when a mouse event is raised by the driver. Determines the view under the mouse and
/// calls the appropriate View mouse event handlers.
Expand Down Expand Up @@ -198,15 +92,6 @@ internal static void RaiseMouseEvent (MouseEventArgs mouseEvent)
return;
}

if (Initialized)
{
WantContinuousButtonPressedView = deepestViewUnderMouse switch
{
{ WantContinuousButtonPressed: true } => deepestViewUnderMouse,
_ => null
};
}

// May be null before the prior condition or the condition may set it as null.
// So, the checking must be outside the prior condition.
if (deepestViewUnderMouse is null)
Expand Down Expand Up @@ -258,12 +143,7 @@ internal static void RaiseMouseEvent (MouseEventArgs mouseEvent)

RaiseMouseEnterLeaveEvents (viewMouseEvent.ScreenPosition, currentViewsUnderMouse);

if (Initialized)
{
WantContinuousButtonPressedView = deepestViewUnderMouse.WantContinuousButtonPressed ? deepestViewUnderMouse : null;
}

while (deepestViewUnderMouse.NewMouseEvent (viewMouseEvent) is not true && MouseGrabView is not { })
while (deepestViewUnderMouse.NewMouseEvent (viewMouseEvent) is not true && MouseGrabHandler.MouseGrabView is not { })
{
if (deepestViewUnderMouse is Adornment adornmentView)
{
Expand Down Expand Up @@ -315,35 +195,35 @@ internal static void RaiseMouseEvent (MouseEventArgs mouseEvent)

internal static bool HandleMouseGrab (View? deepestViewUnderMouse, MouseEventArgs mouseEvent)
{
if (MouseGrabView is { })
if (MouseGrabHandler.MouseGrabView is { })
{
#if DEBUG_IDISPOSABLE
if (View.EnableDebugIDisposableAsserts && MouseGrabView.WasDisposed)
if (View.EnableDebugIDisposableAsserts && MouseGrabHandler.MouseGrabView.WasDisposed)
{
throw new ObjectDisposedException (MouseGrabView.GetType ().FullName);
throw new ObjectDisposedException (MouseGrabHandler.MouseGrabView.GetType ().FullName);
}
#endif

// 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.ScreenToViewport (mouseEvent.ScreenPosition);
Point frameLoc = MouseGrabHandler.MouseGrabView.ScreenToViewport (mouseEvent.ScreenPosition);

var viewRelativeMouseEvent = new MouseEventArgs
{
Position = frameLoc,
Flags = mouseEvent.Flags,
ScreenPosition = mouseEvent.ScreenPosition,
View = deepestViewUnderMouse ?? MouseGrabView
View = deepestViewUnderMouse ?? MouseGrabHandler.MouseGrabView
};

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

// ReSharper disable once ConditionIsAlwaysTrueOrFalse
if (MouseGrabView is null && deepestViewUnderMouse is Adornment)
if (MouseGrabHandler.MouseGrabView is null && deepestViewUnderMouse is Adornment)
{
// The view that grabbed the mouse has been disposed
return true;
Expand Down
7 changes: 3 additions & 4 deletions Terminal.Gui/App/Application.Run.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,9 @@ public static RunState Begin (Toplevel toplevel)
//#endif

// Ensure the mouse is ungrabbed.
if (MouseGrabView is { })
if (MouseGrabHandler.MouseGrabView is { })
{
UngrabMouse ();
MouseGrabView = null;
MouseGrabHandler.UngrabMouse ();
}

var rs = new RunState (toplevel);
Expand Down Expand Up @@ -366,7 +365,7 @@ public static T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDriv
/// Alternatively, to have a program control the main loop and process events manually, call
/// <see cref="Begin(Toplevel)"/> to set things up manually and then repeatedly call
/// <see cref="RunLoop(RunState)"/> with the wait parameter set to false. By doing this the
/// <see cref="RunLoop(RunState)"/> method will only process any pending events, timers, idle handlers and then
/// <see cref="RunLoop(RunState)"/> method will only process any pending events, timers handlers and then
/// return control immediately.
/// </para>
/// <para>
Expand Down
17 changes: 13 additions & 4 deletions Terminal.Gui/App/Application.Screen.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ namespace Terminal.Gui.App;

public static partial class Application // Screen related stuff
{
private static readonly object _lockScreen = new ();
private static Rectangle? _screen;

/// <summary>
Expand All @@ -18,19 +19,27 @@ public static Rectangle Screen
{
get
{
if (_screen == null)
lock (_lockScreen)
{
_screen = Driver?.Screen ?? new (new (0, 0), new (2048, 2048));
if (_screen == null)
{
_screen = Driver?.Screen ?? new (new (0, 0), new (2048, 2048));
}

return _screen.Value;
}
return _screen.Value;
}
set
{
if (value is {} && (value.X != 0 || value.Y != 0))
{
throw new NotImplementedException ($"Screen locations other than 0, 0 are not yet supported");
}
_screen = value;

lock (_lockScreen)
{
_screen = value;
}
}
}

Expand Down
Loading
Loading