diff --git a/Directory.Packages.props b/Directory.Packages.props index 2dc59e0df3..3d57c73d30 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -16,9 +16,6 @@ - - - diff --git a/Examples/UICatalog/Scenarios/Threading.cs b/Examples/UICatalog/Scenarios/Threading.cs index 1adde0320f..dc97292b20 100644 --- a/Examples/UICatalog/Scenarios/Threading.cs +++ b/Examples/UICatalog/Scenarios/Threading.cs @@ -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 (); @@ -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" }; @@ -107,6 +146,10 @@ public override void Main () _btnActionCancel, _logJob, text, + _btnLogarithmic, + _numberLog, + _btnSmooth, + _numberSmooth, btnAction, btnLambda, btnHandler, @@ -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 (); diff --git a/Terminal.Gui/App/Application.Mouse.cs b/Terminal.Gui/App/Application.Mouse.cs index 6e06ab759d..cab426f98a 100644 --- a/Terminal.Gui/App/Application.Mouse.cs +++ b/Terminal.Gui/App/Application.Mouse.cs @@ -19,122 +19,16 @@ public static partial class Application // Mouse handling [ConfigurationProperty (Scope = typeof (SettingsScope))] public static bool IsMouseDisabled { get; set; } - /// Gets that has registered to get continuous mouse button pressed events. - public static View? WantContinuousButtonPressedView { get; internal set; } - /// - /// 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 or the mouse is released. + /// Static reference to the current . /// - public static View? MouseGrabView { get; private set; } - - /// Invoked when a view wants to grab the mouse; can be canceled. - public static event EventHandler? GrabbingMouse; - - /// Invoked when a view wants un-grab the mouse; can be canceled. - public static event EventHandler? UnGrabbingMouse; - - /// Invoked after a view has grabbed the mouse. - public static event EventHandler? GrabbedMouse; - - /// Invoked after a view has un-grabbed the mouse. - public static event EventHandler? UnGrabbedMouse; - - /// - /// Grabs the mouse, forcing all mouse events to be routed to the specified view until - /// is called. - /// - /// View that will receive all mouse events until is invoked. - 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)); } - /// Releases the mouse grab, so mouse events will be routed to the view on which the mouse is. - 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); - } - } - - /// A delegate callback throws an 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; - } - - /// A delegate callback throws an 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; - } - - /// A delegate callback throws an exception. - private static void RaiseGrabbedMouseEvent (View? view) - { - if (view is null) - { - return; - } - - GrabbedMouse?.Invoke (view, new (view)); - } - - /// A delegate callback throws an exception. - private static void RaiseUnGrabbedMouseEvent (View? view) - { - if (view is null) - { - return; - } - - UnGrabbedMouse?.Invoke (view, new (view)); - } - - /// /// 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. @@ -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) @@ -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) { @@ -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; diff --git a/Terminal.Gui/App/Application.Run.cs b/Terminal.Gui/App/Application.Run.cs index d20336e5f8..30c1384a3e 100644 --- a/Terminal.Gui/App/Application.Run.cs +++ b/Terminal.Gui/App/Application.Run.cs @@ -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); @@ -366,7 +365,7 @@ public static T Run (Func? errorHandler = null, IConsoleDriv /// Alternatively, to have a program control the main loop and process events manually, call /// to set things up manually and then repeatedly call /// with the wait parameter set to false. By doing this the - /// method will only process any pending events, timers, idle handlers and then + /// method will only process any pending events, timers handlers and then /// return control immediately. /// /// diff --git a/Terminal.Gui/App/Application.Screen.cs b/Terminal.Gui/App/Application.Screen.cs index bd57585eab..cc9dcb0b66 100644 --- a/Terminal.Gui/App/Application.Screen.cs +++ b/Terminal.Gui/App/Application.Screen.cs @@ -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; /// @@ -18,11 +19,15 @@ 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 { @@ -30,7 +35,11 @@ public static Rectangle Screen { throw new NotImplementedException ($"Screen locations other than 0, 0 are not yet supported"); } - _screen = value; + + lock (_lockScreen) + { + _screen = value; + } } } diff --git a/Terminal.Gui/App/Application.cd b/Terminal.Gui/App/Application.cd index 9c22dd77ba..294a90411b 100644 --- a/Terminal.Gui/App/Application.cd +++ b/Terminal.Gui/App/Application.cd @@ -1,90 +1,116 @@  - + - hEI4FAgAqARIspQfBQo0gTGiACNL0AICESJKoggBSg8= - Application\Application.cs + gEK4FIgQOAQIuhQeBwoUgSCgAAJL0AACESIKoAiBWw8= + App\Application.cs - - + + AABAAAAAAABCAAAAAAAAAAAAAAAAIgIAAAAAAAAAAAA= - Application\ApplicationNavigation.cs + App\ApplicationNavigation.cs - - + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= - Application\IterationEventArgs.cs + App\IterationEventArgs.cs - - + + - CAAAIAAAASAAAQAQAAAAAIBADQAAEAAYIgIIwAAAAAI= - Application\MainLoop.cs + AAAAAAAAACAAAAAAAAAAAAAACBAAEAAIIAIAgAAAEAI= + App\MainLoop.cs - - + + AAAAAgAAAAAAAAAAAEAAAAAACAAAAAAAAAAAAAAAAAA= - Application\MainLoopSyncContext.cs + App\MainLoopSyncContext.cs - - + + AAAAAAAAACACAgAAAAAAAAAAAAAAAAACQAAAAAAAAAA= - Application\RunState.cs + App\RunState.cs - - + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAA= - Application\RunStateEventArgs.cs + App\RunStateEventArgs.cs - - + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAQAA= - Application\Timeout.cs + App\Timeout.cs - - + + AAAAAAAAAAAAAAAAAAAAAAAAAAAACAIAAAAAAAAAAAA= - Application\TimeoutEventArgs.cs + App\TimeoutEventArgs.cs - - + + - AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAQAACAACAAAI= - Application\ApplicationImpl.cs + AABgAAAAIAAIAgQUAAAAAQAAAAAAAAAAQAAKgAAAEAI= + App\ApplicationImpl.cs - - + + + + BAAgAAAAgABAAoAAAAAAABAAACEAAAAAAABAAgAAAAA= + App\MouseGrabHandler.cs + + + + + AAAAAAAACAAAAAQAAAAABAAAAAAAEAAAAAAAAAAAAAA= - Application\MainLoop.cs + App\MainLoop.cs + + + + + + AAAgAAAAAAAIAgQUAAAAAQAAAAAAAAAAAAAKgAAAEAI= + App\IApplication.cs + + + + + + + + + + BAAgAAAAAAAAAgAAAAAAABAAACEAAAAAAAAAAgAAAAA= + App\IMouseGrabHandler.cs - - + + - AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAACAAAAAAI= - Application\IApplication.cs + BAAAIAAAAQAAAAAQACAAAIBAAQAAAAAAAAAIgAAAAAA= + App\ITimedEvents.cs diff --git a/Terminal.Gui/App/Application.cs b/Terminal.Gui/App/Application.cs index 3b59b2e13b..7741b12b18 100644 --- a/Terminal.Gui/App/Application.cs +++ b/Terminal.Gui/App/Application.cs @@ -42,6 +42,15 @@ public static partial class Application /// Gets all cultures supported by the application without the invariant language. public static List? SupportedCultures { get; private set; } = GetSupportedCultures (); + + /// + /// + /// Handles recurring events. These are invoked on the main UI thread - allowing for + /// safe updates to instances. + /// + /// + public static ITimedEvents? TimedEvents => ApplicationImpl.Instance?.TimedEvents; + /// /// Gets a string representation of the Application as rendered by . /// @@ -221,7 +230,7 @@ internal static void ResetState (bool ignoreDisposed = false) // Run State stuff NotifyNewRunState = null; NotifyStopRunState = null; - MouseGrabView = null; + MouseGrabHandler = new MouseGrabHandler (); Initialized = false; // Mouse @@ -229,12 +238,7 @@ internal static void ResetState (bool ignoreDisposed = false) // last mouse pos. //_lastMousePosition = null; CachedViewsUnderMouse.Clear (); - WantContinuousButtonPressedView = null; MouseEvent = null; - GrabbedMouse = null; - UnGrabbingMouse = null; - GrabbedMouse = null; - UnGrabbedMouse = null; // Keyboard KeyDown = null; @@ -252,10 +256,4 @@ internal static void ResetState (bool ignoreDisposed = false) // (https://github.com/gui-cs/Terminal.Gui/issues/1084). SynchronizationContext.SetSynchronizationContext (null); } - - /// - /// Adds specified idle handler function to main iteration processing. The handler function will be called - /// once per iteration of the main loop after other events have been handled. - /// - public static void AddIdle (Func func) { ApplicationImpl.Instance.AddIdle (func); } } diff --git a/Terminal.Gui/App/ApplicationImpl.cs b/Terminal.Gui/App/ApplicationImpl.cs index 111b9783dc..4eadc2ad61 100644 --- a/Terminal.Gui/App/ApplicationImpl.cs +++ b/Terminal.Gui/App/ApplicationImpl.cs @@ -18,6 +18,15 @@ public class ApplicationImpl : IApplication /// public static IApplication Instance => _lazyInstance.Value; + + /// + public virtual ITimedEvents? TimedEvents => Application.MainLoop?.TimedEvents; + + /// + /// Handles which (if any) has captured the mouse + /// + public IMouseGrabHandler MouseGrabHandler { get; set; } = new MouseGrabHandler (); + /// /// Change the singleton implementation, should not be called except before application /// startup. This method lets you provide alternative implementations of core static gateway @@ -119,7 +128,7 @@ public virtual T Run (Func? errorHandler = null, IConsoleDri /// Alternatively, to have a program control the main loop and process events manually, call /// to set things up manually and then repeatedly call /// with the wait parameter set to false. By doing this the - /// method will only process any pending events, timers, idle handlers and then + /// method will only process any pending events, timers handlers and then /// return control immediately. /// /// When using or @@ -261,7 +270,22 @@ public virtual void RequestStop (Toplevel? top) /// public virtual void Invoke (Action action) { - Application.MainLoop?.AddIdle ( + + // If we are already on the main UI thread + if (Application.MainThreadId == Thread.CurrentThread.ManagedThreadId) + { + action (); + return; + } + + if (Application.MainLoop == null) + { + Logging.Warning ("Ignored Invoke because MainLoop is not initialized yet"); + return; + } + + + Application.AddTimeout (TimeSpan.Zero, () => { action (); @@ -274,20 +298,6 @@ public virtual void Invoke (Action action) /// public bool IsLegacy { get; protected set; } = true; - /// - public virtual void AddIdle (Func func) - { - if (Application.MainLoop is null) - { - throw new NotInitializedException ("Cannot add idle before main loop is initialized"); - } - - // Yes in this case we cannot go direct via TimedEvents because legacy main loop - // has established behaviour to do other stuff too e.g. 'wake up'. - Application.MainLoop.AddIdle (func); - - } - /// public virtual object AddTimeout (TimeSpan time, Func callback) { @@ -296,13 +306,13 @@ public virtual object AddTimeout (TimeSpan time, Func callback) throw new NotInitializedException ("Cannot add timeout before main loop is initialized", null); } - return Application.MainLoop.TimedEvents.AddTimeout (time, callback); + return Application.MainLoop.TimedEvents.Add (time, callback); } /// public virtual bool RemoveTimeout (object token) { - return Application.MainLoop?.TimedEvents.RemoveTimeout (token) ?? false; + return Application.MainLoop?.TimedEvents.Remove (token) ?? false; } /// diff --git a/Terminal.Gui/App/IApplication.cs b/Terminal.Gui/App/IApplication.cs index d8df8d5528..abc169bc94 100644 --- a/Terminal.Gui/App/IApplication.cs +++ b/Terminal.Gui/App/IApplication.cs @@ -9,6 +9,17 @@ namespace Terminal.Gui.App; /// public interface IApplication { + /// + /// Handles recurring events. These are invoked on the main UI thread - allowing for + /// safe updates to instances. + /// + ITimedEvents? TimedEvents { get; } + + /// + /// Handles grabbing the mouse (only a single can grab the mouse at once). + /// + IMouseGrabHandler MouseGrabHandler { get; set; } + /// Initializes a new instance of Application. /// Call this method once per instance (or after has been called). /// @@ -106,7 +117,7 @@ public T Run (Func? errorHandler = null, IConsoleDriver? dri /// Alternatively, to have a program control the main loop and process events manually, call /// to set things up manually and then repeatedly call /// with the wait parameter set to false. By doing this the - /// method will only process any pending events, timers, idle handlers and then + /// method will only process any pending events, timers handlers and then /// return control immediately. /// /// When using or @@ -156,13 +167,7 @@ public T Run (Func? errorHandler = null, IConsoleDriver? dri /// is cutting edge. /// bool IsLegacy { get; } - - /// - /// Adds specified idle handler function to main iteration processing. The handler function will be called - /// once per iteration of the main loop after other events have been handled. - /// - void AddIdle (Func func); - + /// Adds a timeout to the application. /// /// When time specified passes, the callback will be invoked. If the callback returns true, the timeout will be diff --git a/Terminal.Gui/App/ITimedEvents.cs b/Terminal.Gui/App/ITimedEvents.cs deleted file mode 100644 index 1fd867a8c8..0000000000 --- a/Terminal.Gui/App/ITimedEvents.cs +++ /dev/null @@ -1,90 +0,0 @@ -#nullable enable -using System.Collections.ObjectModel; - -namespace Terminal.Gui.App; - -/// -/// Manages timers and idles -/// -public interface ITimedEvents -{ - /// - /// Adds specified idle handler function to main iteration processing. The handler function will be called - /// once per iteration of the main loop after other events have been handled. - /// - /// - void AddIdle (Func idleHandler); - - /// - /// Runs all idle hooks - /// - void LockAndRunIdles (); - - /// - /// Runs all timeouts that are due - /// - void LockAndRunTimers (); - - /// - /// Called from to check if there are any outstanding timers or idle - /// handlers. - /// - /// - /// Returns the number of milliseconds remaining in the current timer (if any). Will be -1 if - /// there are no active timers. - /// - /// if there is a timer or idle handler active. - bool CheckTimersAndIdleHandlers (out int waitTimeout); - - /// Adds a timeout to the application. - /// - /// When time specified passes, the callback will be invoked. If the callback returns true, the timeout will be - /// reset, repeating the invocation. If it returns false, the timeout will stop and be removed. The returned value is a - /// token that can be used to stop the timeout by calling . - /// - object AddTimeout (TimeSpan time, Func callback); - - /// Removes a previously scheduled timeout - /// The token parameter is the value returned by AddTimeout. - /// - /// Returns - /// - /// if the timeout is successfully removed; otherwise, - /// - /// . - /// This method also returns - /// - /// if the timeout is not found. - /// - bool RemoveTimeout (object token); - - /// - /// Returns all currently registered idles. May not include - /// actively executing idles. - /// - ReadOnlyCollection> IdleHandlers { get;} - - /// - /// Returns the next planned execution time (key - UTC ticks) - /// for each timeout that is not actively executing. - /// - SortedList Timeouts { get; } - - - /// Removes an idle handler added with from processing. - /// - /// - /// if the idle handler is successfully removed; otherwise, - /// - /// . - /// This method also returns - /// - /// if the idle handler is not found. - bool RemoveIdle (Func fnTrue); - - /// - /// Invoked when a new timeout is added. To be used in the case when - /// is . - /// - event EventHandler? TimeoutAdded; -} diff --git a/Terminal.Gui/App/MainLoop.cs b/Terminal.Gui/App/MainLoop.cs index 65f4ed599f..64aadf73cf 100644 --- a/Terminal.Gui/App/MainLoop.cs +++ b/Terminal.Gui/App/MainLoop.cs @@ -32,7 +32,7 @@ internal interface IMainLoopDriver void Wakeup (); } -/// The MainLoop monitors timers and idle handlers. +/// The main event loop of v1 driver based applications. /// /// Monitoring of file descriptors is only available on Unix, there does not seem to be a way of supporting this /// on Windows. @@ -40,7 +40,7 @@ internal interface IMainLoopDriver public class MainLoop : IDisposable { /// - /// Gets the class responsible for handling idles and timeouts + /// Gets the class responsible for handling timeouts /// public ITimedEvents TimedEvents { get; } = new TimedEvents(); @@ -75,32 +75,6 @@ public void Dispose () MainLoopDriver = null; } - /// - /// Adds specified idle handler function to processing. The handler function will be called - /// once per iteration of the main loop after other events have been handled. - /// - /// - /// Remove an idle handler by calling with the token this method returns. - /// - /// If the returns it will be removed and not called - /// subsequently. - /// - /// - /// Token that can be used to remove the idle handler with . - // QUESTION: Why are we re-inventing the event wheel here? - // PERF: This is heavy. - // CONCURRENCY: Race conditions exist here. - // CONCURRENCY: null delegates will hose this. - // - internal Func AddIdle (Func idleHandler) - { - TimedEvents.AddIdle (idleHandler); - - MainLoopDriver?.Wakeup (); - - return idleHandler; - } - /// Determines whether there are pending events to be processed. /// @@ -127,7 +101,7 @@ internal void Run () /// Runs one iteration of timers and file watches /// - /// Use this to process all pending events (timers, idle handlers and file watches). + /// Use this to process all pending events (timers handlers and file watches). /// /// while (main.EventsPending ()) RunIteration (); /// @@ -138,9 +112,7 @@ internal void RunIteration () MainLoopDriver?.Iteration (); - TimedEvents.LockAndRunTimers (); - - TimedEvents.LockAndRunIdles (); + TimedEvents.RunTimers (); } private void RunAnsiScheduler () diff --git a/Terminal.Gui/App/MainLoopSyncContext.cs b/Terminal.Gui/App/MainLoopSyncContext.cs index d2268c5ad9..548722d6e2 100644 --- a/Terminal.Gui/App/MainLoopSyncContext.cs +++ b/Terminal.Gui/App/MainLoopSyncContext.cs @@ -10,14 +10,16 @@ internal sealed class MainLoopSyncContext : SynchronizationContext public override void Post (SendOrPostCallback d, object state) { - Application.MainLoop?.AddIdle ( - () => - { - d (state); + // Queue the task + Application.MainLoop?.TimedEvents.Add (TimeSpan.Zero, + () => + { + d (state); - return false; - } - ); + return false; + } + ); + Application.MainLoop?.Wakeup (); } //_mainLoop.Driver.Wakeup (); diff --git a/Terminal.Gui/App/Mouse/IMouseGrabHandler.cs b/Terminal.Gui/App/Mouse/IMouseGrabHandler.cs new file mode 100644 index 0000000000..026811e15a --- /dev/null +++ b/Terminal.Gui/App/Mouse/IMouseGrabHandler.cs @@ -0,0 +1,87 @@ +#nullable enable +namespace Terminal.Gui.App; + +/// +/// Defines a contract for tracking which (if any) has 'grabbed' the mouse, +/// giving it exclusive priority for mouse events such as movement, button presses, and release. +/// +/// This is typically used for scenarios like dragging, scrolling, or any interaction where a view +/// needs to receive all mouse events until the operation completes (e.g., a scrollbar thumb being dragged). +/// +/// +/// Usage pattern: +/// +/// +/// Call to route all mouse events to a specific view. +/// +/// +/// Call to release the grab and restore normal mouse routing. +/// +/// +/// +/// Listen to , , , +/// and for grab lifecycle events. +/// +/// +/// +/// +/// +public interface IMouseGrabHandler +{ + /// + /// Occurs after a view has grabbed the mouse. + /// + /// This event is raised after the mouse grab operation is complete and the specified view will receive all mouse + /// events. + /// + /// + public event EventHandler? GrabbedMouse; + + /// + /// Occurs when a view requests to grab the mouse; can be canceled. + /// + /// Handlers can set e.Cancel to to prevent the grab. + /// + /// + public event EventHandler? GrabbingMouse; + + /// + /// Grabs the mouse, forcing all mouse events to be routed to the specified view until is + /// called. + /// + /// + /// The that will receive all mouse events until is invoked. + /// If , the grab is released. + /// + public void GrabMouse (View? view); + + /// + /// Gets the view that currently has grabbed the mouse (e.g., for dragging). + /// + /// When this property is not , all mouse events are routed to this view until + /// is called or the mouse is released. + /// + /// + public View? MouseGrabView { get; } + + /// + /// Occurs after a view has released the mouse grab. + /// + /// This event is raised after the mouse grab has been released and normal mouse routing resumes. + /// + /// + public event EventHandler? UnGrabbedMouse; + + /// + /// Occurs when a view requests to release the mouse grab; can be canceled. + /// + /// Handlers can set e.Cancel to to prevent the ungrab. + /// + /// + public event EventHandler? UnGrabbingMouse; + + /// + /// Releases the mouse grab, so mouse events will be routed to the view under the mouse pointer. + /// + public void UngrabMouse (); +} diff --git a/Terminal.Gui/App/Mouse/MouseGrabHandler.cs b/Terminal.Gui/App/Mouse/MouseGrabHandler.cs new file mode 100644 index 0000000000..65274d8150 --- /dev/null +++ b/Terminal.Gui/App/Mouse/MouseGrabHandler.cs @@ -0,0 +1,118 @@ +#nullable enable +namespace Terminal.Gui.App; + +/// +/// INTERNAL: Implements to manage which (if any) has 'grabbed' the mouse, +/// giving it exclusive priority for mouse events such as movement, button presses, and release. +/// +/// Used for scenarios like dragging, scrolling, or any interaction where a view needs to receive all mouse events +/// until the operation completes (e.g., a scrollbar thumb being dragged). +/// +/// +/// See for usage details. +/// +/// +internal class MouseGrabHandler : IMouseGrabHandler +{ + /// + public View? MouseGrabView { get; private set; } + + /// + public event EventHandler? GrabbingMouse; + + /// + public event EventHandler? UnGrabbingMouse; + + /// + public event EventHandler? GrabbedMouse; + + /// + public event EventHandler? UnGrabbedMouse; + + /// + public void GrabMouse (View? view) + { + if (view is null || RaiseGrabbingMouseEvent (view)) + { + return; + } + + RaiseGrabbedMouseEvent (view); + + // MouseGrabView is a static; only set if the application is initialized. + MouseGrabView = view; + } + + /// + public 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); + } + } + + /// A delegate callback throws an exception. + private bool RaiseGrabbingMouseEvent (View? view) + { + if (view is null) + { + return false; + } + + var evArgs = new GrabMouseEventArgs (view); + GrabbingMouse?.Invoke (view, evArgs); + + return evArgs.Cancel; + } + + /// A delegate callback throws an exception. + private bool RaiseUnGrabbingMouseEvent (View? view) + { + if (view is null) + { + return false; + } + + var evArgs = new GrabMouseEventArgs (view); + UnGrabbingMouse?.Invoke (view, evArgs); + + return evArgs.Cancel; + } + + /// A delegate callback throws an exception. + private void RaiseGrabbedMouseEvent (View? view) + { + if (view is null) + { + return; + } + + GrabbedMouse?.Invoke (view, new (view)); + } + + /// A delegate callback throws an exception. + private void RaiseUnGrabbedMouseEvent (View? view) + { + if (view is null) + { + return; + } + + UnGrabbedMouse?.Invoke (view, new (view)); + } +} diff --git a/Terminal.Gui/App/Timeout.cs b/Terminal.Gui/App/Timeout.cs deleted file mode 100644 index 615ca2d9f6..0000000000 --- a/Terminal.Gui/App/Timeout.cs +++ /dev/null @@ -1,18 +0,0 @@ -// -// MainLoop.cs: IMainLoopDriver and MainLoop for Terminal.Gui -// -// Authors: -// Miguel de Icaza (miguel@gnome.org) -// - -namespace Terminal.Gui.App; - -/// Provides data for timers running manipulation. -public sealed class Timeout -{ - /// The function that will be invoked. - public Func Callback; - - /// Time to wait before invoke the callback. - public TimeSpan Span; -} diff --git a/Terminal.Gui/App/Timeout/ITimedEvents.cs b/Terminal.Gui/App/Timeout/ITimedEvents.cs new file mode 100644 index 0000000000..501da77bc2 --- /dev/null +++ b/Terminal.Gui/App/Timeout/ITimedEvents.cs @@ -0,0 +1,64 @@ +#nullable enable +namespace Terminal.Gui.App; + +/// +/// Manages timers. +/// +public interface ITimedEvents +{ + /// + /// Adds a timeout to the application. + /// + /// + /// When the specified time passes, the callback will be invoked. If the callback returns , the + /// timeout will be + /// reset, repeating the invocation. If it returns , the timeout will stop and be removed. The + /// returned value is a + /// token that can be used to stop the timeout by calling . + /// + object Add (TimeSpan time, Func callback); + + /// + object Add (Timeout timeout); + + /// + /// Invoked when a new timeout is added. To be used in the case when + /// is . + /// + event EventHandler? Added; + + /// + /// Called from to check if there are any outstanding timer handlers. + /// + /// + /// Returns the number of milliseconds remaining in the current timer (if any). Will be -1 if + /// there are no active timers. + /// + /// + /// if there is a timer active; otherwise, . + /// + bool CheckTimers (out int waitTimeout); + + /// + /// Removes a previously scheduled timeout. + /// + /// + /// The token parameter is the value returned by or . + /// + /// + /// if the timeout is successfully removed; otherwise, . + /// This method also returns if the timeout is not found. + /// + bool Remove (object token); + + /// + /// Runs all timeouts that are due. + /// + void RunTimers (); + + /// + /// Returns the next planned execution time (key - UTC ticks) + /// for each timeout that is not actively executing. + /// + SortedList Timeouts { get; } +} diff --git a/Terminal.Gui/App/Timeout/LogarithmicTimeout.cs b/Terminal.Gui/App/Timeout/LogarithmicTimeout.cs new file mode 100644 index 0000000000..eacf09607e --- /dev/null +++ b/Terminal.Gui/App/Timeout/LogarithmicTimeout.cs @@ -0,0 +1,38 @@ +namespace Terminal.Gui.App; + +/// Implements a logarithmic increasing timeout. +public class LogarithmicTimeout : Timeout +{ + /// + /// Creates a new instance where stages are the logarithm multiplied by the + /// (starts fast then slows). + /// + /// Multiple for the logarithm + /// Method to invoke + public LogarithmicTimeout (TimeSpan baseDelay, Func callback) + { + _baseDelay = baseDelay; + Callback = callback; + } + + private readonly TimeSpan _baseDelay; + private int _stage; + + /// Increments the stage to increase the timeout. + public void AdvanceStage () { _stage++; } + + /// Resets the stage back to zero. + public void Reset () { _stage = 0; } + + /// Gets the current calculated Span based on the stage. + public override TimeSpan Span + { + get + { + // Calculate logarithmic increase + double multiplier = Math.Log (_stage + 1); // ln(stage + 1) + + return TimeSpan.FromMilliseconds (_baseDelay.TotalMilliseconds * multiplier); + } + } +} diff --git a/Terminal.Gui/App/Timeout/SmoothAcceleratingTimeout.cs b/Terminal.Gui/App/Timeout/SmoothAcceleratingTimeout.cs new file mode 100644 index 0000000000..962ce4b19f --- /dev/null +++ b/Terminal.Gui/App/Timeout/SmoothAcceleratingTimeout.cs @@ -0,0 +1,53 @@ +namespace Terminal.Gui.App; + +/// +/// Timeout which accelerates slowly at first then fast up to a maximum speed. +/// Use to increment the stage of the timer (e.g. in +/// your timer callback code). +/// +public class SmoothAcceleratingTimeout : Timeout +{ + /// + /// Creates a new instance of the smooth acceleration timeout. + /// + /// Delay before first tick, the longest it will ever take + /// The fastest the timer can get no matter how long it runs + /// Controls how fast the timer accelerates + /// Method to call when timer ticks + public SmoothAcceleratingTimeout (TimeSpan initialDelay, TimeSpan minDelay, double decayFactor, Func callback) + { + _initialDelay = initialDelay; + _minDelay = minDelay; + _decayFactor = decayFactor; + Callback = callback; + } + + private readonly TimeSpan _initialDelay; + private readonly TimeSpan _minDelay; + private readonly double _decayFactor; + private int _stage; + + /// + /// Advances the timer stage, this should be called from your timer callback or whenever + /// you want to advance the speed. + /// + public void AdvanceStage () { _stage++; } + + /// + /// Resets the timer to original speed. + /// + public void Reset () { _stage = 0; } + + /// + public override TimeSpan Span + { + get + { + double initialMs = _initialDelay.TotalMilliseconds; + double minMs = _minDelay.TotalMilliseconds; + double delayMs = minMs + (initialMs - minMs) * Math.Pow (_decayFactor, _stage); + + return TimeSpan.FromMilliseconds (delayMs); + } + } +} diff --git a/Terminal.Gui/App/TimedEvents.cs b/Terminal.Gui/App/Timeout/TimedEvents.cs similarity index 53% rename from Terminal.Gui/App/TimedEvents.cs rename to Terminal.Gui/App/Timeout/TimedEvents.cs index 13553a43b2..b77e0520d3 100644 --- a/Terminal.Gui/App/TimedEvents.cs +++ b/Terminal.Gui/App/Timeout/TimedEvents.cs @@ -1,137 +1,163 @@ #nullable enable -using System.Collections.ObjectModel; - namespace Terminal.Gui.App; /// -/// Handles timeouts and idles +/// Manages scheduled timeouts (timed callbacks) for the application. +/// +/// Allows scheduling of callbacks to be invoked after a specified delay, with optional repetition. +/// Timeouts are stored in a sorted list by their scheduled execution time (UTC ticks). +/// Thread-safe for concurrent access. +/// +/// +/// Typical usage: +/// +/// +/// Call to schedule a callback. +/// +/// +/// +/// Call periodically (e.g., from the main loop) to execute due +/// callbacks. +/// +/// +/// +/// Call to cancel a scheduled timeout. +/// +/// +/// /// public class TimedEvents : ITimedEvents { - internal List> _idleHandlers = new (); internal SortedList _timeouts = new (); - - /// The idle handlers and lock that must be held while manipulating them - private readonly object _idleHandlersLock = new (); - private readonly object _timeoutsLockToken = new (); - - /// Gets a copy of the list of all idle handlers. - public ReadOnlyCollection> IdleHandlers - { - get - { - lock (_idleHandlersLock) - { - return new List> (_idleHandlers).AsReadOnly (); - } - } - } - /// /// Gets the list of all timeouts sorted by the time ticks. A shorter limit time can be /// added at the end, but it will be called before an earlier addition that has a longer limit time. /// public SortedList Timeouts => _timeouts; - /// - public void AddIdle (Func idleHandler) - { - lock (_idleHandlersLock) - { - _idleHandlers.Add (idleHandler); - } - } - /// - public event EventHandler? TimeoutAdded; - + public event EventHandler? Added; - private void AddTimeout (TimeSpan time, Timeout timeout) + /// + public void RunTimers () { lock (_timeoutsLockToken) { - long k = (DateTime.UtcNow + time).Ticks; - _timeouts.Add (NudgeToUniqueKey (k), timeout); - TimeoutAdded?.Invoke (this, new TimeoutEventArgs (timeout, k)); + if (_timeouts.Count > 0) + { + RunTimersImpl (); + } } } - /// - /// Finds the closest number to that is not present in - /// (incrementally). - /// - /// - /// - private long NudgeToUniqueKey (long k) + /// + public bool Remove (object token) { lock (_timeoutsLockToken) { - while (_timeouts.ContainsKey (k)) + int idx = _timeouts.IndexOfValue ((token as Timeout)!); + + if (idx == -1) { - k++; + return false; } + + _timeouts.RemoveAt (idx); } - return k; + return true; } + /// + public object Add (TimeSpan time, Func callback) + { + ArgumentNullException.ThrowIfNull (callback); + + var timeout = new Timeout { Span = time, Callback = callback }; + AddTimeout (time, timeout); - // PERF: This is heavier than it looks. - // CONCURRENCY: Potential deadlock city here. - // CONCURRENCY: Multiple concurrency pitfalls on the delegates themselves. - // INTENT: It looks like the general architecture here is trying to be a form of publisher/consumer pattern. - private void RunIdle () + return timeout; + } + + /// + public object Add (Timeout timeout) { - Func [] iterate; - lock (_idleHandlersLock) - { - iterate = _idleHandlers.ToArray (); - _idleHandlers = new List> (); - } + AddTimeout (timeout.Span, timeout); - foreach (Func idle in iterate) + return timeout; + } + + /// + public bool CheckTimers (out int waitTimeout) + { + long now = DateTime.UtcNow.Ticks; + + waitTimeout = 0; + + lock (_timeoutsLockToken) { - if (idle ()) + if (_timeouts.Count > 0) { - lock (_idleHandlersLock) + waitTimeout = (int)((_timeouts.Keys [0] - now) / TimeSpan.TicksPerMillisecond); + + if (waitTimeout < 0) { - _idleHandlers.Add (idle); + // This avoids 'poll' waiting infinitely if 'waitTimeout < 0' until some action is detected + // This can occur after IMainLoopDriver.Wakeup is executed where the pollTimeout is less than 0 + // and no event occurred in elapsed time when the 'poll' is start running again. + waitTimeout = 0; } + + return true; } + + // ManualResetEventSlim.Wait, which is called by IMainLoopDriver.EventsPending, will wait indefinitely if + // the timeout is -1. + waitTimeout = -1; } + + return false; } - /// - public void LockAndRunTimers () + private void AddTimeout (TimeSpan time, Timeout timeout) { lock (_timeoutsLockToken) { - if (_timeouts.Count > 0) + long k = (DateTime.UtcNow + time).Ticks; + + // if user wants to run as soon as possible set timer such that it expires right away (no race conditions) + if (time == TimeSpan.Zero) { - RunTimers (); + k -= 100; } - } + _timeouts.Add (NudgeToUniqueKey (k), timeout); + Added?.Invoke (this, new (timeout, k)); + } } - /// - public void LockAndRunIdles () + /// + /// Finds the closest number to that is not present in + /// (incrementally). + /// + /// + /// + private long NudgeToUniqueKey (long k) { - bool runIdle; - - lock (_idleHandlersLock) + lock (_timeoutsLockToken) { - runIdle = _idleHandlers.Count > 0; + while (_timeouts.ContainsKey (k)) + { + k++; + } } - if (runIdle) - { - RunIdle (); - } + return k; } - private void RunTimers () + + private void RunTimersImpl () { long now = DateTime.UtcNow.Ticks; SortedList copy; @@ -143,7 +169,7 @@ private void RunTimers () lock (_timeoutsLockToken) { copy = _timeouts; - _timeouts = new SortedList (); + _timeouts = new (); } foreach ((long k, Timeout timeout) in copy) @@ -164,94 +190,4 @@ private void RunTimers () } } } - - /// - public bool RemoveIdle (Func token) - { - lock (_idleHandlersLock) - { - return _idleHandlers.Remove (token); - } - } - - /// Removes a previously scheduled timeout - /// The token parameter is the value returned by AddTimeout. - /// Returns - /// - /// if the timeout is successfully removed; otherwise, - /// - /// . - /// This method also returns - /// - /// if the timeout is not found. - public bool RemoveTimeout (object token) - { - lock (_timeoutsLockToken) - { - int idx = _timeouts.IndexOfValue ((token as Timeout)!); - - if (idx == -1) - { - return false; - } - - _timeouts.RemoveAt (idx); - } - - return true; - } - - - /// Adds a timeout to the . - /// - /// When time specified passes, the callback will be invoked. If the callback returns true, the timeout will be - /// reset, repeating the invocation. If it returns false, the timeout will stop and be removed. The returned value is a - /// token that can be used to stop the timeout by calling . - /// - public object AddTimeout (TimeSpan time, Func callback) - { - ArgumentNullException.ThrowIfNull (callback); - - var timeout = new Timeout { Span = time, Callback = callback }; - AddTimeout (time, timeout); - - return timeout; - } - - /// - public bool CheckTimersAndIdleHandlers (out int waitTimeout) - { - long now = DateTime.UtcNow.Ticks; - - waitTimeout = 0; - - lock (_timeoutsLockToken) - { - if (_timeouts.Count > 0) - { - waitTimeout = (int)((_timeouts.Keys [0] - now) / TimeSpan.TicksPerMillisecond); - - if (waitTimeout < 0) - { - // This avoids 'poll' waiting infinitely if 'waitTimeout < 0' until some action is detected - // This can occur after IMainLoopDriver.Wakeup is executed where the pollTimeout is less than 0 - // and no event occurred in elapsed time when the 'poll' is start running again. - waitTimeout = 0; - } - - return true; - } - - // ManualResetEventSlim.Wait, which is called by IMainLoopDriver.EventsPending, will wait indefinitely if - // the timeout is -1. - waitTimeout = -1; - } - - // There are no timers set, check if there are any idle handlers - - lock (_idleHandlersLock) - { - return _idleHandlers.Count > 0; - } - } -} \ No newline at end of file +} diff --git a/Terminal.Gui/App/Timeout/Timeout.cs b/Terminal.Gui/App/Timeout/Timeout.cs new file mode 100644 index 0000000000..c3054869f8 --- /dev/null +++ b/Terminal.Gui/App/Timeout/Timeout.cs @@ -0,0 +1,33 @@ +namespace Terminal.Gui.App; + +/// +/// Represents a scheduled timeout for use with timer management APIs. +/// +/// Encapsulates a callback function to be invoked after a specified time interval. The callback can optionally +/// indicate whether the timeout should repeat. +/// +/// +/// Used by and related timer systems to manage timed operations in the application. +/// +/// +public class Timeout +{ + /// + /// Gets or sets the function to invoke when the timeout expires. + /// + /// + /// A delegate. If the callback returns , the timeout will be + /// rescheduled and invoked again after the same interval. + /// If the callback returns , the timeout will be removed and not invoked again. + /// + public Func Callback { get; set; } + + /// + /// Gets or sets the time interval to wait before invoking the . + /// + /// + /// A representing the delay before the callback is invoked. If the timeout is rescheduled + /// (i.e., returns ), this interval is used again. + /// + public virtual TimeSpan Span { get; set; } +} diff --git a/Terminal.Gui/App/TimeoutEventArgs.cs b/Terminal.Gui/App/Timeout/TimeoutEventArgs.cs similarity index 93% rename from Terminal.Gui/App/TimeoutEventArgs.cs rename to Terminal.Gui/App/Timeout/TimeoutEventArgs.cs index b77741db43..9ad014fa43 100644 --- a/Terminal.Gui/App/TimeoutEventArgs.cs +++ b/Terminal.Gui/App/Timeout/TimeoutEventArgs.cs @@ -1,6 +1,6 @@ namespace Terminal.Gui.App; -/// for timeout events (e.g. ) +/// for timeout events (e.g. ) public class TimeoutEventArgs : EventArgs { /// Creates a new instance of the class. diff --git a/Terminal.Gui/Drivers/CursesDriver/UnixMainLoop.cs b/Terminal.Gui/Drivers/CursesDriver/UnixMainLoop.cs index c3486e7027..50053f4bb0 100644 --- a/Terminal.Gui/Drivers/CursesDriver/UnixMainLoop.cs +++ b/Terminal.Gui/Drivers/CursesDriver/UnixMainLoop.cs @@ -104,7 +104,7 @@ bool IMainLoopDriver.EventsPending () UpdatePollMap (); - bool checkTimersResult = _mainLoop!.TimedEvents.CheckTimersAndIdleHandlers (out int pollTimeout); + bool checkTimersResult = _mainLoop!.TimedEvents.CheckTimers (out int pollTimeout); int n = poll (_pollMap!, (uint)_pollMap!.Length, pollTimeout); diff --git a/Terminal.Gui/Drivers/EscSeqUtils/EscSeqUtils.cs b/Terminal.Gui/Drivers/EscSeqUtils/EscSeqUtils.cs index b035a2335d..0a99fa8b5d 100644 --- a/Terminal.Gui/Drivers/EscSeqUtils/EscSeqUtils.cs +++ b/Terminal.Gui/Drivers/EscSeqUtils/EscSeqUtils.cs @@ -900,22 +900,8 @@ Action continuousButtonPressedHandler _point = pos; - if ((mouseFlags [0] & MouseFlags.ReportMousePosition) == 0) - { - Application.MainLoop?.AddIdle ( - () => - { - // INTENT: What's this trying to do? - // The task itself is not awaited. - Task.Run ( - async () => await ProcessContinuousButtonPressedAsync ( - buttonState, - continuousButtonPressedHandler)); - return false; - }); - } - else if (mouseFlags [0].HasFlag (MouseFlags.ReportMousePosition)) + if (mouseFlags [0].HasFlag (MouseFlags.ReportMousePosition)) { _point = pos; @@ -945,7 +931,7 @@ Action continuousButtonPressedHandler _isButtonClicked = false; _isButtonDoubleClicked = true; - Application.MainLoop?.AddIdle ( + Application.MainLoop?.TimedEvents.Add (TimeSpan.Zero, () => { Task.Run (async () => await ProcessButtonDoubleClickedAsync ()); @@ -959,7 +945,7 @@ Action continuousButtonPressedHandler // lastMouseButtonReleased = null; // isButtonReleased = false; // isButtonClicked = true; - // Application.MainLoop.AddIdle (() => { + // Application.MainLoop.AddTimeout (() => { // Task.Run (async () => await ProcessButtonClickedAsync ()); // return false; // }); @@ -984,7 +970,7 @@ Action continuousButtonPressedHandler mouseFlags.Add (GetButtonClicked (buttonState)); _isButtonClicked = true; - Application.MainLoop?.AddIdle ( + Application.MainLoop?.TimedEvents.Add (TimeSpan.Zero, () => { Task.Run (async () => await ProcessButtonClickedAsync ()); @@ -1498,29 +1484,6 @@ private static async Task ProcessButtonDoubleClickedAsync () _isButtonDoubleClicked = false; } - private static async Task ProcessContinuousButtonPressedAsync (MouseFlags mouseFlag, Action continuousButtonPressedHandler) - { - // PERF: Pause and poll in a hot loop. - // This should be replaced with event dispatch and a synchronization primitive such as AutoResetEvent. - // Will make a massive difference in responsiveness. - while (_isButtonPressed) - { - await Task.Delay (100); - - View view = Application.WantContinuousButtonPressedView; - - if (view is null) - { - break; - } - - if (_isButtonPressed && _lastMouseButtonPressed is { } && (mouseFlag & MouseFlags.ReportMousePosition) == 0) - { - Application.Invoke (() => continuousButtonPressedHandler (mouseFlag, _point ?? Point.Empty)); - } - } - } - private static MouseFlags SetControlKeyStates (MouseFlags buttonState, MouseFlags mouseFlag) { if ((buttonState & MouseFlags.ButtonCtrl) != 0 && (mouseFlag & MouseFlags.ButtonCtrl) == 0) diff --git a/Terminal.Gui/Drivers/NetDriver/NetMainLoop.cs b/Terminal.Gui/Drivers/NetDriver/NetMainLoop.cs index b33964be42..96aae80390 100644 --- a/Terminal.Gui/Drivers/NetDriver/NetMainLoop.cs +++ b/Terminal.Gui/Drivers/NetDriver/NetMainLoop.cs @@ -57,7 +57,7 @@ bool IMainLoopDriver.EventsPending () _waitForProbe.Set (); - if (_resultQueue.Count > 0 || _mainLoop!.TimedEvents.CheckTimersAndIdleHandlers (out int waitTimeout)) + if (_resultQueue.Count > 0 || _mainLoop!.TimedEvents.CheckTimers (out int waitTimeout)) { return true; } @@ -84,7 +84,7 @@ bool IMainLoopDriver.EventsPending () if (!_eventReadyTokenSource.IsCancellationRequested) { - return _resultQueue.Count > 0 || _mainLoop.TimedEvents.CheckTimersAndIdleHandlers (out _); + return _resultQueue.Count > 0 || _mainLoop.TimedEvents.CheckTimers (out _); } // If cancellation was requested then always return true diff --git a/Terminal.Gui/Drivers/V2/ApplicationV2.cs b/Terminal.Gui/Drivers/V2/ApplicationV2.cs index e10a621b78..ca94ebe575 100644 --- a/Terminal.Gui/Drivers/V2/ApplicationV2.cs +++ b/Terminal.Gui/Drivers/V2/ApplicationV2.cs @@ -21,6 +21,9 @@ public class ApplicationV2 : ApplicationImpl private readonly ITimedEvents _timedEvents = new TimedEvents (); + /// + public override ITimedEvents TimedEvents => _timedEvents; + /// /// Creates anew instance of the Application backend. The provided /// factory methods will be used on Init calls to get things booted. @@ -225,7 +228,14 @@ public override void RequestStop (Toplevel? top) /// public override void Invoke (Action action) { - _timedEvents.AddIdle ( + // If we are already on the main UI thread + if (Application.MainThreadId == Thread.CurrentThread.ManagedThreadId) + { + action (); + return; + } + + _timedEvents.Add (TimeSpan.Zero, () => { action (); @@ -236,20 +246,10 @@ public override void Invoke (Action action) } /// - public override void AddIdle (Func func) { _timedEvents.AddIdle (func); } - - /// - /// Removes an idle function added by - /// - /// Function to remove - /// True if it was found and removed - public bool RemoveIdle (Func fnTrue) { return _timedEvents.RemoveIdle (fnTrue); } - - /// - public override object AddTimeout (TimeSpan time, Func callback) { return _timedEvents.AddTimeout (time, callback); } + public override object AddTimeout (TimeSpan time, Func callback) { return _timedEvents.Add (time, callback); } /// - public override bool RemoveTimeout (object token) { return _timedEvents.RemoveTimeout (token); } + public override bool RemoveTimeout (object token) { return _timedEvents.Remove (token); } /// public override void LayoutAndDraw (bool forceDraw) diff --git a/Terminal.Gui/Drivers/V2/IMainLoop.cs b/Terminal.Gui/Drivers/V2/IMainLoop.cs index 2638e4074b..647776cbe3 100644 --- a/Terminal.Gui/Drivers/V2/IMainLoop.cs +++ b/Terminal.Gui/Drivers/V2/IMainLoop.cs @@ -6,11 +6,11 @@ namespace Terminal.Gui.Drivers; /// /// Interface for main loop that runs the core Terminal.Gui UI loop. /// -/// +/// Type of raw input events processed by the loop e.g. public interface IMainLoop : IDisposable { /// - /// Gets the class responsible for servicing user timeouts and idles + /// Gets the class responsible for servicing user timeouts /// public ITimedEvents TimedEvents { get; } diff --git a/Terminal.Gui/Drivers/V2/MainLoop.cs b/Terminal.Gui/Drivers/V2/MainLoop.cs index e40dfc66b6..5b6d9fdde7 100644 --- a/Terminal.Gui/Drivers/V2/MainLoop.cs +++ b/Terminal.Gui/Drivers/V2/MainLoop.cs @@ -143,9 +143,7 @@ internal void IterationImpl () var swCallbacks = Stopwatch.StartNew (); - TimedEvents.LockAndRunTimers (); - - TimedEvents.LockAndRunIdles (); + TimedEvents.RunTimers (); Logging.IterationInvokesAndTimeouts.Record (swCallbacks.Elapsed.Milliseconds); } diff --git a/Terminal.Gui/Drivers/V2/V2.cd b/Terminal.Gui/Drivers/V2/V2.cd index f5004db3ce..440e918849 100644 --- a/Terminal.Gui/Drivers/V2/V2.cd +++ b/Terminal.Gui/Drivers/V2/V2.cd @@ -6,7 +6,7 @@ - + @@ -18,7 +18,7 @@ - + @@ -27,44 +27,33 @@ - + QIAACAAAACAEAAAAAAAAAAAkAAAAAAAAAwAAAAAAABA= - ConsoleDrivers\V2\WindowsInput.cs + Drivers\V2\WindowsInput.cs - + AAAAAAAAACAEAAAAQAAAAAAgAAAAAAAAAAAAAAAAAAA= - ConsoleDrivers\V2\NetInput.cs + Drivers\V2\NetInput.cs - + AAAAAAAAACAEAQAAAAAAAAAgACAAAAAAAAAAAAAAAAo= - ConsoleDrivers\V2\ConsoleInput.cs + Drivers\V2\ConsoleInput.cs - + - - - - - - - - - - - - + @@ -73,7 +62,7 @@ - + @@ -82,7 +71,7 @@ - + @@ -93,7 +82,7 @@ - + @@ -105,7 +94,7 @@ - + @@ -119,12 +108,11 @@ - QQQAAAAQACABJQQAABAAAQAAACAAAAACAAEAAACAEgg= - ConsoleDrivers\V2\MainLoop.cs + QQQAAAAQACABJQQAABAAAQAAACAAAAACAIEAAAAAEgg= + Drivers\V2\MainLoop.cs - @@ -133,25 +121,25 @@ - + IAAAIAEiCAIABAAAABQAAAAAABAAAQQAIQIABAAACgg= - ConsoleDrivers\V2\MainLoopCoordinator.cs + Drivers\V2\MainLoopCoordinator.cs - + AAQAAAAAAAAACIAAAAAAAAAAAAAgAABAAAAACBAAAAA= - ConsoleDrivers\AnsiResponseParser\AnsiResponseParser.cs + Drivers\AnsiResponseParser\AnsiResponseParser.cs - + @@ -159,29 +147,29 @@ AwAAAAAAAIAAAECIBgAEQIAAAAEMRgAACAAAKABAgAA= - ConsoleDrivers\V2\OutputBuffer.cs + Drivers\V2\OutputBuffer.cs - + - AEAAAAAAACAAAAAAAAAAAAAAAAAAQAAAMACAAAEAgAk= - ConsoleDrivers\V2\NetOutput.cs + AEAAAAAAACAAAAAAAAAQAAAAAAAAQAAAMACAAAEAgAk= + Drivers\V2\NetOutput.cs - + - AEAAABACACAAhAAAAAAAACCAAAgAQAAIMAAAAAEAgAQ= - ConsoleDrivers\V2\WindowsOutput.cs + AEAAABACACAAhAAAAAAQACCAAAgAYAAIMAAAAAEAgAQ= + Drivers\V2\WindowsOutput.cs - + - + @@ -192,7 +180,7 @@ AQAkEAAAAASAiAAEAgwgAAAABAIAAAAAAAAAAAAEAAA= - ConsoleDrivers\V2\InputProcessor.cs + Drivers\V2\InputProcessor.cs @@ -201,28 +189,28 @@ - + AAAAAAAAAAAACBAAAgAAAEAAAAAAAAAAAAAAAAAAAAA= - ConsoleDrivers\V2\NetInputProcessor.cs + Drivers\V2\NetInputProcessor.cs - + AQAAAAAAAAAACAAAAgAAAAAAAgAEAAAAAAAAAAAAAAA= - ConsoleDrivers\V2\WindowsInputProcessor.cs + Drivers\V2\WindowsInputProcessor.cs - + BAAAAAAAAAgAAAAAAAAAAAAAIAAAAAAAQAAAAAAAAAA= - ConsoleDrivers\AnsiResponseParser\AnsiMouseParser.cs + Drivers\AnsiResponseParser\AnsiMouseParser.cs - + @@ -230,41 +218,41 @@ AQcgAAAAAKBAgFEIBBgAQJEAAjkaQiIAGQADKABDgAQ= - ConsoleDrivers\V2\ConsoleDriverFacade.cs + Drivers\V2\ConsoleDriverFacade.cs - + AAQAACAAIAAAIAACAESQAAQAACGAAAAAAAAAAAAAQQA= - ConsoleDrivers\AnsiResponseParser\AnsiRequestScheduler.cs + Drivers\AnsiResponseParser\AnsiRequestScheduler.cs - + - + - + - UAiASAAAEICQALAAQAAAKAAAoAIAAABAAQIAJiAQASQ= - ConsoleDrivers\AnsiResponseParser\AnsiResponseParser.cs + UAiASAAAEICQALCAQAAAKAAAoAIAAABAAQIAJiAQASQ= + Drivers\AnsiResponseParser\AnsiResponseParser.cs @@ -273,296 +261,293 @@ - + AAAABAAAAAAAAAAAAgAAAAAAACAAAAAAAAUAAAAIAAA= - ConsoleDrivers\V2\MouseInterpreter.cs + Drivers\V2\MouseInterpreter.cs - + AAAAAAAAAMwAIAAAAAAAAAAAABCAAAAAAAAABAAEAAg= - ConsoleDrivers\V2\MouseButtonStateEx.cs + Drivers\V2\MouseButtonStateEx.cs - + AAAAAAAAAAIAACAAAAAAAIBAAAAAAACAAAAAAAgAAAA= - ConsoleDrivers\AnsiResponseParser\StringHeld.cs + Drivers\AnsiResponseParser\StringHeld.cs - + AAAAAAAAgAIAACAAAAAAAIBAAAAAAACAAAAAAAAAAAA= - ConsoleDrivers\AnsiResponseParser\GenericHeld.cs + Drivers\AnsiResponseParser\GenericHeld.cs - + AAAAAAAAAEAAAAAAAEAAAAACAAAAAAAAAAAAAAAAAAA= - ConsoleDrivers\AnsiEscapeSequenceRequest.cs + Drivers\AnsiEscapeSequenceRequest.cs - + AAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAgAAEAAAA= - ConsoleDrivers\AnsiEscapeSequence.cs + Drivers\AnsiEscapeSequence.cs - + AAAAAAAAAAAAAAAAAAAAAAAAAAAgACBAAAAACBAAAAA= - ConsoleDrivers\AnsiResponseParser\AnsiResponseParser.cs - - - - - - hEK4FAgAqARIspQeBwoUgTGgACNL0AIAESLKoggBSw8= - Application\Application.cs + Drivers\AnsiResponseParser\AnsiResponseParser.cs - - - - AABAAAAAIAAIAgQQAAAAAQAAAAAAAAAAQAAKgAAAAAI= - Application\ApplicationImpl.cs - - - - - - - + - QAAAAAgABAEIBgAQAAAAAQBAAAAAgAEAAAAKgIAAAgI= - ConsoleDrivers\V2\ApplicationV2.cs + QAAgAAgABAEIBgAQAAAAAQAAAAAAgAEAAAAKAIAAEgI= + Drivers\V2\ApplicationV2.cs - - - - 3/v2dzPLvbb/5+LOHuv1x0dem3Y57v/8c6afz2/e/Y8= - View\View.Adornments.cs - - - - + AAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= - ConsoleDrivers\V2\WindowsKeyConverter.cs + Drivers\V2\WindowsKeyConverter.cs - + AAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= - ConsoleDrivers\V2\NetKeyConverter.cs + Drivers\V2\NetKeyConverter.cs - + AAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAE= - ConsoleDrivers\AnsiResponseParser\Keyboard\AnsiKeyboardParser.cs + Drivers\AnsiResponseParser\Keyboard\AnsiKeyboardParser.cs - + AIAAAAAAAAAAAAEAAAAAAAAAAEIAAAAAAAAAAAAAAAA= - ConsoleDrivers\V2\ToplevelTransitionManager.cs + Drivers\V2\ToplevelTransitionManager.cs - - - - AAAAAAAAAAIgAAAAAAEQAAAAAAAAABAAgAAAAAAAEAA= - ConsoleDrivers\V2\Logging.cs - - - + AAAAgAAAAAAAAAAEAAAAABAAAAAACAAAAAAAAAAAACA= - ConsoleDrivers\V2\WindowSizeMonitor.cs + Drivers\V2\WindowSizeMonitor.cs - + AAACIAAAAAAAAAAAAAAAAAQQAAAAAAAAAAAAAAAACAA= - ConsoleDrivers\AnsiResponseParser\Keyboard\AnsiKeyboardParserPattern.cs + Drivers\AnsiResponseParser\Keyboard\AnsiKeyboardParserPattern.cs - + - AAACAAAAAAAAABAAAAAAAAAQAACAAAAAAAAAAAAAAAA= - ConsoleDrivers\AnsiResponseParser\Keyboard\CsiKeyPattern.cs + AAACQAAAAAAAAAAAAAAAAAAQAACAAAAAAAAAAAAAAAA= + Drivers\AnsiResponseParser\Keyboard\CsiKeyPattern.cs - + AAACAAAAAAAAAAAAAAAAAAAQAACAAAAAAAAAAAAAAAA= - ConsoleDrivers\AnsiResponseParser\Keyboard\EscAsAltPattern.cs + Drivers\AnsiResponseParser\Keyboard\EscAsAltPattern.cs - + AAACAAAAAAAAAAAAAAAAAAAQAACAAAAAAAAAAAAAAAA= - ConsoleDrivers\AnsiResponseParser\Keyboard\Ss3Pattern.cs + Drivers\AnsiResponseParser\Keyboard\Ss3Pattern.cs - + + + + AABgAAAAIAAIAgQUAAAAAQAAAAAAAAAAQAAKAAAAEAI= + App\ApplicationImpl.cs + + + + + + + gEK4FIgYOAQIuhQeBwoUgSCgAAJL0AACESIKoAiBWw8= + App\Application.cs + + + + + + 27u2V3Pfvf7/x/LOXur1x0de3zZt7v/8c+bfzX/e/c8= + ViewBase\View.Adornments.cs + + + + + + + AAAIEAAAAAIgAYAAAAEQABAAAAAAABAAgAAAAAAAEAA= + App\Logging.cs + + + AAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAI= - ConsoleDrivers\V2\IConsoleInput.cs + Drivers\V2\IConsoleInput.cs - + QAQAAAAAAAABIQQAAAAAAAAAAAAAAAACAAAAAAAAEAA= - ConsoleDrivers\V2\IMainLoop.cs + Drivers\V2\IMainLoop.cs - + AAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAMAAAAAEAAAA= - ConsoleDrivers\V2\IConsoleOutput.cs + Drivers\V2\IConsoleOutput.cs - + AQAAAAAAAIAAAEAIAAAAQIAAAAEMRgAACAAAKABAgAA= - ConsoleDrivers\V2\IOutputBuffer.cs + Drivers\V2\IOutputBuffer.cs - + AAAkAAAAAACAgAAAAAggAAAABAIAAAAAAAAAAAAEAAA= - ConsoleDrivers\V2\IInputProcessor.cs + Drivers\V2\IInputProcessor.cs - + AAAAAAAAAAIAACAAAAAAAIBAAAAAAACAAAAAAAAAAAA= - ConsoleDrivers\AnsiResponseParser\IHeld.cs + Drivers\AnsiResponseParser\IHeld.cs - + AAAAQAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAJAAAAAA= - ConsoleDrivers\AnsiResponseParser\IAnsiResponseParser.cs + Drivers\AnsiResponseParser\IAnsiResponseParser.cs - - - - AAAAAAAAAAAIAgQQAAAAAQAAAAAAAAAAAAAKgAAAAAI= - Application\IApplication.cs - - - + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIQIAAAAAAAA= - ConsoleDrivers\V2\IMainLoopCoordinator.cs + Drivers\V2\IMainLoopCoordinator.cs - + AAAAAAAAAAAAAAAEAAAAAAAAAAAACAAAAAAAAAAAAAA= - ConsoleDrivers\V2\IWindowSizeMonitor.cs - - - - - - - - - BAAAIAAAAQAAAAAQACAAAIBAAQAAAAAAAAAIgAAAAAA= - Application\ITimedEvents.cs + Drivers\V2\IWindowSizeMonitor.cs - + AAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= - ConsoleDrivers\V2\IKeyConverter.cs + Drivers\V2\IKeyConverter.cs - + AIAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAA= - ConsoleDrivers\V2\IToplevelTransitionManager.cs + Drivers\V2\IToplevelTransitionManager.cs - + AAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= - ConsoleDrivers\V2\IConsoleDriverFacade.cs + Drivers\V2\IConsoleDriverFacade.cs - + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= - ConsoleDrivers\V2\INetInput.cs + Drivers\V2\INetInput.cs - + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= - ConsoleDrivers\V2\IWindowsInput.cs + Drivers\V2\IWindowsInput.cs + + + + + + + + + CAIAAAAAAQAAAAAAAAAABEAAAAAABAAAAAAAAAAAAAA= + App\ITimedEvents.cs + + + + + + AAAgAAAAAAAIAgQUAAAAAQAAAAAAAAAAAAAKAAAAEAI= + App\IApplication.cs - + AAAAAAAAAAAAAAAAAAAACAAAAAAIAAIAAAAAAAAAAAA= - ConsoleDrivers\AnsiResponseParser\AnsiResponseParserState.cs + Drivers\AnsiResponseParser\AnsiResponseParserState.cs diff --git a/Terminal.Gui/Drivers/WindowsDriver/WindowsDriver.cs b/Terminal.Gui/Drivers/WindowsDriver/WindowsDriver.cs index 3683f0231c..59941924fc 100644 --- a/Terminal.Gui/Drivers/WindowsDriver/WindowsDriver.cs +++ b/Terminal.Gui/Drivers/WindowsDriver/WindowsDriver.cs @@ -895,51 +895,6 @@ private async Task ProcessButtonDoubleClickedAsync () //buttonPressedCount = 0; } - 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 START_DELAY = 500; - const int ITERATIONS_UNTIL_FAST = 4; - const int FAST_DELAY = 50; - - int iterations = 0; - int delay = START_DELAY; - while (_isButtonPressed) - { - // TODO: This makes IConsoleDriver dependent on Application, which is not ideal. This should be moved to Application. - View? view = Application.WantContinuousButtonPressedView; - - if (view is null) - { - break; - } - - if (iterations++ >= ITERATIONS_UNTIL_FAST) - { - delay = FAST_DELAY; - } - await Task.Delay (delay); - - //Debug.WriteLine($"ProcessContinuousButtonPressedAsync: {view}"); - if (_isButtonPressed && (mouseFlag & MouseFlags.ReportMousePosition) == 0) - { - Point pointMove = _pointMove; - // TODO: This makes IConsoleDriver dependent on Application, which is not ideal. This should be moved to Application. - Application.Invoke (() => - { - var me = new MouseEventArgs - { - ScreenPosition = pointMove, - Position = pointMove, - Flags = mouseFlag - }; - OnMouseEvent (me); - }); - } - } - } - private void ResizeScreen () { _outputBuffer = new WindowsConsole.ExtendedCharInfo [Rows * Cols]; @@ -990,7 +945,7 @@ private MouseEventArgs ToDriverMouse (WindowsConsole.MouseEventRecord mouseEvent if (_isButtonDoubleClicked || _isOneFingerDoubleClicked) { // TODO: This makes IConsoleDriver dependent on Application, which is not ideal. This should be moved to Application. - Application.MainLoop!.AddIdle ( + Application.MainLoop!.TimedEvents.Add (TimeSpan.Zero, () => { Task.Run (async () => await ProcessButtonDoubleClickedAsync ()); @@ -1058,18 +1013,6 @@ private MouseEventArgs ToDriverMouse (WindowsConsole.MouseEventRecord mouseEvent _lastMouseButtonPressed = mouseEvent.ButtonState; _isButtonPressed = true; - - if ((mouseFlag & MouseFlags.ReportMousePosition) == 0) - { - // TODO: This makes IConsoleDriver dependent on Application, which is not ideal. This should be moved to Application. - Application.MainLoop!.AddIdle ( - () => - { - Task.Run (async () => await ProcessContinuousButtonPressedAsync (mouseFlag)); - - return false; - }); - } } else if (_lastMouseButtonPressed != null && mouseEvent.EventFlags == 0 diff --git a/Terminal.Gui/Drivers/WindowsDriver/WindowsMainLoop.cs b/Terminal.Gui/Drivers/WindowsDriver/WindowsMainLoop.cs index 0e10a99b9a..73308ae85e 100644 --- a/Terminal.Gui/Drivers/WindowsDriver/WindowsMainLoop.cs +++ b/Terminal.Gui/Drivers/WindowsDriver/WindowsMainLoop.cs @@ -73,7 +73,7 @@ bool IMainLoopDriver.EventsPending () #if HACK_CHECK_WINCHANGED _winChange.Set (); #endif - if (_resultQueue.Count > 0 || _mainLoop!.TimedEvents.CheckTimersAndIdleHandlers (out int waitTimeout)) + if (_resultQueue.Count > 0 || _mainLoop!.TimedEvents.CheckTimers (out int waitTimeout)) { return true; } @@ -102,9 +102,9 @@ bool IMainLoopDriver.EventsPending () if (!_eventReadyTokenSource.IsCancellationRequested) { #if HACK_CHECK_WINCHANGED - return _resultQueue.Count > 0 || _mainLoop.TimedEvents.CheckTimersAndIdleHandlers (out _) || _winChanged; + return _resultQueue.Count > 0 || _mainLoop.TimedEvents.CheckTimers (out _) || _winChanged; #else - return _resultQueue.Count > 0 || _mainLoop.TimedEvents.CheckTimersAndIdleHandlers (out _); + return _resultQueue.Count > 0 || _mainLoop.TimedEvents.CheckTimers (out _); #endif } diff --git a/Terminal.Gui/Terminal.Gui.csproj b/Terminal.Gui/Terminal.Gui.csproj index b78fc2e16b..15d5869758 100644 --- a/Terminal.Gui/Terminal.Gui.csproj +++ b/Terminal.Gui/Terminal.Gui.csproj @@ -62,9 +62,7 @@ - + @@ -205,7 +203,7 @@ - - + + diff --git a/Terminal.Gui/ViewBase/Adornment/Border.Arrangment.cs b/Terminal.Gui/ViewBase/Adornment/Border.Arrangment.cs index a76005138d..546c871c75 100644 --- a/Terminal.Gui/ViewBase/Adornment/Border.Arrangment.cs +++ b/Terminal.Gui/ViewBase/Adornment/Border.Arrangment.cs @@ -431,9 +431,9 @@ private void ApplicationOnMouseEvent (object? sender, MouseEventArgs e) Application.MouseEvent -= ApplicationOnMouseEvent; - if (Application.MouseGrabView == this && _dragPosition.HasValue) + if (Application.MouseGrabHandler.MouseGrabView == this && _dragPosition.HasValue) { - Application.UngrabMouse (); + Application.MouseGrabHandler.UngrabMouse (); } // Clean up all arrangement buttons @@ -498,7 +498,7 @@ protected override bool OnMouseEvent (MouseEventArgs mouseEvent) // Set the start grab point to the Frame coords _startGrabPoint = new (mouseEvent.Position.X + Frame.X, mouseEvent.Position.Y + Frame.Y); _dragPosition = mouseEvent.Position; - Application.GrabMouse (this); + Application.MouseGrabHandler.GrabMouse (this); // Determine the mode based on where the click occurred ViewArrangement arrangeMode = DetermineArrangeModeFromClick (); @@ -511,7 +511,7 @@ protected override bool OnMouseEvent (MouseEventArgs mouseEvent) return true; } - if (mouseEvent.Flags is (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition) && Application.MouseGrabView == this) + if (mouseEvent.Flags is (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition) && Application.MouseGrabHandler.MouseGrabView == this) { if (_dragPosition.HasValue) { @@ -523,7 +523,7 @@ protected override bool OnMouseEvent (MouseEventArgs mouseEvent) if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Released) && _dragPosition.HasValue) { _dragPosition = null; - Application.UngrabMouse (); + Application.MouseGrabHandler.UngrabMouse (); EndArrangeMode (); @@ -763,7 +763,7 @@ internal void HandleDragOperation (MouseEventArgs mouseEvent) private void Application_GrabbingMouse (object? sender, GrabMouseEventArgs e) { - if (Application.MouseGrabView == this && _dragPosition.HasValue) + if (Application.MouseGrabHandler.MouseGrabView == this && _dragPosition.HasValue) { e.Cancel = true; } @@ -771,7 +771,7 @@ private void Application_GrabbingMouse (object? sender, GrabMouseEventArgs e) private void Application_UnGrabbingMouse (object? sender, GrabMouseEventArgs e) { - if (Application.MouseGrabView == this && _dragPosition.HasValue) + if (Application.MouseGrabHandler.MouseGrabView == this && _dragPosition.HasValue) { e.Cancel = true; } @@ -784,8 +784,8 @@ private void Application_UnGrabbingMouse (object? sender, GrabMouseEventArgs e) /// protected override void Dispose (bool disposing) { - Application.GrabbingMouse -= Application_GrabbingMouse; - Application.UnGrabbingMouse -= Application_UnGrabbingMouse; + Application.MouseGrabHandler.GrabbingMouse -= Application_GrabbingMouse; + Application.MouseGrabHandler.UnGrabbingMouse -= Application_UnGrabbingMouse; _dragPosition = null; base.Dispose (disposing); diff --git a/Terminal.Gui/ViewBase/Adornment/Border.cs b/Terminal.Gui/ViewBase/Adornment/Border.cs index 259a459987..b03645bb6f 100644 --- a/Terminal.Gui/ViewBase/Adornment/Border.cs +++ b/Terminal.Gui/ViewBase/Adornment/Border.cs @@ -50,8 +50,8 @@ public Border (View parent) : base (parent) CanFocus = false; TabStop = TabBehavior.TabGroup; - Application.GrabbingMouse += Application_GrabbingMouse; - Application.UnGrabbingMouse += Application_UnGrabbingMouse; + Application.MouseGrabHandler.GrabbingMouse += Application_GrabbingMouse; + Application.MouseGrabHandler.UnGrabbingMouse += Application_UnGrabbingMouse; ThicknessChanged += OnThicknessChanged; } diff --git a/Terminal.Gui/ViewBase/IMouseHeldDown.cs b/Terminal.Gui/ViewBase/IMouseHeldDown.cs new file mode 100644 index 0000000000..5f7435793b --- /dev/null +++ b/Terminal.Gui/ViewBase/IMouseHeldDown.cs @@ -0,0 +1,35 @@ +#nullable enable +using System.ComponentModel; + +namespace Terminal.Gui.ViewBase; + +/// +/// +/// Handler for raising periodic events while the mouse is held down. +/// Typically, mouse button only needs to be pressed down in a view +/// to begin this event after which it can be moved elsewhere. +/// +/// +/// Common use cases for this includes holding a button down to increase +/// a counter (e.g. in ). +/// +/// +public interface IMouseHeldDown : IDisposable +{ + /// + /// Periodically raised when the mouse is pressed down inside the view . + /// + public event EventHandler MouseIsHeldDownTick; + + /// + /// Call to indicate that the mouse has been pressed down and any relevant actions should + /// be undertaken (start timers, etc). + /// + void Start (); + + /// + /// Call to indicate that the mouse has been released and any relevant actions should + /// be undertaken (stop timers, etc). + /// + void Stop (); +} diff --git a/Terminal.Gui/ViewBase/MouseHeldDown.cs b/Terminal.Gui/ViewBase/MouseHeldDown.cs new file mode 100644 index 0000000000..ff0764733a --- /dev/null +++ b/Terminal.Gui/ViewBase/MouseHeldDown.cs @@ -0,0 +1,111 @@ +#nullable enable +using System.ComponentModel; + +namespace Terminal.Gui.ViewBase; + +/// +/// INTERNAL: Manages the logic for handling a "mouse held down" state on a View. It is used to +/// repeatedly trigger an action (via events) while the mouse button is held down, such as for auto-repeat in +/// scrollbars or buttons. +/// +internal class MouseHeldDown : IMouseHeldDown +{ + public MouseHeldDown (View host, ITimedEvents? timedEvents, IMouseGrabHandler? mouseGrabber) + { + _mouseGrabView = host; + _timedEvents = timedEvents; + _mouseGrabber = mouseGrabber; + _smoothTimeout = new (TimeSpan.FromMilliseconds (500), TimeSpan.FromMilliseconds (50), 0.5, TickWhileMouseIsHeldDown); + } + + private readonly View _mouseGrabView; + private readonly ITimedEvents? _timedEvents; + private readonly IMouseGrabHandler? _mouseGrabber; + + private readonly SmoothAcceleratingTimeout _smoothTimeout; + private bool _isDown; + private object? _timeout; + + public event EventHandler? MouseIsHeldDownTick; + + public void Start () + { + if (_isDown) + { + return; + } + + _isDown = true; + _mouseGrabber?.GrabMouse (_mouseGrabView); + + // Then periodic ticks + _timeout = _timedEvents?.Add (_smoothTimeout); + } + + public void Stop () + { + _smoothTimeout.Reset (); + + if (_mouseGrabber?.MouseGrabView == _mouseGrabView) + { + _mouseGrabber?.UngrabMouse (); + } + + if (_timeout != null) + { + _timedEvents?.Remove (_timeout); + } + + _mouseGrabView.MouseState = MouseState.None; + _isDown = false; + } + + public void Dispose () + { + if (_mouseGrabber?.MouseGrabView == _mouseGrabView) + { + Stop (); + } + } + + protected virtual bool OnMouseIsHeldDownTick (CancelEventArgs eventArgs) { return false; } + + private bool RaiseMouseIsHeldDownTick () + { + CancelEventArgs args = new (); + + args.Cancel = OnMouseIsHeldDownTick (args) || args.Cancel; + + if (!args.Cancel && MouseIsHeldDownTick is { }) + { + MouseIsHeldDownTick?.Invoke (this, args); + } + + // User event cancelled the mouse held down status so + // stop the currently running operation. + if (args.Cancel) + { + Stop (); + } + + return args.Cancel; + } + + private bool TickWhileMouseIsHeldDown () + { + Logging.Debug ("Raising TickWhileMouseIsHeldDown..."); + + if (_isDown) + { + _smoothTimeout.AdvanceStage (); + RaiseMouseIsHeldDownTick (); + } + else + { + _smoothTimeout.Reset (); + Stop (); + } + + return _isDown; + } +} diff --git a/Terminal.Gui/ViewBase/View.Mouse.cs b/Terminal.Gui/ViewBase/View.Mouse.cs index a59513ef01..47de711d59 100644 --- a/Terminal.Gui/ViewBase/View.Mouse.cs +++ b/Terminal.Gui/ViewBase/View.Mouse.cs @@ -5,11 +5,18 @@ namespace Terminal.Gui.ViewBase; public partial class View // Mouse APIs { + /// + /// Handles , we have detected a button + /// down in the view and have grabbed the mouse. + /// + public IMouseHeldDown? MouseHeldDown { get; set; } + /// Gets the mouse bindings for this view. public MouseBindings MouseBindings { get; internal set; } = null!; private void SetupMouse () { + MouseHeldDown = new MouseHeldDown (this, Application.TimedEvents,Application.MouseGrabHandler); MouseBindings = new (); // TODO: Should the default really work with any button or just button1? @@ -307,6 +314,19 @@ protected virtual void OnMouseLeave () { } /// , if the event was handled, otherwise. public bool RaiseMouseEvent (MouseEventArgs mouseEvent) { + // TODO: probably this should be moved elsewhere, please advise + if (WantContinuousButtonPressed && MouseHeldDown != null) + { + if (mouseEvent.IsPressed) + { + MouseHeldDown.Start (); + } + else + { + MouseHeldDown.Stop (); + } + } + if (OnMouseEvent (mouseEvent) || mouseEvent.Handled) { return true; @@ -355,7 +375,7 @@ internal bool WhenGrabbedHandleReleased (MouseEventArgs mouseEvent) if (mouseEvent.IsReleased) { - if (Application.MouseGrabView == this) + if (Application.MouseGrabHandler.MouseGrabView == this) { //Logging.Debug ($"{Id} - {MouseState}"); MouseState &= ~MouseState.Pressed; @@ -387,9 +407,9 @@ private bool WhenGrabbedHandlePressed (MouseEventArgs mouseEvent) if (mouseEvent.IsPressed) { // The first time we get pressed event, grab the mouse and set focus - if (Application.MouseGrabView != this) + if (Application.MouseGrabHandler.MouseGrabView != this) { - Application.GrabMouse (this); + Application.MouseGrabHandler.GrabMouse (this); if (!HasFocus && CanFocus) { @@ -425,11 +445,6 @@ private bool WhenGrabbedHandlePressed (MouseEventArgs mouseEvent) } } - if (WantContinuousButtonPressed && Application.MouseGrabView == this) - { - return RaiseMouseClickEvent (mouseEvent); - } - return mouseEvent.Handled = true; } @@ -526,10 +541,10 @@ internal bool WhenGrabbedHandleClicked (MouseEventArgs mouseEvent) { mouseEvent.Handled = false; - if (Application.MouseGrabView == this && mouseEvent.IsSingleClicked) + if (Application.MouseGrabHandler.MouseGrabView == this && mouseEvent.IsSingleClicked) { // We're grabbed. Clicked event comes after the last Release. This is our signal to ungrab - Application.UngrabMouse (); + Application.MouseGrabHandler.UngrabMouse (); // TODO: Prove we need to unset MouseState.Pressed and MouseState.PressedOutside here // TODO: There may be perf gains if we don't unset these flags here @@ -680,4 +695,4 @@ protected virtual void OnMouseStateChanged (EventArgs args) { } #endregion MouseState Handling private void DisposeMouse () { } -} +} \ No newline at end of file diff --git a/Terminal.Gui/ViewBase/View.cs b/Terminal.Gui/ViewBase/View.cs index 3dd4c9c994..2061ef4828 100644 --- a/Terminal.Gui/ViewBase/View.cs +++ b/Terminal.Gui/ViewBase/View.cs @@ -71,14 +71,9 @@ protected virtual void Dispose (bool disposing) DisposeAdornments (); DisposeScrollBars (); - if (Application.MouseGrabView == this) + if (Application.MouseGrabHandler.MouseGrabView == this) { - Application.UngrabMouse (); - } - - if (Application.WantContinuousButtonPressedView == this) - { - Application.WantContinuousButtonPressedView = null; + Application.MouseGrabHandler.UngrabMouse (); } for (int i = InternalSubViews.Count - 1; i >= 0; i--) diff --git a/Terminal.Gui/Views/Autocomplete/Autocomplete.cd b/Terminal.Gui/Views/Autocomplete/Autocomplete.cd index ca47d6435b..8b0f16b4f0 100644 --- a/Terminal.Gui/Views/Autocomplete/Autocomplete.cd +++ b/Terminal.Gui/Views/Autocomplete/Autocomplete.cd @@ -1,13 +1,13 @@  - + AAAgAABAAQIAAAAAAAAAAAAABAAAIAQAgAEIAggAIAA= Core\Autocomplete\AppendAutocomplete.cs - + AAQgAAAAAUAAIAAAAAIAAAAAAAEAIAQIgQAIQAAAMBA= @@ -15,10 +15,10 @@ - + - + Core\Autocomplete\PopupAutocomplete.cs @@ -29,7 +29,7 @@ Core\Autocomplete\PopupAutocomplete.cs - + CEAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAIAA= @@ -37,28 +37,28 @@ - + AAAAAAAAAAAAAEAAAAAABAAAAAAAAAAAAAAAAAAAAAE= Core\Autocomplete\Suggestion.cs - + AAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAgAAAAAAAAAA= Views\TextField.cs - + AAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAgAAAAAAAAAA= Views\TextView.cs - + AAQgAAAAAUAAIAAAAAAAAAAAAAEAIAQIgQAIQAAAMBA= @@ -68,7 +68,7 @@ - + AEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAA= diff --git a/Terminal.Gui/Views/Autocomplete/PopupAutocomplete.cs b/Terminal.Gui/Views/Autocomplete/PopupAutocomplete.cs index 1c977715e9..2c07f1abab 100644 --- a/Terminal.Gui/Views/Autocomplete/PopupAutocomplete.cs +++ b/Terminal.Gui/Views/Autocomplete/PopupAutocomplete.cs @@ -125,7 +125,7 @@ public override bool OnMouseEvent (MouseEventArgs me, bool fromHost = false) { Visible = true; HostControl?.SetNeedsDraw (); - Application.UngrabMouse (); + Application.MouseGrabHandler.UngrabMouse (); return false; } diff --git a/Terminal.Gui/Views/Button.cs b/Terminal.Gui/Views/Button.cs index 894af07d68..942e97b118 100644 --- a/Terminal.Gui/Views/Button.cs +++ b/Terminal.Gui/Views/Button.cs @@ -69,6 +69,11 @@ public Button () base.ShadowStyle = DefaultShadow; HighlightStates = DefaultHighlightStates; + + if (MouseHeldDown != null) + { + MouseHeldDown.MouseIsHeldDownTick += (_,_) => RaiseAccepting (null); + } } private bool? HandleHotKeyCommand (ICommandContext commandContext) diff --git a/Terminal.Gui/Views/CharMap/CharMap.cs b/Terminal.Gui/Views/CharMap/CharMap.cs index d62e9df930..683ee0601a 100644 --- a/Terminal.Gui/Views/CharMap/CharMap.cs +++ b/Terminal.Gui/Views/CharMap/CharMap.cs @@ -767,7 +767,7 @@ private void ShowDetails () } // BUGBUG: This is a workaround for some weird ScrollView related mouse grab bug - Application.GrabMouse (this); + Application.MouseGrabHandler.GrabMouse (this); } #endregion Details Dialog diff --git a/Terminal.Gui/Views/CollectionNavigation/CollectionNavigation.cd b/Terminal.Gui/Views/CollectionNavigation/CollectionNavigation.cd index 7236c3adfe..02bb4adb16 100644 --- a/Terminal.Gui/Views/CollectionNavigation/CollectionNavigation.cd +++ b/Terminal.Gui/Views/CollectionNavigation/CollectionNavigation.cd @@ -9,7 +9,7 @@ - + AAgEAAAAAAAQAAAIAAEAAgAAAAAABAAEAAAAACwAAAA= @@ -20,7 +20,7 @@ - + AAAAAAAAAAAAQAAAAAAAAgAAAAAAAAAEAAAAAAAAAAA= @@ -28,7 +28,7 @@ - + AAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAQA= @@ -36,14 +36,14 @@ - + AAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAEAAAAIAAAAAA= Views\CollectionNavigation\TableCollectionNavigator.cs - + AAE+ASAkEnAAABAAKGAggYAZJAIAABEAcBAaAwAQIAA= @@ -54,25 +54,7 @@ - - - - - - - iIY4LQFUHDKVIHIESBgigQcFT6GxhBDABGJItBQAwAQ= - Views\FileDialog.cs - - - - - - - AAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAEAAAAAAAAAAA= - Views\FileDialogCollectionNavigator.cs - - - + QwUeAxwgICIAcABIABeR0oBAkhoFGGOBDABgAN3oPEI= @@ -80,7 +62,7 @@ - + AAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAA= @@ -88,7 +70,7 @@ - + UwAGySBgBSBGMAQgIiCaBDUItJIBSAWwRMQOSgQCwJI= @@ -99,21 +81,39 @@ - + + + + + + + iIY4LQFUHDKVIHIESBoigQcFT6GxhBDABGJItBQAwAQ= + Views\FileDialogs\FileDialog.cs + + + + + + + AAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAEAAAAAAAAAAA= + Views\FileDialogs\FileDialogCollectionNavigator.cs + + + AAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAA= Views\CollectionNavigation\ICollectionNavigatorMatcher.cs - + AAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= Views\CollectionNavigation\IListCollectionNavigator.cs - + AAgAAAAAAAAAAAAIAAAAAAAAAAAABAAAAAAAACgAAAA= diff --git a/Terminal.Gui/Views/ComboBox.cs b/Terminal.Gui/Views/ComboBox.cs index 158c7cf23c..369de9e8a6 100644 --- a/Terminal.Gui/Views/ComboBox.cs +++ b/Terminal.Gui/Views/ComboBox.cs @@ -958,7 +958,7 @@ protected override void OnHasFocusChanged (bool newHasFocus, [CanBeNull] View pr { _isFocusing = true; _highlighted = _container.SelectedItem; - Application.GrabMouse (this); + Application.MouseGrabHandler.GrabMouse (this); } } else @@ -967,7 +967,7 @@ protected override void OnHasFocusChanged (bool newHasFocus, [CanBeNull] View pr { _isFocusing = false; _highlighted = _container.SelectedItem; - Application.UngrabMouse (); + Application.MouseGrabHandler.UngrabMouse (); } } } diff --git a/Terminal.Gui/Views/DatePicker.cs b/Terminal.Gui/Views/DatePicker.cs index 2e2ffa4bc9..a3253f1221 100644 --- a/Terminal.Gui/Views/DatePicker.cs +++ b/Terminal.Gui/Views/DatePicker.cs @@ -227,13 +227,7 @@ private void SetInitialProperties (DateTime date) NoDecorations = true, ShadowStyle = ShadowStyle.None }; - - _previousMonthButton.Accepting += (sender, e) => - { - Date = _date.AddMonths (-1); - CreateCalendar (); - _dateField.Date = Date; - }; + _previousMonthButton.Accepting += (_, _) => AdjustMonth (-1); _nextMonthButton = new Button { @@ -248,12 +242,7 @@ private void SetInitialProperties (DateTime date) ShadowStyle = ShadowStyle.None }; - _nextMonthButton.Accepting += (sender, e) => - { - Date = _date.AddMonths (1); - CreateCalendar (); - _dateField.Date = Date; - }; + _nextMonthButton.Accepting += (_, _) => AdjustMonth (1); CreateCalendar (); SelectDayOnCalendar (_date.Day); @@ -287,6 +276,13 @@ private void SetInitialProperties (DateTime date) Add (_dateLabel, _dateField, _calendar, _previousMonthButton, _nextMonthButton); } + private void AdjustMonth (int offset) + { + Date = _date.AddMonths (offset); + CreateCalendar (); + _dateField.Date = Date; + } + /// protected override bool OnDrawingText () { return true; } diff --git a/Terminal.Gui/Views/FileDialogs/FileDialog.cd b/Terminal.Gui/Views/FileDialogs/FileDialog.cd index a8174bf7e7..22893eb03a 100644 --- a/Terminal.Gui/Views/FileDialogs/FileDialog.cd +++ b/Terminal.Gui/Views/FileDialogs/FileDialog.cd @@ -1,6 +1,21 @@  - + + + + AAACAAAAAAgAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + FileServices\DefaultSearchMatcher.cs + + + + + + + ABAIQAIIIAAAAAACQAAAAIQAAAQAAIAAAQAAAAAIAAI= + FileServices\FileSystemInfoStats.cs + + + @@ -8,190 +23,146 @@ - - - Windows\FileDialog.cs - - - + - Views\FileDialog.cs + Views\FileDialogs\FileDialog.cs - g4YYDAEXEDKZgHMFyFAikQCFSKUQhRDABqJIlBSAwgw= - Views\FileDialog.cs - - - - - - - - - - - - - - AAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAA= - FileServices\AllowedType.cs + iIY4LQFUHDKVIHIESBoigQcFT6GxhBDABGJItBQAwAQ= + Views\FileDialogs\FileDialog.cs - - - - AAAAAAAAAAAgAAAEAAAAAAAAAAAAAAAAAAgAAAAABAA= - FileServices\AllowedType.cs - - - - - + + - AAAAAAAAAAAAACAAAAAAACAAAAEAAAAAAAAAAAAAgAA= - FileServices\DefaultFileOperations.cs + GgBAAAFHAAAAuAAAAAAAEAQQBYAAKREAAAAYQCCAAAA= + Views\FileDialogs\FileDialogStyle.cs - - - + + + + + - AAACAAAAAAgAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= - FileServices\DefaultSearchMatcher.cs + AAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAA= + Views\FileDialogs\FilesSelectedEventArgs.cs - - + AQABAgEAAAAAAAAAIACAAAAAAAAAAQAAAAAAAAAADAI= - FileServices\FileDialogHistory.cs + Views\FileDialogs\FileDialogHistory.cs - - - - - - - AAAAEAAAAAAAAAIEAAAAAAAAAAAAAAAAAAAAAAAAAAA= - FileServices\FileDialogRootTreeNode.cs - - - + AABAABAAAAAAAAIAAAAEQAAAAAAAQAAAAgAAAAAAAAI= - FileServices\FileDialogState.cs + Views\FileDialogs\FileDialogState.cs - - + + - GgBIAAFEAAAAuAAAAgAEEASABQACKRkAAAEYACCAAAA= - FileServices\FileDialogStyle.cs + AAAAAAAAAAAgAAAEAAAAAAAAAAAAAAAAAAgAAAAABAA= + Views\FileDialogs\AllowedType.cs + - - + + - EAAACAAAAAAAAAAAQAAAAAQAAAAAQAAAAAAAAAQACAA= - FileServices\FileDialogTreeBuilder.cs + AAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAA= + Views\FileDialogs\AllowedType.cs - - + + - + - AAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAA= - FileServices\FilesSelectedEventArgs.cs + AIACAAABQAAAAAAAAAAACAAAIACAAAAAAAIAAAAAAAA= + Text\NerdFonts.cs - - + + - ABAIQAIIIAAAAAACQAAAAIQAAAQAAIAAAQAAAAAIAAI= - FileServices\FileSystemInfoStats.cs + EAAAAAAAAAAAAAAAAAAABAAwAAAAQAAAAABAAAAACAA= + FileServices\FileSystemTreeBuilder.cs + - - - - - + + - AIACAAABQAAAAAAAAAAAAAAAIACAAAAAAAIAAAQAAAA= - Views\NerdFonts.cs + AAAAAAAAAAAAACAAAAAAACAAAAEAAAAAAAAAAAAAgAA= + Views\FileDialogs\DefaultFileOperations.cs + - - + + - AAAAAAAAAgAAAAAAAAAQAAAAAAAEAAAAAAAAAAAAAAA= - FileServices\FileDialogIconGetterArgs.cs + AgAAAAAAAEAAAAAAAAAAAAEAAAAAAACAAAAAAAAAAAA= + FileServices\FileSystemColorProvider.cs - - + + + + ABAAAAAAAACAQAAAAAAAAEAgAAAAAQAAAAAAAAAAAiA= + FileServices\FileSystemIconProvider.cs + + + + AQAAAAAAIAACAEAACAAAAAACAAAEAAAEAAAAgAgBBAA= - Views\FileDialogTableSource.cs + Views\FileDialogs\FileDialogTableSource.cs - - - - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAA= - FileServices\AllowedType.cs - - - + AAAAAAAAAAAAACAAAAAAAAAAAAEAAAAAAAAAAAAAgAA= FileServices\IFileOperations.cs - + AAACAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= FileServices\ISearchMatcher.cs - - + + - AAAAABAAAAAAACAAAAAAAAAAAAAEAAAAAAAAAAAAAAA= - Views\OpenDialog.cs + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAA= + Views\FileDialogs\AllowedType.cs - - - + + + - AAAAAAAAAACAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAA= - FileServices\FileDialogIconGetterContext.cs + AAAAABAAAAAAACAAAAAAAAAAAAAEAAAAAAAAAAAAAAA= + Views\FileDialogs\OpenMode.cs - - - - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAA= - FileServices\FileDialogRootTreeNode.cs - - \ No newline at end of file diff --git a/Terminal.Gui/Views/Menuv1/Menu.cs b/Terminal.Gui/Views/Menuv1/Menu.cs index 54bd6ed6d1..8969595acd 100644 --- a/Terminal.Gui/Views/Menuv1/Menu.cs +++ b/Terminal.Gui/Views/Menuv1/Menu.cs @@ -19,7 +19,7 @@ public Menu () } Application.MouseEvent += Application_RootMouseEvent; - Application.UnGrabbedMouse += Application_UnGrabbedMouse; + Application.MouseGrabHandler.UnGrabbedMouse += Application_UnGrabbedMouse; // Things this view knows how to do AddCommand (Command.Up, () => MoveUp ()); @@ -220,7 +220,7 @@ public void Run (Action? action) return; } - Application.UngrabMouse (); + Application.MouseGrabHandler.UngrabMouse (); _host.CloseAllMenus (); Application.LayoutAndDraw (true); @@ -238,7 +238,7 @@ protected override void Dispose (bool disposing) } Application.MouseEvent -= Application_RootMouseEvent; - Application.UnGrabbedMouse -= Application_UnGrabbedMouse; + Application.MouseGrabHandler.UnGrabbedMouse -= Application_UnGrabbedMouse; base.Dispose (disposing); } @@ -535,7 +535,7 @@ private void Application_UnGrabbedMouse (object? sender, ViewEventArgs a) private void CloseAllMenus () { - Application.UngrabMouse (); + Application.MouseGrabHandler.UngrabMouse (); _host.CloseAllMenus (); } diff --git a/Terminal.Gui/Views/Menuv1/MenuBar.cs b/Terminal.Gui/Views/Menuv1/MenuBar.cs index b9d233e70e..615241e579 100644 --- a/Terminal.Gui/Views/Menuv1/MenuBar.cs +++ b/Terminal.Gui/Views/Menuv1/MenuBar.cs @@ -442,12 +442,12 @@ out OpenCurrentMenu._currentChild if (_isContextMenuLoading) { - Application.GrabMouse (_openMenu); + Application.MouseGrabHandler.GrabMouse (_openMenu); _isContextMenuLoading = false; } else { - Application.GrabMouse (this); + Application.MouseGrabHandler.GrabMouse (this); } } @@ -524,16 +524,16 @@ internal void CleanUp () SetNeedsDraw (); - if (Application.MouseGrabView is { } && Application.MouseGrabView is MenuBar && Application.MouseGrabView != this) + if (Application.MouseGrabHandler.MouseGrabView is { } && Application.MouseGrabHandler.MouseGrabView is MenuBar && Application.MouseGrabHandler.MouseGrabView != this) { - var menuBar = Application.MouseGrabView as MenuBar; + var menuBar = Application.MouseGrabHandler.MouseGrabView as MenuBar; if (menuBar!.IsMenuOpen) { menuBar.CleanUp (); } } - Application.UngrabMouse (); + Application.MouseGrabHandler.UngrabMouse (); _isCleaning = false; } @@ -556,7 +556,7 @@ internal void CloseAllMenus () _selected = -1; } - Application.UngrabMouse (); + Application.MouseGrabHandler.UngrabMouse (); } if (OpenCurrentMenu is { }) @@ -622,9 +622,9 @@ internal bool CloseMenu (bool reopen, bool isSubMenu, bool ignoreUseSubMenusSing _previousFocused.SetFocus (); } - if (Application.MouseGrabView == _openMenu) + if (Application.MouseGrabHandler.MouseGrabView == _openMenu) { - Application.UngrabMouse (); + Application.MouseGrabHandler.UngrabMouse (); } _openMenu?.Dispose (); _openMenu = null; @@ -652,9 +652,9 @@ internal bool CloseMenu (bool reopen, bool isSubMenu, bool ignoreUseSubMenusSing if (OpenCurrentMenu is { }) { SuperView?.Remove (OpenCurrentMenu); - if (Application.MouseGrabView == OpenCurrentMenu) + if (Application.MouseGrabHandler.MouseGrabView == OpenCurrentMenu) { - Application.UngrabMouse (); + Application.MouseGrabHandler.UngrabMouse (); } OpenCurrentMenu.Dispose (); OpenCurrentMenu = null; @@ -845,9 +845,9 @@ internal void OpenMenu (int index, int sIndex = -1, MenuBarItem? subMenu = null! if (_openMenu is { }) { SuperView?.Remove (_openMenu); - if (Application.MouseGrabView == _openMenu) + if (Application.MouseGrabHandler.MouseGrabView == _openMenu) { - Application.UngrabMouse (); + Application.MouseGrabHandler.UngrabMouse (); } _openMenu.Dispose (); _openMenu = null; @@ -935,7 +935,7 @@ internal void OpenMenu (int index, int sIndex = -1, MenuBarItem? subMenu = null! Host = this, X = first!.Frame.Left, Y = first.Frame.Top, BarItems = newSubMenu }; last!.Visible = false; - Application.GrabMouse (OpenCurrentMenu); + Application.MouseGrabHandler.GrabMouse (OpenCurrentMenu); } OpenCurrentMenu._previousSubFocused = last._previousSubFocused; @@ -1029,9 +1029,9 @@ internal void RemoveAllOpensSubMenus () foreach (Menu item in _openSubMenu) { SuperView?.Remove (item); - if (Application.MouseGrabView == item) + if (Application.MouseGrabHandler.MouseGrabView == item) { - Application.UngrabMouse (); + Application.MouseGrabHandler.UngrabMouse (); } item.Dispose (); } @@ -1045,7 +1045,7 @@ internal bool Run (Action? action) return false; } - Application.AddIdle ( + Application.AddTimeout (TimeSpan.Zero, () => { action (); @@ -1137,7 +1137,7 @@ internal bool SelectItem (MenuItem? item) return false; } - Application.UngrabMouse (); + Application.MouseGrabHandler.UngrabMouse (); CloseAllMenus (); Application.LayoutAndDraw (true); _openedByAltKey = true; @@ -1209,15 +1209,15 @@ private bool ProcessMenu (int i, MenuBarItem mi) Point screen = ViewportToScreen (new Point (0, i)); var menu = new Menu { Host = this, X = screen.X, Y = screen.Y, BarItems = mi }; menu.Run (mi.Action); - if (Application.MouseGrabView == menu) + if (Application.MouseGrabHandler.MouseGrabView == menu) { - Application.UngrabMouse (); + Application.MouseGrabHandler.UngrabMouse (); } menu.Dispose (); } else { - Application.GrabMouse (this); + Application.MouseGrabHandler.GrabMouse (this); _selected = i; OpenMenu (i); @@ -1280,9 +1280,9 @@ private void RemoveSubMenu (int index, bool ignoreUseSubMenusSingleFrame = false SuperView!.Remove (menu); _openSubMenu.Remove (menu); - if (Application.MouseGrabView == menu) + if (Application.MouseGrabHandler.MouseGrabView == menu) { - Application.GrabMouse (this); + Application.MouseGrabHandler.GrabMouse (this); } menu.Dispose (); @@ -1458,9 +1458,9 @@ protected override bool OnMouseEvent (MouseEventArgs me) Point screen = ViewportToScreen (new Point (0, i)); var menu = new Menu { Host = this, X = screen.X, Y = screen.Y, BarItems = Menus [i] }; menu.Run (Menus [i].Action); - if (Application.MouseGrabView == menu) + if (Application.MouseGrabHandler.MouseGrabView == menu) { - Application.UngrabMouse (); + Application.MouseGrabHandler.UngrabMouse (); } menu.Dispose (); @@ -1535,7 +1535,7 @@ protected override bool OnMouseEvent (MouseEventArgs me) internal bool HandleGrabView (MouseEventArgs me, View current) { - if (Application.MouseGrabView is { }) + if (Application.MouseGrabHandler.MouseGrabView is { }) { if (me.View is MenuBar or Menu) { @@ -1546,7 +1546,7 @@ internal bool HandleGrabView (MouseEventArgs me, View current) if (me.Flags == MouseFlags.Button1Clicked) { mbar.CleanUp (); - Application.GrabMouse (me.View); + Application.MouseGrabHandler.GrabMouse (me.View); } else { @@ -1556,10 +1556,10 @@ internal bool HandleGrabView (MouseEventArgs me, View current) } } - if (Application.MouseGrabView != me.View) + if (Application.MouseGrabHandler.MouseGrabView != me.View) { View v = me.View; - Application.GrabMouse (v); + Application.MouseGrabHandler.GrabMouse (v); return true; } @@ -1567,7 +1567,7 @@ internal bool HandleGrabView (MouseEventArgs me, View current) if (me.View != current) { View v = me.View; - Application.GrabMouse (v); + Application.MouseGrabHandler.GrabMouse (v); MouseEventArgs nme; if (me.Position.Y > -1) @@ -1599,7 +1599,7 @@ internal bool HandleGrabView (MouseEventArgs me, View current) && me.Flags != MouseFlags.ReportMousePosition && me.Flags != 0) { - Application.UngrabMouse (); + Application.MouseGrabHandler.UngrabMouse (); if (IsMenuOpen) { @@ -1625,11 +1625,11 @@ internal bool HandleGrabView (MouseEventArgs me, View current) MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition ))) { - Application.GrabMouse (current); + Application.MouseGrabHandler.GrabMouse (current); } else if (IsMenuOpen && (me.View is MenuBar || me.View is Menu)) { - Application.GrabMouse (me.View); + Application.MouseGrabHandler.GrabMouse (me.View); } else { @@ -1645,7 +1645,7 @@ internal bool HandleGrabView (MouseEventArgs me, View current) private MenuBar? GetMouseGrabViewInstance (View? view) { - if (view is null || Application.MouseGrabView is null) + if (view is null || Application.MouseGrabHandler.MouseGrabView is null) { return null; } @@ -1661,7 +1661,7 @@ internal bool HandleGrabView (MouseEventArgs me, View current) hostView = ((Menu)view).Host; } - View grabView = Application.MouseGrabView; + View grabView = Application.MouseGrabHandler.MouseGrabView; MenuBar? hostGrabView = null; if (grabView is MenuBar bar) diff --git a/Terminal.Gui/Views/ScrollBar/ScrollSlider.cs b/Terminal.Gui/Views/ScrollBar/ScrollSlider.cs index c0558192ad..6cb7d5433a 100644 --- a/Terminal.Gui/Views/ScrollBar/ScrollSlider.cs +++ b/Terminal.Gui/Views/ScrollBar/ScrollSlider.cs @@ -307,9 +307,9 @@ protected override bool OnMouseEvent (MouseEventArgs mouseEvent) { if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed) && _lastLocation == -1) { - if (Application.MouseGrabView != this) + if (Application.MouseGrabHandler.MouseGrabView != this) { - Application.GrabMouse (this); + Application.MouseGrabHandler.GrabMouse (this); _lastLocation = location; } } @@ -333,9 +333,9 @@ protected override bool OnMouseEvent (MouseEventArgs mouseEvent) { _lastLocation = -1; - if (Application.MouseGrabView == this) + if (Application.MouseGrabHandler.MouseGrabView == this) { - Application.UngrabMouse (); + Application.MouseGrabHandler.UngrabMouse (); } } diff --git a/Terminal.Gui/Views/Slider/Slider.cs b/Terminal.Gui/Views/Slider/Slider.cs index 4dff5965ea..4808a939f6 100644 --- a/Terminal.Gui/Views/Slider/Slider.cs +++ b/Terminal.Gui/Views/Slider/Slider.cs @@ -1311,7 +1311,7 @@ protected override bool OnMouseEvent (MouseEventArgs mouseEvent) { _dragPosition = mouseEvent.Position; _moveRenderPosition = ClampMovePosition ((Point)_dragPosition); - Application.GrabMouse (this); + Application.MouseGrabHandler.GrabMouse (this); } SetNeedsDraw (); @@ -1357,7 +1357,7 @@ protected override bool OnMouseEvent (MouseEventArgs mouseEvent) || mouseEvent.Flags.HasFlag (MouseFlags.Button1Clicked)) { // End Drag - Application.UngrabMouse (); + Application.MouseGrabHandler.UngrabMouse (); _dragPosition = null; _moveRenderPosition = null; diff --git a/Terminal.Gui/Views/TextInput/TextField.cs b/Terminal.Gui/Views/TextInput/TextField.cs index 559b7d8a22..7d171c0936 100644 --- a/Terminal.Gui/Views/TextInput/TextField.cs +++ b/Terminal.Gui/Views/TextInput/TextField.cs @@ -855,16 +855,16 @@ protected override bool OnMouseEvent (MouseEventArgs ev) _isButtonReleased = false; PrepareSelection (x); - if (Application.MouseGrabView is null) + if (Application.MouseGrabHandler.MouseGrabView is null) { - Application.GrabMouse (this); + Application.MouseGrabHandler.GrabMouse (this); } } else if (ev.Flags == MouseFlags.Button1Released) { _isButtonReleased = true; _isButtonPressed = false; - Application.UngrabMouse (); + Application.MouseGrabHandler.UngrabMouse (); } else if (ev.Flags == MouseFlags.Button1DoubleClicked) { @@ -1007,12 +1007,12 @@ protected override bool OnDrawingContent () /// protected override void OnHasFocusChanged (bool newHasFocus, View previousFocusedView, View view) { - if (Application.MouseGrabView is { } && Application.MouseGrabView == this) + if (Application.MouseGrabHandler.MouseGrabView is { } && Application.MouseGrabHandler.MouseGrabView == this) { - Application.UngrabMouse (); + Application.MouseGrabHandler.UngrabMouse (); } - //if (SelectedLength != 0 && !(Application.MouseGrabView is MenuBar)) + //if (SelectedLength != 0 && !(Application.MouseGrabHandler.MouseGrabView is MenuBar)) // ClearAllSelection (); } diff --git a/Terminal.Gui/Views/TextInput/TextView.cs b/Terminal.Gui/Views/TextInput/TextView.cs index 6d4d7b7c65..5450835d83 100644 --- a/Terminal.Gui/Views/TextInput/TextView.cs +++ b/Terminal.Gui/Views/TextInput/TextView.cs @@ -1677,15 +1677,15 @@ protected override bool OnMouseEvent (MouseEventArgs ev) _lastWasKill = false; _columnTrack = CurrentColumn; - if (Application.MouseGrabView is null) + if (Application.MouseGrabHandler.MouseGrabView is null) { - Application.GrabMouse (this); + Application.MouseGrabHandler.GrabMouse (this); } } else if (ev.Flags.HasFlag (MouseFlags.Button1Released)) { _isButtonReleased = true; - Application.UngrabMouse (); + Application.MouseGrabHandler.UngrabMouse (); } else if (ev.Flags.HasFlag (MouseFlags.Button1DoubleClicked)) { @@ -1893,9 +1893,9 @@ protected override bool OnDrawingContent () /// protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? view) { - if (Application.MouseGrabView is { } && Application.MouseGrabView == this) + if (Application.MouseGrabHandler.MouseGrabView is { } && Application.MouseGrabHandler.MouseGrabView == this) { - Application.UngrabMouse (); + Application.MouseGrabHandler.UngrabMouse (); } } @@ -2039,7 +2039,7 @@ public void Paste () return null; } - if (Application.MouseGrabView == this && IsSelecting) + if (Application.MouseGrabHandler.MouseGrabView == this && IsSelecting) { // BUGBUG: customized rect aren't supported now because the Redraw isn't using the Intersect method. //var minRow = Math.Min (Math.Max (Math.Min (selectionStartRow, currentRow) - topRow, 0), Viewport.Height); diff --git a/Terminal.Gui/Views/TileView.cs b/Terminal.Gui/Views/TileView.cs index 6a993ae577..0e929ac960 100644 --- a/Terminal.Gui/Views/TileView.cs +++ b/Terminal.Gui/Views/TileView.cs @@ -916,7 +916,7 @@ protected override bool OnMouseEvent (MouseEventArgs mouseEvent) { dragPosition = mouseEvent.Position; dragOrignalPos = Orientation == Orientation.Horizontal ? Y : X; - Application.GrabMouse (this); + Application.MouseGrabHandler.GrabMouse (this); if (Orientation == Orientation.Horizontal) { } @@ -960,7 +960,7 @@ protected override bool OnMouseEvent (MouseEventArgs mouseEvent) { // End Drag - Application.UngrabMouse (); + Application.MouseGrabHandler.UngrabMouse (); //Driver.UncookMouse (); FinalisePosition ( diff --git a/Tests/TerminalGuiFluentTesting/GuiTestContext.cs b/Tests/TerminalGuiFluentTesting/GuiTestContext.cs index 773a4c97dd..6321a86cbc 100644 --- a/Tests/TerminalGuiFluentTesting/GuiTestContext.cs +++ b/Tests/TerminalGuiFluentTesting/GuiTestContext.cs @@ -88,7 +88,7 @@ internal GuiTestContext (Func topLevelBuilder, int width, int height, } // Wait for booting to complete with a timeout to avoid hangs - if (!booting.WaitAsync (TimeSpan.FromSeconds (5)).Result) + if (!booting.WaitAsync (TimeSpan.FromSeconds (10)).Result) { throw new TimeoutException ("Application failed to start within the allotted time."); } diff --git a/Tests/UnitTests/Application/ApplicationTests.cs b/Tests/UnitTests/Application/ApplicationTests.cs index 40d2ca5389..b504b94846 100644 --- a/Tests/UnitTests/Application/ApplicationTests.cs +++ b/Tests/UnitTests/Application/ApplicationTests.cs @@ -307,8 +307,7 @@ void CheckReset () // Public Properties Assert.Null (Application.Top); - Assert.Null (Application.MouseGrabView); - Assert.Null (Application.WantContinuousButtonPressedView); + Assert.Null (Application.MouseGrabHandler.MouseGrabView); // Don't check Application.ForceDriver // Assert.Empty (Application.ForceDriver); @@ -569,8 +568,7 @@ public void Internal_Properties_Correct () Assert.Null (Application.Top); RunState rs = Application.Begin (new ()); Assert.Equal (Application.Top, rs.Toplevel); - Assert.Null (Application.MouseGrabView); // public - Assert.Null (Application.WantContinuousButtonPressedView); // public + Assert.Null (Application.MouseGrabHandler.MouseGrabView); // public Application.Top!.Dispose (); } @@ -952,7 +950,7 @@ public void Run_A_Modal_Toplevel_Refresh_Background_On_Moving () Assert.Equal (new (0, 0), w.Frame.Location); Application.RaiseMouseEvent (new () { Flags = MouseFlags.Button1Pressed }); - Assert.Equal (w.Border, Application.MouseGrabView); + Assert.Equal (w.Border, Application.MouseGrabHandler.MouseGrabView); Assert.Equal (new (0, 0), w.Frame.Location); // Move down and to the right. @@ -1116,6 +1114,8 @@ public void Run_t_Does_Not_Creates_Top_Without_Init () private class TestToplevel : Toplevel { } + private readonly object _forceDriverLock = new (); + [Theory] [InlineData ("v2win", typeof (ConsoleDriverFacade))] [InlineData ("v2net", typeof (ConsoleDriverFacade))] @@ -1129,20 +1129,29 @@ public void Run_T_Call_Init_ForceDriver_Should_Pick_Correct_Driver (string drive var result = false; - Task.Run (() => - { - Task.Delay (300).Wait (); - }).ContinueWith ( - (t, _) => + lock (_forceDriverLock) + { + Task.Run (() => + { + while (!Application.Initialized) + { + Task.Delay (300).Wait (); + } + }) + .ContinueWith ( + (t, _) => + { + // no longer loading + Assert.True (Application.Initialized); + + Application.Invoke (() => { - // no longer loading - Application.Invoke (() => - { - result = true; - Application.RequestStop (); - }); - }, - TaskScheduler.FromCurrentSynchronizationContext ()); + result = true; + Application.RequestStop (); + }); + }, + TaskScheduler.FromCurrentSynchronizationContext ()); + } Application.ForceDriver = driverName; Application.Run (); diff --git a/Tests/UnitTests/Application/MainLoopTests.cs b/Tests/UnitTests/Application/MainLoopTests.cs index b40f362b08..5c7866aa55 100644 --- a/Tests/UnitTests/Application/MainLoopTests.cs +++ b/Tests/UnitTests/Application/MainLoopTests.cs @@ -29,61 +29,38 @@ public class MainLoopTests // Idle Handler tests [Fact] - public void AddIdle_Adds_And_Removes () + public void AddTimeout_Adds_And_Removes () { var ml = new MainLoop (new FakeMainLoop ()); Func fnTrue = () => true; Func fnFalse = () => false; - ml.AddIdle (fnTrue); - ml.AddIdle (fnFalse); + var a = ml.TimedEvents.Add (TimeSpan.Zero, fnTrue); + var b = ml.TimedEvents.Add (TimeSpan.Zero, fnFalse); - Assert.Equal (2, ml.TimedEvents.IdleHandlers.Count); - Assert.Equal (fnTrue, ml.TimedEvents.IdleHandlers [0]); - Assert.NotEqual (fnFalse, ml.TimedEvents.IdleHandlers [0]); + Assert.Equal (2, ml.TimedEvents.Timeouts.Count); + Assert.Equal (fnTrue, ml.TimedEvents.Timeouts.ElementAt (0).Value.Callback); + Assert.NotEqual (fnFalse, ml.TimedEvents.Timeouts.ElementAt (0).Value.Callback); - Assert.True (ml.TimedEvents.RemoveIdle (fnTrue)); - Assert.Single (ml.TimedEvents.IdleHandlers); + Assert.True (ml.TimedEvents.Remove (a)); + Assert.Single (ml.TimedEvents.Timeouts); // BUGBUG: This doesn't throw or indicate an error. Ideally RemoveIdle would either // throw an exception in this case, or return an error. // No. Only need to return a boolean. - Assert.False (ml.TimedEvents.RemoveIdle (fnTrue)); + Assert.False (ml.TimedEvents.Remove (a)); - Assert.True (ml.TimedEvents.RemoveIdle (fnFalse)); + Assert.True (ml.TimedEvents.Remove (b)); // BUGBUG: This doesn't throw an exception or indicate an error. Ideally RemoveIdle would either // throw an exception in this case, or return an error. // No. Only need to return a boolean. - Assert.False (ml.TimedEvents.RemoveIdle (fnFalse)); - - // Add again, but with dupe - ml.AddIdle (fnTrue); - ml.AddIdle (fnTrue); - - Assert.Equal (2, ml.TimedEvents.IdleHandlers.Count); - Assert.Equal (fnTrue, ml.TimedEvents.IdleHandlers [0]); - Assert.True (ml.TimedEvents.IdleHandlers [0] ()); - Assert.Equal (fnTrue, ml.TimedEvents.IdleHandlers [1]); - Assert.True (ml.TimedEvents.IdleHandlers [1] ()); - - Assert.True (ml.TimedEvents.RemoveIdle (fnTrue)); - Assert.Single (ml.TimedEvents.IdleHandlers); - Assert.Equal (fnTrue, ml.TimedEvents.IdleHandlers [0]); - Assert.NotEqual (fnFalse, ml.TimedEvents.IdleHandlers [0]); - - Assert.True (ml.TimedEvents.RemoveIdle (fnTrue)); - Assert.Empty (ml.TimedEvents.IdleHandlers); - - // BUGBUG: This doesn't throw an exception or indicate an error. Ideally RemoveIdle would either - // throw an exception in this case, or return an error. - // No. Only need to return a boolean. - Assert.False (ml.TimedEvents.RemoveIdle (fnTrue)); + Assert.False (ml.TimedEvents.Remove(b)); } [Fact] - public void AddIdle_Function_GetsCalled_OnIteration () + public void AddTimeout_Function_GetsCalled_OnIteration () { var ml = new MainLoop (new FakeMainLoop ()); @@ -96,13 +73,13 @@ public void AddIdle_Function_GetsCalled_OnIteration () return true; }; - ml.AddIdle (fn); + ml.TimedEvents.Add (TimeSpan.Zero, fn); ml.RunIteration (); Assert.Equal (1, functionCalled); } [Fact] - public void AddIdle_Twice_Returns_False_Called_Twice () + public void AddTimeout_Twice_Returns_False_Called_Twice () { var ml = new MainLoop (new FakeMainLoop ()); @@ -130,19 +107,21 @@ public void AddIdle_Twice_Returns_False_Called_Twice () return true; }; - ml.AddIdle (fnStop); - ml.AddIdle (fn1); - ml.AddIdle (fn1); + var a = ml.TimedEvents.Add (TimeSpan.Zero, fnStop); + var b = ml.TimedEvents.Add (TimeSpan.Zero, fn1); ml.Run (); - Assert.True (ml.TimedEvents.RemoveIdle (fnStop)); - Assert.False (ml.TimedEvents.RemoveIdle (fn1)); - Assert.False (ml.TimedEvents.RemoveIdle (fn1)); - Assert.Equal (2, functionCalled); + Assert.True (ml.TimedEvents.Remove(a)); + Assert.False (ml.TimedEvents.Remove (a)); + + // Cannot remove b because it returned false i.e. auto removes itself + Assert.False (ml.TimedEvents.Remove (b)); + + Assert.Equal (1, functionCalled); } [Fact] - public void AddIdleTwice_Function_CalledTwice () + public void AddTimeoutTwice_Function_CalledTwice () { var ml = new MainLoop (new FakeMainLoop ()); @@ -155,24 +134,24 @@ public void AddIdleTwice_Function_CalledTwice () return true; }; - ml.AddIdle (fn); - ml.AddIdle (fn); + var a = ml.TimedEvents.Add (TimeSpan.Zero, fn); + var b = ml.TimedEvents.Add (TimeSpan.Zero, fn); ml.RunIteration (); Assert.Equal (2, functionCalled); - Assert.Equal (2, ml.TimedEvents.IdleHandlers.Count); + Assert.Equal (2, ml.TimedEvents.Timeouts.Count); functionCalled = 0; - Assert.True (ml.TimedEvents.RemoveIdle (fn)); - Assert.Single (ml.TimedEvents.IdleHandlers); + Assert.True (ml.TimedEvents.Remove (a)); + Assert.Single (ml.TimedEvents.Timeouts); ml.RunIteration (); Assert.Equal (1, functionCalled); functionCalled = 0; - Assert.True (ml.TimedEvents.RemoveIdle (fn)); - Assert.Empty (ml.TimedEvents.IdleHandlers); + Assert.True (ml.TimedEvents.Remove (b)); + Assert.Empty (ml.TimedEvents.Timeouts); ml.RunIteration (); Assert.Equal (0, functionCalled); - Assert.False (ml.TimedEvents.RemoveIdle (fn)); + Assert.False (ml.TimedEvents.Remove (b)); } [Fact] @@ -189,8 +168,8 @@ public void AddThenRemoveIdle_Function_NotCalled () return true; }; - ml.AddIdle (fn); - Assert.True (ml.TimedEvents.RemoveIdle (fn)); + var a = ml.TimedEvents.Add (TimeSpan.Zero, fn); + Assert.True (ml.TimedEvents.Remove (a)); ml.RunIteration (); Assert.Equal (0, functionCalled); } @@ -211,13 +190,13 @@ public void AddTimer_Adds_Removes_NoFaults () return true; }; - object token = ml.TimedEvents.AddTimeout (TimeSpan.FromMilliseconds (ms), callback); + object token = ml.TimedEvents.Add (TimeSpan.FromMilliseconds (ms), callback); - Assert.True (ml.TimedEvents.RemoveTimeout (token)); + Assert.True (ml.TimedEvents.Remove (token)); // BUGBUG: This should probably fault? // Must return a boolean. - Assert.False (ml.TimedEvents.RemoveTimeout (token)); + Assert.False (ml.TimedEvents.Remove (token)); } [Fact] @@ -241,8 +220,8 @@ public async Task AddTimer_Duplicate_Keys_Not_Allowed () return true; }; - var task1 = new Task (() => token1 = ml.TimedEvents.AddTimeout (TimeSpan.FromMilliseconds (ms), callback)); - var task2 = new Task (() => token2 = ml.TimedEvents.AddTimeout (TimeSpan.FromMilliseconds (ms), callback)); + var task1 = new Task (() => token1 = ml.TimedEvents.Add (TimeSpan.FromMilliseconds (ms), callback)); + var task2 = new Task (() => token2 = ml.TimedEvents.Add (TimeSpan.FromMilliseconds (ms), callback)); Assert.Null (token1); Assert.Null (token2); task1.Start (); @@ -251,8 +230,8 @@ public async Task AddTimer_Duplicate_Keys_Not_Allowed () Assert.NotNull (token1); Assert.NotNull (token2); await Task.WhenAll (task1, task2); - Assert.True (ml.TimedEvents.RemoveTimeout (token1)); - Assert.True (ml.TimedEvents.RemoveTimeout (token2)); + Assert.True (ml.TimedEvents.Remove (token1)); + Assert.True (ml.TimedEvents.Remove (token2)); Assert.Equal (2, callbackCount); } @@ -278,13 +257,13 @@ public void AddTimer_EventFired () object sender = null; TimeoutEventArgs args = null; - ml.TimedEvents.TimeoutAdded += (s, e) => + ml.TimedEvents.Added += (s, e) => { sender = s; args = e; }; - object token = ml.TimedEvents.AddTimeout (TimeSpan.FromMilliseconds (ms), callback); + object token = ml.TimedEvents.Add (TimeSpan.FromMilliseconds (ms), callback); Assert.Same (ml.TimedEvents, sender); Assert.NotNull (args.Timeout); @@ -313,14 +292,14 @@ public void AddTimer_In_Parallel_Wont_Throw () }; Parallel.Invoke ( - () => token1 = ml.TimedEvents.AddTimeout (TimeSpan.FromMilliseconds (ms), callback), - () => token2 = ml.TimedEvents.AddTimeout (TimeSpan.FromMilliseconds (ms), callback) + () => token1 = ml.TimedEvents.Add (TimeSpan.FromMilliseconds (ms), callback), + () => token2 = ml.TimedEvents.Add (TimeSpan.FromMilliseconds (ms), callback) ); ml.Run (); Assert.NotNull (token1); Assert.NotNull (token2); - Assert.True (ml.TimedEvents.RemoveTimeout (token1)); - Assert.True (ml.TimedEvents.RemoveTimeout (token2)); + Assert.True (ml.TimedEvents.Remove (token1)); + Assert.True (ml.TimedEvents.Remove (token2)); Assert.Equal (2, callbackCount); } @@ -345,7 +324,7 @@ public void AddTimer_Remove_NotCalled () return true; }; - ml.AddIdle (fnStop); + ml.TimedEvents.Add (TimeSpan.Zero, fnStop); var callbackCount = 0; @@ -356,8 +335,8 @@ public void AddTimer_Remove_NotCalled () return true; }; - object token = ml.TimedEvents.AddTimeout (ms, callback); - Assert.True (ml.TimedEvents.RemoveTimeout (token)); + object token = ml.TimedEvents.Add (ms, callback); + Assert.True (ml.TimedEvents.Remove (token)); ml.Run (); Assert.Equal (0, callbackCount); } @@ -383,7 +362,7 @@ public void AddTimer_ReturnFalse_StopsBeingCalled () return true; }; - ml.AddIdle (fnStop); + ml.TimedEvents.Add (TimeSpan.Zero, fnStop); var callbackCount = 0; @@ -394,11 +373,11 @@ public void AddTimer_ReturnFalse_StopsBeingCalled () return false; }; - object token = ml.TimedEvents.AddTimeout (ms, callback); + object token = ml.TimedEvents.Add (ms, callback); ml.Run (); Assert.Equal (1, callbackCount); Assert.Equal (10, stopCount); - Assert.False (ml.TimedEvents.RemoveTimeout (token)); + Assert.False (ml.TimedEvents.Remove (token)); } [Fact] @@ -417,9 +396,9 @@ public void AddTimer_Run_Called () return true; }; - object token = ml.TimedEvents.AddTimeout (TimeSpan.FromMilliseconds (ms), callback); + object token = ml.TimedEvents.Add (TimeSpan.FromMilliseconds (ms), callback); ml.Run (); - Assert.True (ml.TimedEvents.RemoveTimeout (token)); + Assert.True (ml.TimedEvents.Remove (token)); Assert.Equal (1, callbackCount); } @@ -442,7 +421,7 @@ public void AddTimer_Run_CalledAtApproximatelyRightTime () return true; }; - object token = ml.TimedEvents.AddTimeout (ms, callback); + object token = ml.TimedEvents.Add (ms, callback); watch.Start (); ml.Run (); @@ -450,7 +429,7 @@ public void AddTimer_Run_CalledAtApproximatelyRightTime () // https://github.com/xunit/assert.xunit/pull/25 Assert.Equal (ms * callbackCount, watch.Elapsed, new MillisecondTolerance (100)); - Assert.True (ml.TimedEvents.RemoveTimeout (token)); + Assert.True (ml.TimedEvents.Remove (token)); Assert.Equal (1, callbackCount); } @@ -476,7 +455,7 @@ public void AddTimer_Run_CalledTwiceApproximatelyRightTime () return true; }; - object token = ml.TimedEvents.AddTimeout (ms, callback); + object token = ml.TimedEvents.Add (ms, callback); watch.Start (); ml.Run (); @@ -484,7 +463,7 @@ public void AddTimer_Run_CalledTwiceApproximatelyRightTime () // https://github.com/xunit/assert.xunit/pull/25 Assert.Equal (ms * callbackCount, watch.Elapsed, new MillisecondTolerance (100)); - Assert.True (ml.TimedEvents.RemoveTimeout (token)); + Assert.True (ml.TimedEvents.Remove (token)); Assert.Equal (2, callbackCount); } @@ -492,7 +471,7 @@ public void AddTimer_Run_CalledTwiceApproximatelyRightTime () public void CheckTimersAndIdleHandlers_NoTimers_Returns_False () { var ml = new MainLoop (new FakeMainLoop ()); - bool retVal = ml.TimedEvents.CheckTimersAndIdleHandlers (out int waitTimeOut); + bool retVal = ml.TimedEvents.CheckTimers(out int waitTimeOut); Assert.False (retVal); Assert.Equal (-1, waitTimeOut); } @@ -503,10 +482,10 @@ public void CheckTimersAndIdleHandlers_NoTimers_WithIdle_Returns_True () var ml = new MainLoop (new FakeMainLoop ()); Func fnTrue = () => true; - ml.AddIdle (fnTrue); - bool retVal = ml.TimedEvents.CheckTimersAndIdleHandlers (out int waitTimeOut); + ml.TimedEvents.Add (TimeSpan.Zero, fnTrue); + bool retVal = ml.TimedEvents.CheckTimers(out int waitTimeOut); Assert.True (retVal); - Assert.Equal (-1, waitTimeOut); + Assert.Equal (0, waitTimeOut); } [Fact] @@ -517,8 +496,8 @@ public void CheckTimersAndIdleHandlers_With1Timer_Returns_Timer () static bool Callback () { return false; } - _ = ml.TimedEvents.AddTimeout (ms, Callback); - bool retVal = ml.TimedEvents.CheckTimersAndIdleHandlers (out int waitTimeOut); + _ = ml.TimedEvents.Add (ms, Callback); + bool retVal = ml.TimedEvents.CheckTimers (out int waitTimeOut); Assert.True (retVal); @@ -534,9 +513,9 @@ public void CheckTimersAndIdleHandlers_With2Timers_Returns_Timer () static bool Callback () { return false; } - _ = ml.TimedEvents.AddTimeout (ms, Callback); - _ = ml.TimedEvents.AddTimeout (ms, Callback); - bool retVal = ml.TimedEvents.CheckTimersAndIdleHandlers (out int waitTimeOut); + _ = ml.TimedEvents.Add (ms, Callback); + _ = ml.TimedEvents.Add (ms, Callback); + bool retVal = ml.TimedEvents.CheckTimers (out int waitTimeOut); Assert.True (retVal); @@ -578,11 +557,11 @@ public void False_Idle_Stops_It_Being_Called_Again () return true; }; - ml.AddIdle (fnStop); - ml.AddIdle (fn1); + var a = ml.TimedEvents.Add (TimeSpan.Zero, fnStop); + var b = ml.TimedEvents.Add (TimeSpan.Zero, fn1); ml.Run (); - Assert.True (ml.TimedEvents.RemoveIdle (fnStop)); - Assert.False (ml.TimedEvents.RemoveIdle (fn1)); + Assert.True (ml.TimedEvents.Remove (a)); + Assert.False (ml.TimedEvents.Remove (a)); Assert.Equal (10, functionCalled); Assert.Equal (20, stopCount); @@ -594,7 +573,6 @@ public void Internal_Tests () var testMainloop = new TestMainloop (); var mainloop = new MainLoop (testMainloop); Assert.Empty (mainloop.TimedEvents.Timeouts); - Assert.Empty (mainloop.TimedEvents.IdleHandlers); Assert.NotNull ( new App.Timeout { Span = new (), Callback = () => true } @@ -602,8 +580,8 @@ public void Internal_Tests () } [Theory] - [MemberData (nameof (TestAddIdle))] - public void Mainloop_Invoke_Or_AddIdle_Can_Be_Used_For_Events_Or_Actions ( + [MemberData (nameof (TestAddTimeout))] + public void Mainloop_Invoke_Or_AddTimeout_Can_Be_Used_For_Events_Or_Actions ( Action action, string pclickMe, string pcancel, @@ -683,7 +661,7 @@ int pfour Application.Shutdown (); } - + [Fact] public void RemoveIdle_Function_NotCalled () { @@ -698,7 +676,7 @@ public void RemoveIdle_Function_NotCalled () return true; }; - Assert.False (ml.TimedEvents.RemoveIdle (fn)); + Assert.False (ml.TimedEvents.Remove ("flibble")); ml.RunIteration (); Assert.Equal (0, functionCalled); } @@ -722,14 +700,14 @@ public void Run_Runs_Idle_Stop_Stops_Idle () return true; }; - ml.AddIdle (fn); + var a = ml.TimedEvents.Add (TimeSpan.Zero, fn); ml.Run (); - Assert.True (ml.TimedEvents.RemoveIdle (fn)); + Assert.True (ml.TimedEvents.Remove (a)); Assert.Equal (10, functionCalled); } - public static IEnumerable TestAddIdle + public static IEnumerable TestAddTimeout { get { diff --git a/Tests/UnitTests/Application/Mouse/ApplicationMouseTests.cs b/Tests/UnitTests/Application/Mouse/ApplicationMouseTests.cs index 3ae825d94d..6851cb6384 100644 --- a/Tests/UnitTests/Application/Mouse/ApplicationMouseTests.cs +++ b/Tests/UnitTests/Application/Mouse/ApplicationMouseTests.cs @@ -260,39 +260,39 @@ public void MouseGrabView_WithNullMouseEventView () // if (iterations == 0) // { // Assert.True (tf.HasFocus); - // Assert.Null (Application.MouseGrabView); + // Assert.Null (Application.MouseGrabHandler.MouseGrabView); // Application.RaiseMouseEvent (new () { ScreenPosition = new (5, 5), Flags = MouseFlags.ReportMousePosition }); - // Assert.Equal (sv, Application.MouseGrabView); + // Assert.Equal (sv, Application.MouseGrabHandler.MouseGrabView); // MessageBox.Query ("Title", "Test", "Ok"); - // Assert.Null (Application.MouseGrabView); + // Assert.Null (Application.MouseGrabHandler.MouseGrabView); // } // else if (iterations == 1) // { - // // Application.MouseGrabView is null because + // // Application.MouseGrabHandler.MouseGrabView is null because // // another toplevel (Dialog) was opened - // Assert.Null (Application.MouseGrabView); + // Assert.Null (Application.MouseGrabHandler.MouseGrabView); // Application.RaiseMouseEvent (new () { ScreenPosition = new (5, 5), Flags = MouseFlags.ReportMousePosition }); - // Assert.Null (Application.MouseGrabView); + // Assert.Null (Application.MouseGrabHandler.MouseGrabView); // Application.RaiseMouseEvent (new () { ScreenPosition = new (40, 12), Flags = MouseFlags.ReportMousePosition }); - // Assert.Null (Application.MouseGrabView); + // Assert.Null (Application.MouseGrabHandler.MouseGrabView); // Application.RaiseMouseEvent (new () { ScreenPosition = new (0, 0), Flags = MouseFlags.Button1Pressed }); - // Assert.Null (Application.MouseGrabView); + // Assert.Null (Application.MouseGrabHandler.MouseGrabView); // Application.RequestStop (); // } // else if (iterations == 2) // { - // Assert.Null (Application.MouseGrabView); + // Assert.Null (Application.MouseGrabHandler.MouseGrabView); // Application.RequestStop (); // } @@ -313,33 +313,33 @@ public void MouseGrabView_GrabbedMouse_UnGrabbedMouse () var view2 = new View { Id = "view2" }; var view3 = new View { Id = "view3" }; - Application.GrabbedMouse += Application_GrabbedMouse; - Application.UnGrabbedMouse += Application_UnGrabbedMouse; + Application.MouseGrabHandler.GrabbedMouse += Application_GrabbedMouse; + Application.MouseGrabHandler.UnGrabbedMouse += Application_UnGrabbedMouse; - Application.GrabMouse (view1); + Application.MouseGrabHandler.GrabMouse (view1); Assert.Equal (0, count); Assert.Equal (grabView, view1); - Assert.Equal (view1, Application.MouseGrabView); + Assert.Equal (view1, Application.MouseGrabHandler.MouseGrabView); - Application.UngrabMouse (); + Application.MouseGrabHandler.UngrabMouse (); Assert.Equal (1, count); Assert.Equal (grabView, view1); - Assert.Null (Application.MouseGrabView); + Assert.Null (Application.MouseGrabHandler.MouseGrabView); - Application.GrabbedMouse += Application_GrabbedMouse; - Application.UnGrabbedMouse += Application_UnGrabbedMouse; + Application.MouseGrabHandler.GrabbedMouse += Application_GrabbedMouse; + Application.MouseGrabHandler.UnGrabbedMouse += Application_UnGrabbedMouse; - Application.GrabMouse (view2); + Application.MouseGrabHandler.GrabMouse (view2); Assert.Equal (1, count); Assert.Equal (grabView, view2); - Assert.Equal (view2, Application.MouseGrabView); + Assert.Equal (view2, Application.MouseGrabHandler.MouseGrabView); - Application.UngrabMouse (); + Application.MouseGrabHandler.UngrabMouse (); Assert.Equal (2, count); Assert.Equal (grabView, view2); - Assert.Equal (view3, Application.MouseGrabView); - Application.UngrabMouse (); - Assert.Null (Application.MouseGrabView); + Assert.Equal (view3, Application.MouseGrabHandler.MouseGrabView); + Application.MouseGrabHandler.UngrabMouse (); + Assert.Null (Application.MouseGrabHandler.MouseGrabView); void Application_GrabbedMouse (object sender, ViewEventArgs e) { @@ -354,7 +354,7 @@ void Application_GrabbedMouse (object sender, ViewEventArgs e) grabView = view2; } - Application.GrabbedMouse -= Application_GrabbedMouse; + Application.MouseGrabHandler.GrabbedMouse -= Application_GrabbedMouse; } void Application_UnGrabbedMouse (object sender, ViewEventArgs e) @@ -375,10 +375,10 @@ void Application_UnGrabbedMouse (object sender, ViewEventArgs e) if (count > 1) { // It's possible to grab another view after the previous was ungrabbed - Application.GrabMouse (view3); + Application.MouseGrabHandler.GrabMouse (view3); } - Application.UnGrabbedMouse -= Application_UnGrabbedMouse; + Application.MouseGrabHandler.UnGrabbedMouse -= Application_UnGrabbedMouse; } } @@ -393,18 +393,18 @@ public void View_Is_Responsible_For_Calling_UnGrabMouse_Before_Being_Disposed () top.Add (view); Application.Begin (top); - Assert.Null (Application.MouseGrabView); - Application.GrabMouse (view); - Assert.Equal (view, Application.MouseGrabView); + Assert.Null (Application.MouseGrabHandler.MouseGrabView); + Application.MouseGrabHandler.GrabMouse (view); + Assert.Equal (view, Application.MouseGrabHandler.MouseGrabView); top.Remove (view); - Application.UngrabMouse (); + Application.MouseGrabHandler.UngrabMouse (); view.Dispose (); #if DEBUG_IDISPOSABLE Assert.True (view.WasDisposed); #endif Application.RaiseMouseEvent (new () { ScreenPosition = new (0, 0), Flags = MouseFlags.Button1Pressed }); - Assert.Null (Application.MouseGrabView); + Assert.Null (Application.MouseGrabHandler.MouseGrabView); Assert.Equal (0, count); top.Dispose (); } diff --git a/Tests/UnitTests/ConsoleDrivers/MainLoopDriverTests.cs b/Tests/UnitTests/ConsoleDrivers/MainLoopDriverTests.cs index 228d9e5726..2648ef2688 100644 --- a/Tests/UnitTests/ConsoleDrivers/MainLoopDriverTests.cs +++ b/Tests/UnitTests/ConsoleDrivers/MainLoopDriverTests.cs @@ -15,7 +15,7 @@ public class MainLoopDriverTests [InlineData (typeof (WindowsDriver), typeof (WindowsMainLoop))] //[InlineData (typeof (ANSIDriver), typeof (AnsiMainLoopDriver))] - public void MainLoop_AddIdle_ValidIdleHandler_ReturnsToken (Type driverType, Type mainLoopDriverType) + public void MainLoop_AddTimeout_ValidIdleHandler_ReturnsToken (Type driverType, Type mainLoopDriverType) { var driver = (IConsoleDriver)Activator.CreateInstance (driverType); var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver); @@ -29,7 +29,7 @@ bool IdleHandler () return false; } - Func token = mainLoop.AddIdle (IdleHandler); + var token = mainLoop.TimedEvents.Add(TimeSpan.Zero, IdleHandler); Assert.NotNull (token); Assert.False (idleHandlerInvoked); // Idle handler should not be invoked immediately @@ -52,7 +52,7 @@ public void MainLoop_AddTimeout_ValidParameters_ReturnsToken (Type driverType, T var mainLoop = new MainLoop (mainLoopDriver); var callbackInvoked = false; - object token = mainLoop.TimedEvents.AddTimeout ( + object token = mainLoop.TimedEvents.Add ( TimeSpan.FromMilliseconds (100), () => { @@ -87,11 +87,11 @@ Type mainLoopDriverType var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver); var mainLoop = new MainLoop (mainLoopDriver); - mainLoop.AddIdle (() => false); - bool result = mainLoop.TimedEvents.CheckTimersAndIdleHandlers (out int waitTimeout); + mainLoop.TimedEvents.Add (TimeSpan.Zero, () => false); + bool result = mainLoop.TimedEvents.CheckTimers (out int waitTimeout); Assert.True (result); - Assert.Equal (-1, waitTimeout); + Assert.Equal (0, waitTimeout); mainLoop.Dispose (); } @@ -102,7 +102,7 @@ Type mainLoopDriverType [InlineData (typeof (WindowsDriver), typeof (WindowsMainLoop))] //[InlineData (typeof (ANSIDriver), typeof (AnsiMainLoopDriver))] - public void MainLoop_CheckTimersAndIdleHandlers_NoTimersOrIdleHandlers_ReturnsFalse ( + public void MainLoop_CheckTimers_NoTimersOrIdleHandlers_ReturnsFalse ( Type driverType, Type mainLoopDriverType ) @@ -111,7 +111,7 @@ Type mainLoopDriverType var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver); var mainLoop = new MainLoop (mainLoopDriver); - bool result = mainLoop.TimedEvents.CheckTimersAndIdleHandlers (out int waitTimeout); + bool result = mainLoop.TimedEvents.CheckTimers (out int waitTimeout); Assert.False (result); Assert.Equal (-1, waitTimeout); @@ -134,8 +134,8 @@ Type mainLoopDriverType var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver); var mainLoop = new MainLoop (mainLoopDriver); - mainLoop.TimedEvents.AddTimeout (TimeSpan.FromMilliseconds (100), () => false); - bool result = mainLoop.TimedEvents.CheckTimersAndIdleHandlers (out int waitTimeout); + mainLoop.TimedEvents.Add (TimeSpan.FromMilliseconds (100), () => false); + bool result = mainLoop.TimedEvents.CheckTimers(out int waitTimeout); Assert.True (result); Assert.True (waitTimeout >= 0); @@ -158,7 +158,6 @@ public void MainLoop_Constructs_Disposes (Type driverType, Type mainLoopDriverTy // Check default values Assert.NotNull (mainLoop); Assert.Equal (mainLoopDriver, mainLoop.MainLoopDriver); - Assert.Empty (mainLoop.TimedEvents.IdleHandlers); Assert.Empty (mainLoop.TimedEvents.Timeouts); Assert.False (mainLoop.Running); @@ -168,7 +167,6 @@ public void MainLoop_Constructs_Disposes (Type driverType, Type mainLoopDriverTy // TODO: It'd be nice if we could really verify IMainLoopDriver.TearDown was called // and that it was actually cleaned up. Assert.Null (mainLoop.MainLoopDriver); - Assert.Empty (mainLoop.TimedEvents.IdleHandlers); Assert.Empty (mainLoop.TimedEvents.Timeouts); Assert.False (mainLoop.Running); } @@ -186,7 +184,7 @@ public void MainLoop_RemoveIdle_InvalidToken_ReturnsFalse (Type driverType, Type var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver); var mainLoop = new MainLoop (mainLoopDriver); - bool result = mainLoop.TimedEvents.RemoveIdle (() => false); + bool result = mainLoop.TimedEvents.Remove("flibble"); Assert.False (result); mainLoop.Dispose (); @@ -207,8 +205,9 @@ public void MainLoop_RemoveIdle_ValidToken_ReturnsTrue (Type driverType, Type ma bool IdleHandler () { return false; } - Func token = mainLoop.AddIdle (IdleHandler); - bool result = mainLoop.TimedEvents.RemoveIdle (token); + + var token = mainLoop.TimedEvents.Add (TimeSpan.Zero, IdleHandler); + bool result = mainLoop.TimedEvents.Remove (token); Assert.True (result); mainLoop.Dispose (); @@ -227,7 +226,7 @@ public void MainLoop_RemoveTimeout_InvalidToken_ReturnsFalse (Type driverType, T var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver); var mainLoop = new MainLoop (mainLoopDriver); - bool result = mainLoop.TimedEvents.RemoveTimeout (new object ()); + bool result = mainLoop.TimedEvents.Remove (new object ()); Assert.False (result); } @@ -245,8 +244,8 @@ public void MainLoop_RemoveTimeout_ValidToken_ReturnsTrue (Type driverType, Type var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver); var mainLoop = new MainLoop (mainLoopDriver); - object token = mainLoop.TimedEvents.AddTimeout (TimeSpan.FromMilliseconds (100), () => false); - bool result = mainLoop.TimedEvents.RemoveTimeout (token); + object token = mainLoop.TimedEvents.Add (TimeSpan.FromMilliseconds (100), () => false); + bool result = mainLoop.TimedEvents.Remove (token); Assert.True (result); mainLoop.Dispose (); @@ -273,7 +272,7 @@ public void MainLoop_RunIteration_ValidIdleHandler_CallsIdleHandler (Type driver return false; }; - mainLoop.AddIdle (idleHandler); + mainLoop.TimedEvents.Add (TimeSpan.Zero, idleHandler); mainLoop.RunIteration (); // Run an iteration to process the idle handler Assert.True (idleHandlerInvoked); diff --git a/Tests/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs b/Tests/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs index 0a34417fac..d0165a45ae 100644 --- a/Tests/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs +++ b/Tests/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs @@ -401,7 +401,7 @@ public void InitRunShutdown_Generic_IdleForExit () v2.Init (); - v2.AddIdle (IdleExit); + v2.AddTimeout (TimeSpan.Zero, IdleExit); Assert.Null (Application.Top); // Blocks until the timeout call is hit @@ -448,7 +448,7 @@ public void Shutdown_Closing_Closed_Raised () Assert.Same (t, a.Toplevel); }; - v2.AddIdle (IdleExit); + v2.AddTimeout(TimeSpan.Zero, IdleExit); // Blocks until the timeout call is hit diff --git a/Tests/UnitTests/Input/EscSeqUtilsTests.cs b/Tests/UnitTests/Input/EscSeqUtilsTests.cs index 73ec0998ea..6daf4bd1ec 100644 --- a/Tests/UnitTests/Input/EscSeqUtilsTests.cs +++ b/Tests/UnitTests/Input/EscSeqUtilsTests.cs @@ -679,14 +679,7 @@ public void DecodeEscSeq_Multiple_Tests () Assert.Equal (new () { MouseFlags.Button1TripleClicked }, _mouseFlags); Assert.Equal (new (1, 2), _pos); Assert.False (_isResponse); - - var view = new View { Width = Dim.Fill (), Height = Dim.Fill (), WantContinuousButtonPressed = true }; - var top = new Toplevel (); - top.Add (view); - Application.Begin (top); - - Application.RaiseMouseEvent (new () { Position = new (0, 0), Flags = 0 }); - + ClearAll (); _cki = new ConsoleKeyInfo [] @@ -734,26 +727,8 @@ public void DecodeEscSeq_Multiple_Tests () Assert.Equal (new (1, 2), _pos); Assert.False (_isResponse); - Application.Iteration += (s, a) => - { - if (_actionStarted) - { - // set Application.WantContinuousButtonPressedView to null - view.WantContinuousButtonPressed = false; - - Application.RaiseMouseEvent (new () { Position = new (0, 0), Flags = 0 }); - - Application.RequestStop (); - } - }; - - Application.Run (top); - top.Dispose (); - - Assert.Null (Application.WantContinuousButtonPressedView); - - Assert.Equal (MouseFlags.Button1Pressed, _arg1); - Assert.Equal (new (1, 2), _arg2); + Assert.Equal (MouseFlags.None, _arg1); + Assert.Equal (new (0, 0), _arg2); ClearAll (); diff --git a/Tests/UnitTests/View/Adornment/ShadowStyleTests.cs b/Tests/UnitTests/View/Adornment/ShadowStyleTests.cs index 6866ffe473..6d45ac9b75 100644 --- a/Tests/UnitTests/View/Adornment/ShadowStyleTests.cs +++ b/Tests/UnitTests/View/Adornment/ShadowStyleTests.cs @@ -160,7 +160,7 @@ public void ShadowStyle_Button1Pressed_Causes_Movement (ShadowStyle style, int e view.NewMouseEvent (new () { Flags = MouseFlags.Button1Released, Position = new (0, 0) }); Assert.Equal (origThickness, view.Margin.Thickness); - // Button1Pressed, Button1Released cause Application.MouseGrabView to be set + // Button1Pressed, Button1Released cause Application.MouseGrabHandler.MouseGrabView to be set Application.ResetState (true); } } diff --git a/Tests/UnitTests/View/Mouse/MouseTests.cs b/Tests/UnitTests/View/Mouse/MouseTests.cs index f32e208f61..6d8a68eab3 100644 --- a/Tests/UnitTests/View/Mouse/MouseTests.cs +++ b/Tests/UnitTests/View/Mouse/MouseTests.cs @@ -1,4 +1,5 @@ -using UnitTests; +using Moq; +using UnitTests; namespace Terminal.Gui.ViewMouseTests; @@ -95,7 +96,7 @@ public void WantContinuousButtonPressed_False_Button_Press_Release_DoesNotClick view.Dispose (); - // Button1Pressed, Button1Released cause Application.MouseGrabView to be set + // Button1Pressed, Button1Released cause Application.MouseGrabHandler.MouseGrabView to be set Application.ResetState (true); } @@ -125,7 +126,7 @@ public void WantContinuousButtonPressed_True_Button_Clicked_Raises_MouseClick (M view.Dispose (); - // Button1Pressed, Button1Released cause Application.MouseGrabView to be set + // Button1Pressed, Button1Released cause Application.MouseGrabHandler.MouseGrabView to be set Application.ResetState (true); } @@ -155,7 +156,7 @@ public void WantContinuousButtonPressed_True_Button_Clicked_Raises_Selecting (Mo view.Dispose (); - // Button1Pressed, Button1Released cause Application.MouseGrabView to be set + // Button1Pressed, Button1Released cause Application.MouseGrabHandler.MouseGrabView to be set Application.ResetState (true); } @@ -166,7 +167,6 @@ public void WantContinuousButtonPressed_True_Button_Clicked_Raises_Selecting (Mo [InlineData (MouseFlags.Button4Pressed, MouseFlags.Button4Released)] public void WantContinuousButtonPressed_True_And_WantMousePositionReports_True_Button_Press_Release_Clicks (MouseFlags pressed, MouseFlags released) { - Application.Init (new FakeDriver ()); var me = new MouseEventArgs (); var view = new View @@ -177,28 +177,43 @@ public void WantContinuousButtonPressed_True_And_WantMousePositionReports_True_B WantMousePositionReports = true }; + // Setup components for mouse held down + var timed = new TimedEvents (); + var grab = new MouseGrabHandler (); + view.MouseHeldDown = new MouseHeldDown (view, timed, grab); + + // Register callback for what to do when the mouse is held down var clickedCount = 0; + view.MouseHeldDown.MouseIsHeldDownTick += (_, _) => clickedCount++; - view.MouseClick += (s, e) => clickedCount++; + // Mouse is currently not held down so should be no timers running + Assert.Empty(timed.Timeouts); + // When mouse is held down me.Flags = pressed; view.NewMouseEvent (me); Assert.Equal (0, clickedCount); me.Handled = false; - me.Flags = pressed; - view.NewMouseEvent (me); + // A timer should begin + var t = Assert.Single (timed.Timeouts); + + // Invoke the timer + t.Value.Callback.Invoke (); + + // Event should have been raised Assert.Equal (1, clickedCount); - me.Handled = false; + Assert.NotEmpty(timed.Timeouts); + // When mouse is released me.Flags = released; view.NewMouseEvent (me); + + // timer should stop + Assert.Empty (timed.Timeouts); Assert.Equal (1, clickedCount); view.Dispose (); - - // Button1Pressed, Button1Released cause Application.MouseGrabView to be set - Application.ResetState (true); } [Theory] @@ -212,7 +227,6 @@ public void WantContinuousButtonPressed_True_And_WantMousePositionReports_True_B MouseFlags clicked ) { - Application.Init (new FakeDriver ()); var me = new MouseEventArgs (); var view = new View @@ -223,39 +237,49 @@ MouseFlags clicked WantMousePositionReports = true }; + // Setup components for mouse held down + var timed = new TimedEvents (); + var grab = new MouseGrabHandler (); + view.MouseHeldDown = new MouseHeldDown (view, timed, grab); + + // Register callback for what to do when the mouse is held down var clickedCount = 0; + view.MouseHeldDown.MouseIsHeldDownTick += (_, _) => clickedCount++; - view.MouseClick += (s, e) => clickedCount++; + Assert.Empty (timed.Timeouts); me.Flags = pressed; view.NewMouseEvent (me); Assert.Equal (0, clickedCount); me.Handled = false; + Assert.NotEmpty(timed.Timeouts); + Assert.Single (timed.Timeouts).Value.Callback.Invoke (); + me.Flags = pressed; view.NewMouseEvent (me); Assert.Equal (1, clickedCount); me.Handled = false; + Assert.NotEmpty (timed.Timeouts); + me.Flags = released; view.NewMouseEvent (me); Assert.Equal (1, clickedCount); me.Handled = false; + Assert.Empty (timed.Timeouts); + me.Flags = clicked; view.NewMouseEvent (me); Assert.Equal (1, clickedCount); view.Dispose (); - - // Button1Pressed, Button1Released cause Application.MouseGrabView to be set - Application.ResetState (true); } [Fact] public void WantContinuousButtonPressed_True_And_WantMousePositionReports_True_Move_InViewport_OutOfViewport_Keeps_Counting () { - Application.Init (new FakeDriver ()); var me = new MouseEventArgs (); var view = new View @@ -266,9 +290,14 @@ public void WantContinuousButtonPressed_True_And_WantMousePositionReports_True_M WantMousePositionReports = true }; - var clickedCount = 0; + // Setup components for mouse held down + var timed = new TimedEvents (); + var grab = new MouseGrabHandler (); + view.MouseHeldDown = new MouseHeldDown (view, timed, grab); - view.MouseClick += (s, e) => clickedCount++; + // Register callback for what to do when the mouse is held down + var clickedCount = 0; + view.MouseHeldDown.MouseIsHeldDownTick += (_, _) => clickedCount++; // Start in Viewport me.Flags = MouseFlags.Button1Pressed; @@ -277,17 +306,30 @@ public void WantContinuousButtonPressed_True_And_WantMousePositionReports_True_M Assert.Equal (0, clickedCount); me.Handled = false; + // Mouse is held down so timer should be ticking + Assert.NotEmpty (timed.Timeouts); + Assert.Equal (clickedCount,0); + + // Don't wait, just force it to expire + Assert.Single (timed.Timeouts).Value.Callback.Invoke (); + Assert.Equal (clickedCount, 1); + // Move out of Viewport me.Flags = MouseFlags.Button1Pressed; me.Position = me.Position with { X = 1 }; view.NewMouseEvent (me); - Assert.Equal (1, clickedCount); + + Assert.Single (timed.Timeouts).Value.Callback.Invoke (); + Assert.Equal (clickedCount, 2); + me.Handled = false; // Move into Viewport me.Flags = MouseFlags.Button1Pressed; me.Position = me.Position with { X = 0 }; view.NewMouseEvent (me); + + Assert.NotEmpty (timed.Timeouts); Assert.Equal (2, clickedCount); me.Handled = false; @@ -295,13 +337,13 @@ public void WantContinuousButtonPressed_True_And_WantMousePositionReports_True_M me.Flags = MouseFlags.Button1Pressed; me.Position = me.Position with { X = 0 }; view.NewMouseEvent (me); + + Assert.Single (timed.Timeouts).Value.Callback.Invoke (); + Assert.Equal (3, clickedCount); me.Handled = false; view.Dispose (); - - // Button1Pressed, Button1Released cause Application.MouseGrabView to be set - Application.ResetState (true); } //[Theory] @@ -335,7 +377,7 @@ public void WantContinuousButtonPressed_True_And_WantMousePositionReports_True_M // testView.Dispose (); - // // Button1Pressed, Button1Released cause Application.MouseGrabView to be set + // // Button1Pressed, Button1Released cause Application.MouseGrabHandler.MouseGrabView to be set // Application.ResetState (true); //} @@ -400,7 +442,7 @@ public void MouseState_None_Button1_Pressed_Move_No_Changes (int x) testView.Dispose (); - // Button1Pressed, Button1Released cause Application.MouseGrabView to be set + // Button1Pressed, Button1Released cause Application.MouseGrabHandler.MouseGrabView to be set Application.ResetState (true); } @@ -462,7 +504,7 @@ public void MouseState_Pressed_Button1_Pressed_Move_Keeps_Pressed (int x) testView.Dispose (); - // Button1Pressed, Button1Released cause Application.MouseGrabView to be set + // Button1Pressed, Button1Released cause Application.MouseGrabHandler.MouseGrabView to be set Application.ResetState (true); } @@ -525,7 +567,7 @@ public void MouseState_PressedOutside_Button1_Pressed_Move_Raises_PressedOutside testView.Dispose (); - // Button1Pressed, Button1Released cause Application.MouseGrabView to be set + // Button1Pressed, Button1Released cause Application.MouseGrabHandler.MouseGrabView to be set Application.ResetState (true); } @@ -589,7 +631,7 @@ public void MouseState_PressedOutside_Button1_Pressed_Move_Raises_PressedOutside testView.Dispose (); - // Button1Pressed, Button1Released cause Application.MouseGrabView to be set + // Button1Pressed, Button1Released cause Application.MouseGrabHandler.MouseGrabView to be set Application.ResetState (true); } private class MouseEventTestView : View diff --git a/Tests/UnitTests/Views/Menuv1/MenuBarv1Tests.cs b/Tests/UnitTests/Views/Menuv1/MenuBarv1Tests.cs index acc93a2197..087823380e 100644 --- a/Tests/UnitTests/Views/Menuv1/MenuBarv1Tests.cs +++ b/Tests/UnitTests/Views/Menuv1/MenuBarv1Tests.cs @@ -2571,11 +2571,11 @@ public void MouseEvent_Test () if (i is < 0 or > 0) { - Assert.Equal (menu, Application.MouseGrabView); + Assert.Equal (menu, Application.MouseGrabHandler.MouseGrabView); } else { - Assert.Equal (menuBar, Application.MouseGrabView); + Assert.Equal (menuBar, Application.MouseGrabHandler.MouseGrabView); } Assert.Equal ("_Edit", miCurrent.Parent.Title); diff --git a/Tests/UnitTests/Views/SpinnerViewTests.cs b/Tests/UnitTests/Views/SpinnerViewTests.cs index 24dc88a422..6382f82971 100644 --- a/Tests/UnitTests/Views/SpinnerViewTests.cs +++ b/Tests/UnitTests/Views/SpinnerViewTests.cs @@ -106,6 +106,9 @@ private SpinnerView GetSpinnerView () top.Add (view); Application.Begin (top); + // Required to clear the initial 'Invoke nothing' that Begin does + Application.MainLoop.TimedEvents.Timeouts.Clear (); + Assert.Equal (1, view.Frame.Width); Assert.Equal (1, view.Frame.Height); diff --git a/Tests/UnitTests/Views/ToplevelTests.cs b/Tests/UnitTests/Views/ToplevelTests.cs index 862ae44f70..78b2ba3a0b 100644 --- a/Tests/UnitTests/Views/ToplevelTests.cs +++ b/Tests/UnitTests/Views/ToplevelTests.cs @@ -305,17 +305,17 @@ public void Mouse_Drag_On_Top_With_Superview_Null () } else if (iterations == 2) { - Assert.Null (Application.MouseGrabView); + Assert.Null (Application.MouseGrabHandler.MouseGrabView); // Grab the mouse Application.RaiseMouseEvent (new () { ScreenPosition = new (3, 2), Flags = MouseFlags.Button1Pressed }); - Assert.Equal (Application.Top!.Border, Application.MouseGrabView); + Assert.Equal (Application.Top!.Border, Application.MouseGrabHandler.MouseGrabView); Assert.Equal (new (2, 2, 10, 3), Application.Top.Frame); } else if (iterations == 3) { - Assert.Equal (Application.Top!.Border, Application.MouseGrabView); + Assert.Equal (Application.Top!.Border, Application.MouseGrabHandler.MouseGrabView); // Drag to left Application.RaiseMouseEvent ( @@ -326,19 +326,19 @@ public void Mouse_Drag_On_Top_With_Superview_Null () }); Application.LayoutAndDraw (); - Assert.Equal (Application.Top.Border, Application.MouseGrabView); + Assert.Equal (Application.Top.Border, Application.MouseGrabHandler.MouseGrabView); Assert.Equal (new (1, 2, 10, 3), Application.Top.Frame); } else if (iterations == 4) { - Assert.Equal (Application.Top!.Border, Application.MouseGrabView); + Assert.Equal (Application.Top!.Border, Application.MouseGrabHandler.MouseGrabView); Assert.Equal (new (1, 2), Application.Top.Frame.Location); - Assert.Equal (Application.Top.Border, Application.MouseGrabView); + Assert.Equal (Application.Top.Border, Application.MouseGrabHandler.MouseGrabView); } else if (iterations == 5) { - Assert.Equal (Application.Top!.Border, Application.MouseGrabView); + Assert.Equal (Application.Top!.Border, Application.MouseGrabHandler.MouseGrabView); // Drag up Application.RaiseMouseEvent ( @@ -349,26 +349,26 @@ public void Mouse_Drag_On_Top_With_Superview_Null () }); Application.LayoutAndDraw (); - Assert.Equal (Application.Top!.Border, Application.MouseGrabView); + Assert.Equal (Application.Top!.Border, Application.MouseGrabHandler.MouseGrabView); Assert.Equal (new (1, 1, 10, 3), Application.Top.Frame); } else if (iterations == 6) { - Assert.Equal (Application.Top!.Border, Application.MouseGrabView); + Assert.Equal (Application.Top!.Border, Application.MouseGrabHandler.MouseGrabView); Assert.Equal (new (1, 1), Application.Top.Frame.Location); - Assert.Equal (Application.Top.Border, Application.MouseGrabView); + Assert.Equal (Application.Top.Border, Application.MouseGrabHandler.MouseGrabView); Assert.Equal (new (1, 1, 10, 3), Application.Top.Frame); } else if (iterations == 7) { - Assert.Equal (Application.Top!.Border, Application.MouseGrabView); + Assert.Equal (Application.Top!.Border, Application.MouseGrabHandler.MouseGrabView); // Ungrab the mouse Application.RaiseMouseEvent (new () { ScreenPosition = new (2, 1), Flags = MouseFlags.Button1Released }); Application.LayoutAndDraw (); - Assert.Null (Application.MouseGrabView); + Assert.Null (Application.MouseGrabHandler.MouseGrabView); } else if (iterations == 8) { @@ -411,7 +411,7 @@ public void Mouse_Drag_On_Top_With_Superview_Not_Null () { location = win.Frame; - Assert.Null (Application.MouseGrabView); + Assert.Null (Application.MouseGrabHandler.MouseGrabView); // Grab the mouse Application.RaiseMouseEvent ( @@ -420,11 +420,11 @@ public void Mouse_Drag_On_Top_With_Superview_Not_Null () ScreenPosition = new (win.Frame.X, win.Frame.Y), Flags = MouseFlags.Button1Pressed }); - Assert.Equal (win.Border, Application.MouseGrabView); + Assert.Equal (win.Border, Application.MouseGrabHandler.MouseGrabView); } else if (iterations == 2) { - Assert.Equal (win.Border, Application.MouseGrabView); + Assert.Equal (win.Border, Application.MouseGrabHandler.MouseGrabView); // Drag to left movex = 1; @@ -438,18 +438,18 @@ public void Mouse_Drag_On_Top_With_Superview_Not_Null () | MouseFlags.ReportMousePosition }); - Assert.Equal (win.Border, Application.MouseGrabView); + Assert.Equal (win.Border, Application.MouseGrabHandler.MouseGrabView); } else if (iterations == 3) { // we should have moved +1, +0 - Assert.Equal (win.Border, Application.MouseGrabView); - Assert.Equal (win.Border, Application.MouseGrabView); + Assert.Equal (win.Border, Application.MouseGrabHandler.MouseGrabView); + Assert.Equal (win.Border, Application.MouseGrabHandler.MouseGrabView); location.Offset (movex, movey); } else if (iterations == 4) { - Assert.Equal (win.Border, Application.MouseGrabView); + Assert.Equal (win.Border, Application.MouseGrabHandler.MouseGrabView); // Drag up movex = 0; @@ -463,18 +463,18 @@ public void Mouse_Drag_On_Top_With_Superview_Not_Null () | MouseFlags.ReportMousePosition }); - Assert.Equal (win.Border, Application.MouseGrabView); + Assert.Equal (win.Border, Application.MouseGrabHandler.MouseGrabView); } else if (iterations == 5) { // we should have moved +0, -1 - Assert.Equal (win.Border, Application.MouseGrabView); + Assert.Equal (win.Border, Application.MouseGrabHandler.MouseGrabView); location.Offset (movex, movey); Assert.Equal (location, win.Frame); } else if (iterations == 6) { - Assert.Equal (win.Border, Application.MouseGrabView); + Assert.Equal (win.Border, Application.MouseGrabHandler.MouseGrabView); // Ungrab the mouse movex = 0; @@ -487,7 +487,7 @@ public void Mouse_Drag_On_Top_With_Superview_Not_Null () Flags = MouseFlags.Button1Released }); - Assert.Null (Application.MouseGrabView); + Assert.Null (Application.MouseGrabHandler.MouseGrabView); } else if (iterations == 7) { @@ -602,11 +602,11 @@ public void Window_Viewport_Bigger_Than_Driver_Cols_And_Rows_Allow_Drag_Beyond_L Assert.Equal (new (0, 0, 40, 10), top.Frame); Assert.Equal (new (0, 0, 20, 3), window.Frame); - Assert.Null (Application.MouseGrabView); + Assert.Null (Application.MouseGrabHandler.MouseGrabView); Application.RaiseMouseEvent (new () { ScreenPosition = new (0, 0), Flags = MouseFlags.Button1Pressed }); - Assert.Equal (window.Border, Application.MouseGrabView); + Assert.Equal (window.Border, Application.MouseGrabHandler.MouseGrabView); Application.RaiseMouseEvent ( new () @@ -694,14 +694,14 @@ public void Modal_As_Top_Will_Drag_Cleanly () RunState rs = Application.Begin (window); - Assert.Null (Application.MouseGrabView); + Assert.Null (Application.MouseGrabHandler.MouseGrabView); Assert.Equal (new (0, 0, 10, 3), window.Frame); Application.RaiseMouseEvent (new () { ScreenPosition = new (0, 0), Flags = MouseFlags.Button1Pressed }); var firstIteration = false; Application.RunIteration (ref rs, firstIteration); - Assert.Equal (window.Border, Application.MouseGrabView); + Assert.Equal (window.Border, Application.MouseGrabHandler.MouseGrabView); Assert.Equal (new (0, 0, 10, 3), window.Frame); @@ -713,7 +713,7 @@ public void Modal_As_Top_Will_Drag_Cleanly () firstIteration = false; Application.RunIteration (ref rs, firstIteration); - Assert.Equal (window.Border, Application.MouseGrabView); + Assert.Equal (window.Border, Application.MouseGrabHandler.MouseGrabView); Assert.Equal (new (1, 1, 10, 3), window.Frame); Application.End (rs); diff --git a/Tests/UnitTestsParallelizable/Application/LogarithmicTimeoutTests.cs b/Tests/UnitTestsParallelizable/Application/LogarithmicTimeoutTests.cs new file mode 100644 index 0000000000..bb95dbc6d7 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Application/LogarithmicTimeoutTests.cs @@ -0,0 +1,81 @@ +namespace Terminal.Gui.ApplicationTests; + +public class LogarithmicTimeoutTests +{ + [Fact] + public void Span_Should_Return_BaseDelay_When_Stage_Is_Zero () + { + var baseDelay = TimeSpan.FromMilliseconds (1000); + var timeout = new LogarithmicTimeout (baseDelay, () => true); + + Assert.Equal (TimeSpan.Zero, timeout.Span); + } + + [Fact] + public void Span_Should_Increase_Logarithmically () + { + var baseDelay = TimeSpan.FromMilliseconds (1000); + var timeout = new LogarithmicTimeout (baseDelay, () => true); + + var stage0 = timeout.Span; + + timeout.AdvanceStage (); // stage = 1 + var stage1 = timeout.Span; + + timeout.AdvanceStage (); // stage = 2 + var stage2 = timeout.Span; + + timeout.AdvanceStage (); // stage = 3 + var stage3 = timeout.Span; + + Assert.True (stage1 > stage0, "Stage 1 should be greater than stage 0"); + Assert.True (stage2 > stage1, "Stage 2 should be greater than stage 1"); + Assert.True (stage3 > stage2, "Stage 3 should be greater than stage 2"); + } + + [Theory] + [MemberData (nameof (GetLogarithmicTestData))] + public void Span_Should_Match_Expected_Logarithmic_Value ( + double baseDelayMs, int stage, double expectedMs) + { + var baseDelay = TimeSpan.FromMilliseconds (baseDelayMs); + var timeout = new LogarithmicTimeout (baseDelay, () => true); + + for (int i = 0; i < stage; i++) + { + timeout.AdvanceStage (); + } + + double actualMs = timeout.Span.TotalMilliseconds; + double tolerance = 0.001; // Allow minor rounding error + + Assert.InRange (actualMs, expectedMs - tolerance, expectedMs + tolerance); + } + + public static IEnumerable GetLogarithmicTestData () + { + // baseDelayMs, stage, expectedSpanMs + double baseMs = 1000; + + yield return new object [] { baseMs, 0, 0 }; + yield return new object [] { baseMs, 1, baseMs * Math.Log (2) }; + yield return new object [] { baseMs, 2, baseMs * Math.Log (3) }; + yield return new object [] { baseMs, 5, baseMs * Math.Log (6) }; + yield return new object [] { baseMs, 10, baseMs * Math.Log (11) }; + } + + + [Fact] + public void Reset_Should_Set_Stage_Back_To_Zero () + { + var baseDelay = TimeSpan.FromMilliseconds (1000); + var timeout = new LogarithmicTimeout (baseDelay, () => true); + + timeout.AdvanceStage (); + timeout.AdvanceStage (); + Assert.NotEqual (baseDelay, timeout.Span); + + timeout.Reset (); + Assert.Equal (TimeSpan.Zero, timeout.Span); + } +} \ No newline at end of file diff --git a/Tests/UnitTestsParallelizable/Application/SmoothAcceleratingTimeoutTests.cs b/Tests/UnitTestsParallelizable/Application/SmoothAcceleratingTimeoutTests.cs new file mode 100644 index 0000000000..c6fd8b91c9 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Application/SmoothAcceleratingTimeoutTests.cs @@ -0,0 +1,70 @@ +namespace Terminal.Gui.ApplicationTests; + + +public class SmoothAcceleratingTimeoutTests +{ + [Fact] + public void Span_Should_Return_InitialDelay_On_StageZero () + { + var initialDelay = TimeSpan.FromMilliseconds (500); + var minDelay = TimeSpan.FromMilliseconds (50); + double decayFactor = 0.7; + + var timeout = new SmoothAcceleratingTimeout (initialDelay, minDelay, decayFactor, () => true); + + Assert.Equal (initialDelay, timeout.Span); + } + + [Fact] + public void Span_Should_Decrease_As_Stage_Increases () + { + var initialDelay = TimeSpan.FromMilliseconds (500); + var minDelay = TimeSpan.FromMilliseconds (50); + double decayFactor = 0.7; + + var timeout = new SmoothAcceleratingTimeout (initialDelay, minDelay, decayFactor, () => true); + + var previousSpan = timeout.Span; + for (int i = 0; i < 10; i++) + { + timeout.AdvanceStage (); + var currentSpan = timeout.Span; + Assert.True (currentSpan <= previousSpan, $"Stage {i + 1}: {currentSpan} should be <= {previousSpan}"); + previousSpan = currentSpan; + } + } + + [Fact] + public void Span_Should_Not_Go_Below_MinDelay () + { + var initialDelay = TimeSpan.FromMilliseconds (500); + var minDelay = TimeSpan.FromMilliseconds (50); + double decayFactor = 0.5; + + var timeout = new SmoothAcceleratingTimeout (initialDelay, minDelay, decayFactor, () => true); + + for (int i = 0; i < 100; i++) + { + timeout.AdvanceStage (); + } + + Assert.Equal (minDelay, timeout.Span); + } + + [Fact] + public void Reset_Should_Set_Stage_Back_To_Zero () + { + var initialDelay = TimeSpan.FromMilliseconds (500); + var minDelay = TimeSpan.FromMilliseconds (50); + double decayFactor = 0.7; + + var timeout = new SmoothAcceleratingTimeout (initialDelay, minDelay, decayFactor, () => true); + + timeout.AdvanceStage (); + timeout.AdvanceStage (); + Assert.NotEqual (initialDelay, timeout.Span); + + timeout.Reset (); + Assert.Equal (initialDelay, timeout.Span); + } +} diff --git a/Tests/UnitTestsParallelizable/TestSetup.cs b/Tests/UnitTestsParallelizable/TestSetup.cs index b85638e932..bddfea00fd 100644 --- a/Tests/UnitTestsParallelizable/TestSetup.cs +++ b/Tests/UnitTestsParallelizable/TestSetup.cs @@ -40,8 +40,7 @@ private void CheckDefaultState () // Public Properties Assert.Null (Application.Top); - Assert.Null (Application.MouseGrabView); - Assert.Null (Application.WantContinuousButtonPressedView); + Assert.Null (Application.MouseGrabHandler.MouseGrabView); // Don't check Application.ForceDriver // Assert.Empty (Application.ForceDriver); diff --git a/docfx/docs/multitasking.md b/docfx/docs/multitasking.md index 42142b71e2..a85f01b288 100644 --- a/docfx/docs/multitasking.md +++ b/docfx/docs/multitasking.md @@ -119,44 +119,6 @@ public class ClockView : View - **Keep timer callbacks fast** - they run on the main thread - **Use appropriate intervals** - too frequent updates can impact performance -## Idle Processing - -Idle handlers run when the application has no events to process, useful for background maintenance: - -```csharp -public class AutoSaveView : View -{ - private object idleToken; - private DateTime lastSave = DateTime.Now; - - public AutoSaveView() - { - idleToken = Application.MainLoop.AddIdle(CheckAutoSave); - } - - private bool CheckAutoSave() - { - if (DateTime.Now - lastSave > TimeSpan.FromMinutes(5)) - { - if (HasUnsavedChanges()) - { - SaveDocument(); - lastSave = DateTime.Now; - } - } - return true; // Continue idle processing - } - - protected override void Dispose(bool disposing) - { - if (disposing && idleToken != null) - { - Application.MainLoop.RemoveIdle(idleToken); - } - base.Dispose(disposing); - } -} -``` ## Common Patterns