diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs index b5db7847be..a1de352064 100644 --- a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs @@ -395,10 +395,7 @@ MouseEvent ToDriverMouse (Curses.MouseEvent cev) }; if ((mouseFlag & MouseFlags.ReportMousePosition) == 0) { - Application.MainLoop.AddIdle (() => { - Task.Run (async () => await ProcessContinuousButtonPressedAsync (mouseFlag)); - return false; - }); + Application.MainLoop.Invoke (async () => await ProcessContinuousButtonPressedAsync (mouseFlag)); } @@ -1500,6 +1497,7 @@ protected override void SetClipboardDataImpl (string text) } }) { powershell.Start (); + powershell.WaitForExit (); if (!powershell.DoubleWaitForExit ()) { var timeoutError = $@"Process timed out. Command line: bash {powershell.StartInfo.Arguments}. Output: {powershell.StandardOutput.ReadToEnd ()} diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver.cs b/Terminal.Gui/ConsoleDrivers/NetDriver.cs index c6c65caa74..f0871751f3 100644 --- a/Terminal.Gui/ConsoleDrivers/NetDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/NetDriver.cs @@ -713,10 +713,7 @@ void GetMouseEvent (ConsoleKeyInfo [] cki) //System.Diagnostics.Debug.WriteLine ($"ButtonState: {mouseEvent.ButtonState} X: {mouseEvent.Position.X} Y: {mouseEvent.Position.Y}"); if (isButtonDoubleClicked) { - Application.MainLoop.AddIdle (() => { - Task.Run (async () => await ProcessButtonDoubleClickedAsync ()); - return false; - }); + Application.MainLoop.Invoke (async () => await ProcessButtonDoubleClickedAsync ()); } if ((buttonState & MouseButtonState.Button1Pressed) != 0 @@ -782,12 +779,9 @@ void GetMouseEvent (ConsoleKeyInfo [] cki) isButtonClicked = false; isButtonDoubleClicked = true; ProcessButtonDoubleClicked (mouseEvent); - Application.MainLoop.AddIdle (() => { - Task.Run (async () => { - await Task.Delay (600); - isButtonDoubleClicked = false; - }); - return false; + Application.MainLoop.Invoke (async () => { + await Task.Delay (600); + isButtonDoubleClicked = false; }); inputReady.Set (); return; @@ -829,12 +823,9 @@ void GetMouseEvent (ConsoleKeyInfo [] cki) || (buttonState & MouseButtonState.Button3Released) != 0)) { isButtonClicked = true; ProcessButtonClicked (mouseEvent); - Application.MainLoop.AddIdle (() => { - Task.Run (async () => { - await Task.Delay (300); - isButtonClicked = false; - }); - return false; + Application.MainLoop.Invoke (async () => { + await Task.Delay (300); + isButtonClicked = false; }); inputReady.Set (); return; @@ -852,10 +843,7 @@ void GetMouseEvent (ConsoleKeyInfo [] cki) }; } if ((buttonState & MouseButtonState.ReportMousePosition) == 0) { - Application.MainLoop.AddIdle (() => { - Task.Run (async () => await ProcessContinuousButtonPressedAsync ()); - return false; - }); + Application.MainLoop.Invoke (async () => await ProcessContinuousButtonPressedAsync ()); } } @@ -951,7 +939,7 @@ async Task ProcessContinuousButtonPressedAsync () if (view == null) { break; } - if (isButtonPressed && (lastMouseEvent.ButtonState & MouseButtonState.ReportMousePosition) == 0) { + if (isButtonPressed) { inputResultQueue.Enqueue (new InputResult () { EventType = EventType.Mouse, MouseEvent = lastMouseEvent diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs index e25027050d..6cb6861a00 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs @@ -914,10 +914,7 @@ MouseEvent ToDriverMouse (WindowsConsole.MouseEventRecord mouseEvent) // $"X:{mouseEvent.MousePosition.X};Y:{mouseEvent.MousePosition.Y};ButtonState:{mouseEvent.ButtonState};EventFlags:{mouseEvent.EventFlags}"); if (isButtonDoubleClicked || isOneFingerDoubleClicked) { - Application.MainLoop.AddIdle (() => { - Task.Run (async () => await ProcessButtonDoubleClickedAsync ()); - return false; - }); + Application.MainLoop.Invoke (async () => await ProcessButtonDoubleClickedAsync ()); } // The ButtonState member of the MouseEvent structure has bit corresponding to each mouse button. @@ -1025,10 +1022,7 @@ MouseEvent ToDriverMouse (WindowsConsole.MouseEventRecord mouseEvent) isButtonPressed = true; if ((mouseFlag & MouseFlags.ReportMousePosition) == 0) { - Application.MainLoop.AddIdle (() => { - Task.Run (async () => await ProcessContinuousButtonPressedAsync (mouseFlag)); - return false; - }); + Application.MainLoop.Invoke (async () => await ProcessContinuousButtonPressedAsync (mouseFlag)); } } else if (lastMouseButtonPressed != null && mouseEvent.EventFlags == 0 diff --git a/Terminal.Gui/Core/Clipboard/ClipboardBase.cs b/Terminal.Gui/Core/Clipboard/ClipboardBase.cs index db61af80f0..9eb17e174f 100644 --- a/Terminal.Gui/Core/Clipboard/ClipboardBase.cs +++ b/Terminal.Gui/Core/Clipboard/ClipboardBase.cs @@ -92,7 +92,8 @@ public bool TrySetClipboardData (string text) try { SetClipboardDataImpl (text); return true; - } catch (Exception) { + } catch (Exception ex) { + System.Diagnostics.Debug.WriteLine ($"TrySetClipboardData: {ex.Message}"); return false; } } diff --git a/Terminal.Gui/Core/ShortcutHelper.cs b/Terminal.Gui/Core/ShortcutHelper.cs index 437a6e4a40..ec68c7b9bf 100644 --- a/Terminal.Gui/Core/ShortcutHelper.cs +++ b/Terminal.Gui/Core/ShortcutHelper.cs @@ -244,7 +244,8 @@ public static bool PostShortcutValidation (Key key) public static bool FindAndOpenByShortcut (KeyEvent kb, View view = null) { if (view == null) { - return false; } + return false; + } var key = kb.KeyValue; var keys = GetModifiersKey (kb); @@ -252,12 +253,7 @@ public static bool FindAndOpenByShortcut (KeyEvent kb, View view = null) foreach (var v in view.Subviews) { if (v.Shortcut != Key.Null && v.Shortcut == (Key)key) { var action = v.ShortcutAction; - if (action != null) { - Application.MainLoop.AddIdle (() => { - action (); - return false; - }); - } + Application.MainLoop.Invoke (() => action?.Invoke ()); return true; } if (FindAndOpenByShortcut (kb, v)) { diff --git a/Terminal.Gui/Views/Menu.cs b/Terminal.Gui/Views/Menu.cs index 804ddb5ed3..518f916de1 100644 --- a/Terminal.Gui/Views/Menu.cs +++ b/Terminal.Gui/Views/Menu.cs @@ -577,10 +577,7 @@ public void Run (Action action) host.CloseAllMenus (); Application.Refresh (); - Application.MainLoop.AddIdle (() => { - action (); - return false; - }); + Application.MainLoop.Invoke (() => action?.Invoke ()); } public override bool OnLeave (View view) @@ -1089,10 +1086,7 @@ void Selected (MenuItem item) CloseAllMenus (); Application.Refresh (); - Application.MainLoop.AddIdle (() => { - action (); - return false; - }); + Application.MainLoop.Invoke (() => action?.Invoke ()); } /// @@ -1635,12 +1629,7 @@ internal bool FindAndOpenMenuByShortcut (KeyEvent kb, MenuItem [] children = nul } if ((!(mi is MenuBarItem mbiTopLevel) || mbiTopLevel.IsTopLevel) && mi.Shortcut != Key.Null && mi.Shortcut == (Key)key) { var action = mi.Action; - if (action != null) { - Application.MainLoop.AddIdle (() => { - action (); - return false; - }); - } + Application.MainLoop.Invoke (() => action?.Invoke ()); return true; } if (mi is MenuBarItem menuBarItem && !menuBarItem.IsTopLevel && FindAndOpenMenuByShortcut (kb, menuBarItem.Children)) { diff --git a/Terminal.Gui/Views/StatusBar.cs b/Terminal.Gui/Views/StatusBar.cs index f8db8920c8..25cbbd4a55 100644 --- a/Terminal.Gui/Views/StatusBar.cs +++ b/Terminal.Gui/Views/StatusBar.cs @@ -222,10 +222,7 @@ void Run (Action action) if (action == null) return; - Application.MainLoop.AddIdle (() => { - action (); - return false; - }); + Application.MainLoop.Invoke (() => action?.Invoke ()); } /// diff --git a/Terminal.sln b/Terminal.sln index 03b0011e98..29fac1e30f 100644 --- a/Terminal.sln +++ b/Terminal.sln @@ -20,6 +20,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .github\workflows\dotnet-core.yml = .github\workflows\dotnet-core.yml .github\workflows\publish.yml = .github\workflows\publish.yml README.md = README.md + testenvironments.json = testenvironments.json EndProjectSection EndProject Global diff --git a/UnitTests/MainLoopTests.cs b/UnitTests/MainLoopTests.cs index 4cf1d5b712..d3de069fe3 100644 --- a/UnitTests/MainLoopTests.cs +++ b/UnitTests/MainLoopTests.cs @@ -591,5 +591,190 @@ public async Task InvokeLeakTest () Assert.Equal ((numIncrements * numPasses), tbCounter); } + + private static int total; + private static Button btn; + private static string clickMe; + private static string cancel; + private static string pewPew; + private static int zero; + private static int one; + private static int two; + private static int three; + private static int four; + + [Theory, AutoInitShutdown] + [MemberData (nameof (TestAddIdle))] + public void Mainloop_Invoke_Or_AddIdle_Should_Not_Be_Used_For_Events_Or_Actions (Action action, string pclickMe, string pcancel, string ppewPew, int pzero, int pone, int ptwo, int pthree, int pfour) + { + total = 0; + btn = null; + clickMe = pclickMe; + cancel = pcancel; + pewPew = ppewPew; + zero = pzero; + one = pone; + two = ptwo; + three = pthree; + four = pfour; + + var btnLaunch = new Button ("Open Window"); + + btnLaunch.Clicked += () => action (); + + Application.Top.Add (btnLaunch); + + var iterations = -1; + + Application.Iteration += () => { + iterations++; + if (iterations == 0) { + Assert.Null (btn); + Assert.Equal (zero, total); + Assert.True (btnLaunch.ProcessKey (new KeyEvent (Key.Enter, null))); + if (btn == null) { + Assert.Null (btn); + Assert.Equal (zero, total); + } else { + Assert.Equal (clickMe, btn.Text); + Assert.Equal (four, total); + } + } else if (iterations == 1) { + Assert.Equal (clickMe, btn.Text); + Assert.Equal (zero, total); + Assert.True (btn.ProcessKey (new KeyEvent (Key.Enter, null))); + Assert.Equal (cancel, btn.Text); + Assert.Equal (one, total); + } else if (iterations == 20000) { + if (total == one) { + Assert.Equal (cancel, btn.Text); + Assert.Equal (one, total); + + Application.RequestStop (); + } + } + }; + + Application.Run (); + + if (four == 4) { + Assert.Equal (clickMe, btn.Text); + Assert.Equal (four, total); + } else { + Assert.Equal (cancel, btn.Text); + Assert.Equal (four, total); + } + + Application.Shutdown (); + + } + + public static IEnumerable TestAddIdle { + get { + // Goes fine + Action a1 = StartWindow; + yield return new object [] { a1, "Click Me", "Cancel", "Pew Pew", 0, 1, 2, 3, 4 }; + + // Goes badly + Action a2 = () => Application.MainLoop.Invoke (StartWindow); + yield return new object [] { a2, "Click Me", "Cancel", "Cancel", 0, 1, 1, 1, 1 }; + } + } + + private static void StartWindow () + { + var startWindow = new Window { + Modal = true + }; + + btn = new Button { + Text = "Click Me" + }; + + btn.Clicked += RunAsyncTest; + + var totalbtn = new Button () { + X = Pos.Right (btn), + Text = "total" + }; + + totalbtn.Clicked += () => { + MessageBox.Query ("Count", $"Count is {total}", "Ok"); + }; + + startWindow.Add (btn); + startWindow.Add (totalbtn); + + Application.Run (startWindow); + + if (four == 4) { + Assert.Equal (clickMe, btn.Text); + Assert.Equal (four, total); + } else { + Assert.Equal (cancel, btn.Text); + Assert.Equal (four, total); + } + + Application.RequestStop (); + } + + private static void RunAsyncTest () + { + Assert.Equal (clickMe, btn.Text); + Assert.Equal (zero, total); + + btn.Text = "Cancel"; + Interlocked.Increment (ref total); + btn.SetNeedsDisplay (); + + var task = Task.Run (() => { + try { + Assert.Equal (cancel, btn.Text); + Assert.Equal (one, total); + + RunSql (); + } finally { + SetReadyToRun (); + } + }).ContinueWith ((s, e) => { + + Assert.Equal (clickMe, btn.Text); + Assert.Equal (three, total); + + Interlocked.Increment (ref total); + + Assert.Equal (clickMe, btn.Text); + Assert.Equal (four, total); + + Application.RequestStop (); + + }, TaskScheduler.FromCurrentSynchronizationContext ()); + } + + private static void RunSql () + { + Thread.Sleep (100); + Assert.Equal (cancel, btn.Text); + Assert.Equal (one, total); + + Application.MainLoop.Invoke (() => { + btn.Text = "Pew Pew"; + Interlocked.Increment (ref total); + btn.SetNeedsDisplay (); + }); + } + + private static void SetReadyToRun () + { + Thread.Sleep (100); + Assert.Equal (pewPew, btn.Text); + Assert.Equal (two, total); + + Application.MainLoop.Invoke (() => { + btn.Text = "Click Me"; + Interlocked.Increment (ref total); + btn.SetNeedsDisplay (); + }); + } } } diff --git a/UnitTests/ToplevelTests.cs b/UnitTests/ToplevelTests.cs index 61f39abd8f..7050b7fbcf 100644 --- a/UnitTests/ToplevelTests.cs +++ b/UnitTests/ToplevelTests.cs @@ -33,9 +33,10 @@ public void Create_Toplevel () [AutoInitShutdown] public void Application_Top_EnsureVisibleBounds_To_Driver_Rows_And_Cols () { - var iterations = 0; + var iterations = -1; Application.Iteration += () => { + iterations++; if (iterations == 0) { Assert.False (Application.Top.AutoSize); Assert.Equal ("Top1", Application.Top.Text); @@ -78,7 +79,6 @@ public void Application_Top_EnsureVisibleBounds_To_Driver_Rows_And_Cols () Application.Top.ProcessHotKey (new KeyEvent (Key.CtrlMask | Key.Q, new KeyModifiers ())); } - iterations++; }; Application.Run (Top1 ()); diff --git a/testenvironments.json b/testenvironments.json new file mode 100644 index 0000000000..70dbd0b4c0 --- /dev/null +++ b/testenvironments.json @@ -0,0 +1,15 @@ +{ + "version": "1", + "environments": [ + { + "name": "WSL-Ubuntu", + "type": "wsl", + "wslDistribution": "Ubuntu" + }, + { + "name": "WSL-Debian", + "type": "wsl", + "wslDistribution": "Debian" + } + ] +} \ No newline at end of file