Browse Source

Exposes APIs to support upcoming Web console feature (#1770)

* Implementations to allow a WebConsole feature

* Preventing application hanging on the RunLoop.

* Implementations to allow a WebConsole feature

* Preventing application hanging on the RunLoop.

* Making WantContinuousButtonPressedView public.

* Added TimeoutAdded event.

* Fixes unit test for WantContinuousButtonPressedView.

* Changes as requested resolved.

Co-authored-by: BDisp <[email protected]>
Freddie Mercurial (Jessica M.) 3 years ago
parent
commit
68b40a93f1

+ 2 - 2
Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs

@@ -30,7 +30,7 @@ namespace Terminal.Gui {
 		IClipboard clipboard;
 		int [,,] contents;
 
-		internal override int [,,] Contents => contents;
+		public override int [,,] Contents => contents;
 
 		// Current row, and current col, tracked by Move/AddRune only
 		int ccol, crow;
@@ -488,7 +488,7 @@ namespace Terminal.Gui {
 					Flags = mouseFlag
 				};
 
-				var view = Application.wantContinuousButtonPressedView;
+				var view = Application.WantContinuousButtonPressedView;
 				if (view == null)
 					break;
 				if (isButtonPressed && lastMouseButtonPressed != null && (mouseFlag & MouseFlags.ReportMousePosition) == 0) {

+ 1 - 1
Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs

@@ -35,7 +35,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Assists with testing, the format is rows, columns and 3 values on the last column: Rune, Attribute and Dirty Flag
 		/// </summary>
-		internal override int [,,] Contents => contents;
+		public override int [,,] Contents => contents;
 
 		//void UpdateOffscreen ()
 		//{

+ 2 - 2
Terminal.Gui/ConsoleDrivers/NetDriver.cs

@@ -947,7 +947,7 @@ namespace Terminal.Gui {
 			await Task.Delay (200);
 			while (isButtonPressed) {
 				await Task.Delay (100);
-				var view = Application.wantContinuousButtonPressedView;
+				var view = Application.WantContinuousButtonPressedView;
 				if (view == null) {
 					break;
 				}
@@ -1187,7 +1187,7 @@ namespace Terminal.Gui {
 		public NetWinVTConsole NetWinConsole { get; }
 		public bool IsWinPlatform { get; }
 		public override IClipboard Clipboard { get; }
-		internal override int [,,] Contents => contents;
+		public override int [,,] Contents => contents;
 
 		int largestWindowHeight;
 

+ 2 - 2
Terminal.Gui/ConsoleDrivers/WindowsDriver.cs

@@ -732,7 +732,7 @@ namespace Terminal.Gui {
 		public override int Top => top;
 		public override bool HeightAsBuffer { get; set; }
 		public override IClipboard Clipboard => clipboard;
-		internal override int [,,] Contents => contents;
+		public override int [,,] Contents => contents;
 
 		public WindowsConsole WinConsole { get; private set; }
 
@@ -1181,7 +1181,7 @@ namespace Terminal.Gui {
 					Flags = mouseFlag
 				};
 
-				var view = Application.wantContinuousButtonPressedView;
+				var view = Application.WantContinuousButtonPressedView;
 				if (view == null) {
 					break;
 				}

+ 101 - 55
Terminal.Gui/Core/Application.cs

@@ -105,6 +105,11 @@ namespace Terminal.Gui {
 		/// <value>The current.</value>
 		public static Toplevel Current { get; private set; }
 
+		/// <summary>
+		/// The current <see cref="View"/> object that wants continuous mouse button pressed events.
+		/// </summary>
+		public static View WantContinuousButtonPressedView { get; private set; }
+
 		/// <summary>
 		/// The current <see cref="ConsoleDriver.HeightAsBuffer"/> used in the terminal.
 		/// </summary>
@@ -210,6 +215,24 @@ namespace Terminal.Gui {
 		/// </summary>
 		public static bool IsMouseDisabled { get; set; }
 
+		/// <summary>
+		/// Set to true to cause the RunLoop method to exit after the first iterations.
+		/// Set to false (the default) to cause the RunLoop to continue running until Application.RequestStop() is called.
+		/// </summary>
+		public static bool ExitRunLoopAfterFirstIteration { get; set; } = false;
+
+		/// <summary>
+		/// Notify that a new <see cref="RunState"/> token was created,
+		/// used if <see cref="ExitRunLoopAfterFirstIteration"/> is true.
+		/// </summary>
+		public static event Action<RunState> NotifyNewRunState;
+
+		/// <summary>
+		/// Notify that a existent <see cref="RunState"/> token is stopping,
+		/// used if <see cref="ExitRunLoopAfterFirstIteration"/> is true.
+		/// </summary>
+		public static event Action<Toplevel> NotifyStopRunState;
+
 		/// <summary>
 		///   This event is raised on each iteration of the <see cref="MainLoop"/> 
 		/// </summary>
@@ -297,12 +320,12 @@ namespace Terminal.Gui {
 			}
 
 			// Used only for start debugging on Unix.
-//#if DEBUG
-//			while (!System.Diagnostics.Debugger.IsAttached) {
-//				System.Threading.Thread.Sleep (100);
-//			}
-//			System.Diagnostics.Debugger.Break ();
-//#endif
+			//#if DEBUG
+			//			while (!System.Diagnostics.Debugger.IsAttached) {
+			//				System.Threading.Thread.Sleep (100);
+			//			}
+			//			System.Diagnostics.Debugger.Break ();
+			//#endif
 
 			// Reset all class variables (Application is a singleton).
 			ResetState ();
@@ -352,7 +375,10 @@ namespace Terminal.Gui {
 			{
 				Toplevel = view;
 			}
-			internal Toplevel Toplevel;
+			/// <summary>
+			/// The <see cref="Toplevel"/> belong to this <see cref="RunState"/>.
+			/// </summary>
+			public Toplevel Toplevel { get; internal set; }
 
 			/// <summary>
 			/// Releases alTop = l resource used by the <see cref="Application.RunState"/> object.
@@ -385,7 +411,7 @@ namespace Terminal.Gui {
 
 		static void ProcessKeyEvent (KeyEvent ke)
 		{
-			if(RootKeyEvent?.Invoke(ke) ?? false) {
+			if (RootKeyEvent?.Invoke (ke) ?? false) {
 				return;
 			}
 
@@ -580,9 +606,8 @@ namespace Terminal.Gui {
 		/// </para>
 		/// <para>Return true to suppress the KeyPress event</para>
 		/// </summary>
-		public static Func<KeyEvent,bool> RootKeyEvent;
+		public static Func<KeyEvent, bool> RootKeyEvent;
 
-		internal static View wantContinuousButtonPressedView;
 		static View lastMouseOwnerView;
 
 		static void ProcessMouseEvent (MouseEvent me)
@@ -594,9 +619,9 @@ namespace Terminal.Gui {
 			var view = FindDeepestView (Current, me.X, me.Y, out int rx, out int ry);
 
 			if (view != null && view.WantContinuousButtonPressed)
-				wantContinuousButtonPressedView = view;
+				WantContinuousButtonPressedView = view;
 			else
-				wantContinuousButtonPressedView = null;
+				WantContinuousButtonPressedView = null;
 			if (view != null) {
 				me.View = view;
 			}
@@ -655,9 +680,9 @@ namespace Terminal.Gui {
 					return;
 
 				if (view.WantContinuousButtonPressed)
-					wantContinuousButtonPressedView = view;
+					WantContinuousButtonPressedView = view;
 				else
-					wantContinuousButtonPressedView = null;
+					WantContinuousButtonPressedView = null;
 
 				// Should we bubbled up the event, if it is not handled?
 				view.OnMouseEvent (nme);
@@ -952,51 +977,65 @@ namespace Terminal.Gui {
 
 			bool firstIteration = true;
 			for (state.Toplevel.Running = true; state.Toplevel.Running;) {
-				if (MainLoop.EventsPending (wait)) {
-					// Notify Toplevel it's ready
-					if (firstIteration) {
-						state.Toplevel.OnReady ();
-					}
-					firstIteration = false;
-
-					MainLoop.MainIteration ();
-					Iteration?.Invoke ();
-
-					EnsureModalOrVisibleAlwaysOnTop (state.Toplevel);
-					if ((state.Toplevel != Current && Current?.Modal == true)
-						|| (state.Toplevel != Current && Current?.Modal == false)) {
-						MdiTop?.OnDeactivate (state.Toplevel);
-						state.Toplevel = Current;
-						MdiTop?.OnActivate (state.Toplevel);
-						Top.SetChildNeedsDisplay ();
-						Refresh ();
-					}
-					if (Driver.EnsureCursorVisibility ()) {
-						state.Toplevel.SetNeedsDisplay ();
-					}
-				} else if (!wait) {
+				if (ExitRunLoopAfterFirstIteration && !firstIteration)
 					return;
+				RunMainLoopIteration (ref state, wait, ref firstIteration);
+			}
+		}
+
+		/// <summary>
+		/// Run one iteration of the MainLoop.
+		/// </summary>
+		/// <param name="state">The state returned by the Begin method.</param>
+		/// <param name="wait">If will execute the runloop waiting for events.</param>
+		/// <param name="firstIteration">If it's the first run loop iteration.</param>
+		public static void RunMainLoopIteration (ref RunState state, bool wait, ref bool firstIteration)
+		{
+			if (MainLoop.EventsPending (wait)) {
+				// Notify Toplevel it's ready
+				if (firstIteration) {
+					state.Toplevel.OnReady ();
 				}
-				if (state.Toplevel != Top
-					&& (!Top.NeedDisplay.IsEmpty || Top.ChildNeedsDisplay || Top.LayoutNeeded)) {
-					Top.Redraw (Top.Bounds);
-					state.Toplevel.SetNeedsDisplay (state.Toplevel.Bounds);
+
+				MainLoop.MainIteration ();
+				Iteration?.Invoke ();
+
+				EnsureModalOrVisibleAlwaysOnTop (state.Toplevel);
+				if ((state.Toplevel != Current && Current?.Modal == true)
+					|| (state.Toplevel != Current && Current?.Modal == false)) {
+					MdiTop?.OnDeactivate (state.Toplevel);
+					state.Toplevel = Current;
+					MdiTop?.OnActivate (state.Toplevel);
+					Top.SetChildNeedsDisplay ();
+					Refresh ();
 				}
-				if (!state.Toplevel.NeedDisplay.IsEmpty || state.Toplevel.ChildNeedsDisplay || state.Toplevel.LayoutNeeded
-					|| MdiChildNeedsDisplay ()) {
-					state.Toplevel.Redraw (state.Toplevel.Bounds);
-					if (DebugDrawBounds) {
-						DrawBounds (state.Toplevel);
-					}
-					state.Toplevel.PositionCursor ();
-					Driver.Refresh ();
-				} else {
-					Driver.UpdateCursor ();
+				if (Driver.EnsureCursorVisibility ()) {
+					state.Toplevel.SetNeedsDisplay ();
 				}
-				if (state.Toplevel != Top && !state.Toplevel.Modal
-					&& (!Top.NeedDisplay.IsEmpty || Top.ChildNeedsDisplay || Top.LayoutNeeded)) {
-					Top.Redraw (Top.Bounds);
+			} else if (!wait) {
+				return;
+			}
+			firstIteration = false;
+
+			if (state.Toplevel != Top
+				&& (!Top.NeedDisplay.IsEmpty || Top.ChildNeedsDisplay || Top.LayoutNeeded)) {
+				Top.Redraw (Top.Bounds);
+				state.Toplevel.SetNeedsDisplay (state.Toplevel.Bounds);
+			}
+			if (!state.Toplevel.NeedDisplay.IsEmpty || state.Toplevel.ChildNeedsDisplay || state.Toplevel.LayoutNeeded
+				|| MdiChildNeedsDisplay ()) {
+				state.Toplevel.Redraw (state.Toplevel.Bounds);
+				if (DebugDrawBounds) {
+					DrawBounds (state.Toplevel);
 				}
+				state.Toplevel.PositionCursor ();
+				Driver.Refresh ();
+			} else {
+				Driver.UpdateCursor ();
+			}
+			if (state.Toplevel != Top && !state.Toplevel.Modal
+				&& (!Top.NeedDisplay.IsEmpty || Top.ChildNeedsDisplay || Top.LayoutNeeded)) {
+				Top.Redraw (Top.Bounds);
 			}
 		}
 
@@ -1107,7 +1146,12 @@ namespace Terminal.Gui {
 				resume = false;
 				var runToken = Begin (view);
 				RunLoop (runToken);
-				End (runToken);
+				if (!ExitRunLoopAfterFirstIteration)
+					End (runToken);
+				else
+					// If ExitRunLoopAfterFirstIteration is true then the user must deal his disposing when it ends
+					// by using NotifyStopRunState event.
+					NotifyNewRunState?.Invoke (runToken);
 #if !DEBUG
 				}
 				catch (Exception error)
@@ -1190,6 +1234,8 @@ namespace Terminal.Gui {
 					return;
 				}
 				currentTop.Running = false;
+				if (ExitRunLoopAfterFirstIteration)
+					NotifyStopRunState?.Invoke (currentTop);
 			}
 		}
 

+ 4 - 2
Terminal.Gui/Core/ConsoleDriver.cs

@@ -665,8 +665,10 @@ namespace Terminal.Gui {
 		/// </summary>
 		public abstract bool HeightAsBuffer { get; set; }
 
-		// The format is rows, columns and 3 values on the last column: Rune, Attribute and Dirty Flag
-		internal abstract int [,,] Contents { get; }
+		/// <summary>
+		/// The format is rows, columns and 3 values on the last column: Rune, Attribute and Dirty Flag
+		/// </summary>
+		public virtual int [,,] Contents { get; }
 
 		/// <summary>
 		/// Initializes the driver

+ 32 - 5
Terminal.Gui/Core/MainLoop.cs

@@ -45,8 +45,17 @@ namespace Terminal.Gui {
 	///   does not seem to be a way of supporting this on Windows.
 	/// </remarks>
 	public class MainLoop {
-		internal class Timeout {
+		/// <summary>
+		/// Provides data for timers running manipulation.
+		/// </summary>
+		public sealed class Timeout {
+			/// <summary>
+			/// Time to wait before invoke the callback.
+			/// </summary>
 			public TimeSpan Span;
+			/// <summary>
+			/// The function that will be invoked.
+			/// </summary>
 			public Func<MainLoop, bool> Callback;
 		}
 
@@ -54,12 +63,30 @@ namespace Terminal.Gui {
 		object timeoutsLockToken = new object ();
 		internal List<Func<bool>> idleHandlers = new List<Func<bool>> ();
 
+		/// <summary>
+		/// Gets the list of all timeouts sorted by the <see cref="TimeSpan"/> 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.
+		/// </summary>
+		public SortedList<long, Timeout> Timeouts => timeouts;
+
+		/// <summary>
+		/// Gets the list of all idle handlers.
+		/// </summary>
+		public List<Func<bool>> IdleHandlers => idleHandlers;
+
 		/// <summary>
 		/// The current IMainLoopDriver in use.
 		/// </summary>
 		/// <value>The driver.</value>
 		public IMainLoopDriver Driver { get; }
 
+		/// <summary>
+		/// Invoked when a new timeout is added to be used on the case
+		/// if <see cref="Application.ExitRunLoopAfterFirstIteration"/> is true,
+		/// </summary>
+		public event Action<long> TimeoutAdded;
+
 		/// <summary>
 		///  Creates a new Mainloop. 
 		/// </summary>
@@ -120,7 +147,8 @@ namespace Terminal.Gui {
 		{
 			lock (timeoutsLockToken) {
 				var k = (DateTime.UtcNow + time).Ticks;
-				timeouts.Add (NudgeToUniqueKey(k), timeout);
+				timeouts.Add (NudgeToUniqueKey (k), timeout);
+				TimeoutAdded?.Invoke (k);
 			}
 		}
 
@@ -188,11 +216,10 @@ namespace Terminal.Gui {
 						AddTimeout (timeout.Span, timeout);
 				} else {
 					lock (timeoutsLockToken) {
-						timeouts.Add (NudgeToUniqueKey(k), timeout);
+						timeouts.Add (NudgeToUniqueKey (k), timeout);
 					}
 				}
 			}
-			
 		}
 
 		/// <summary>
@@ -203,7 +230,7 @@ namespace Terminal.Gui {
 		/// <returns></returns>
 		private long NudgeToUniqueKey (long k)
 		{
-			lock(timeoutsLockToken) {
+			lock (timeoutsLockToken) {
 				while (timeouts.ContainsKey (k)) {
 					k++;
 				}

+ 1 - 1
UnitTests/ApplicationTests.cs

@@ -1144,7 +1144,7 @@ namespace Terminal.Gui.Core {
 			var rs = Application.Begin (Application.Top);
 			Assert.Equal (Application.Top, rs.Toplevel);
 			Assert.Null (Application.mouseGrabView);
-			Assert.Null (Application.wantContinuousButtonPressedView);
+			Assert.Null (Application.WantContinuousButtonPressedView);
 			Assert.False (Application.DebugDrawBounds);
 			Assert.False (Application.ShowChild (Application.Top));
 			Application.End (Application.Top);