diff --git a/Examples/UICatalog/Scenarios/SendKeys.cs b/Examples/UICatalog/Scenarios/SendKeys.cs index 04a57d4e4e..4e5591559b 100644 --- a/Examples/UICatalog/Scenarios/SendKeys.cs +++ b/Examples/UICatalog/Scenarios/SendKeys.cs @@ -1,4 +1,4 @@ -using System; +using System.Text; namespace UICatalog.Scenarios; @@ -39,7 +39,7 @@ public override void Main () txtResult.KeyDown += (s, e) => { - rKeys += (char)e.KeyCode; + rKeys += e.ToString (); if (!IsShift && e.IsShift) { @@ -81,17 +81,15 @@ void ProcessInput () foreach (char r in txtInput.Text) { - ConsoleKey ck = char.IsLetter (r) - ? (ConsoleKey)char.ToUpper (r) - : (ConsoleKey)r; + ConsoleKeyInfo consoleKeyInfo = EscSeqUtils.MapConsoleKeyInfo (new (r, ConsoleKey.None, false, false, false)); Application.Driver?.SendKeys ( - r, - ck, - ckbShift.CheckedState == CheckState.Checked, - ckbAlt.CheckedState == CheckState.Checked, - ckbControl.CheckedState == CheckState.Checked - ); + r, + consoleKeyInfo.Key, + ckbShift.CheckedState == CheckState.Checked || (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0, + ckbAlt.CheckedState == CheckState.Checked || (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0, + ckbControl.CheckedState == CheckState.Checked || (consoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0 + ); } lblShippedKeys.Text = rKeys; diff --git a/Terminal.Gui/App/Application.Run.cs b/Terminal.Gui/App/Application.Run.cs index 32127a49cf..756290cc2c 100644 --- a/Terminal.Gui/App/Application.Run.cs +++ b/Terminal.Gui/App/Application.Run.cs @@ -460,7 +460,7 @@ internal static void LayoutAndDrawImpl (bool forceDraw = false) /// This event is raised on each iteration of the main loop. /// See also public static event EventHandler? Iteration; - + /// The driver for the application /// The main loop. internal static MainLoop? MainLoop { get; set; } @@ -618,4 +618,8 @@ public static void End (RunState runState) LayoutAndDraw (true); } + internal static void RaiseIteration () + { + Iteration?.Invoke (null, new ()); + } } diff --git a/Terminal.Gui/App/Application.cs b/Terminal.Gui/App/Application.cs index 7741b12b18..e1012c85c1 100644 --- a/Terminal.Gui/App/Application.cs +++ b/Terminal.Gui/App/Application.cs @@ -51,6 +51,20 @@ public static partial class Application /// public static ITimedEvents? TimedEvents => ApplicationImpl.Instance?.TimedEvents; + /// + /// Maximum number of iterations of the main loop (and hence draws) + /// to allow to occur per second. Defaults to > which is a 40ms sleep + /// after iteration (factoring in how long iteration took to run). + /// Note that not every iteration draws (see ). + /// Only affects v2 drivers. + /// + public static ushort MaximumIterationsPerSecond = DefaultMaximumIterationsPerSecond; + + /// + /// Default value for + /// + public const ushort DefaultMaximumIterationsPerSecond = 25; + /// /// Gets a string representation of the Application as rendered by . /// diff --git a/Terminal.Gui/Drivers/V2/ApplicationV2.cs b/Terminal.Gui/Drivers/V2/ApplicationV2.cs index ca94ebe575..a3964328f6 100644 --- a/Terminal.Gui/Drivers/V2/ApplicationV2.cs +++ b/Terminal.Gui/Drivers/V2/ApplicationV2.cs @@ -1,5 +1,6 @@ #nullable enable using System.Collections.Concurrent; +using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; @@ -12,10 +13,7 @@ namespace Terminal.Gui.Drivers; /// public class ApplicationV2 : ApplicationImpl { - private readonly Func _netInputFactory; - private readonly Func _netOutputFactory; - private readonly Func _winInputFactory; - private readonly Func _winOutputFactory; + private readonly IComponentFactory? _componentFactory; private IMainLoopCoordinator? _coordinator; private string? _driverName; @@ -24,29 +22,20 @@ public class ApplicationV2 : ApplicationImpl /// public override ITimedEvents TimedEvents => _timedEvents; + internal IMainLoopCoordinator? Coordinator => _coordinator; + /// /// Creates anew instance of the Application backend. The provided /// factory methods will be used on Init calls to get things booted. /// - public ApplicationV2 () : this ( - () => new NetInput (), - () => new NetOutput (), - () => new WindowsInput (), - () => new WindowsOutput () - ) - { } - - internal ApplicationV2 ( - Func netInputFactory, - Func netOutputFactory, - Func winInputFactory, - Func winOutputFactory - ) + public ApplicationV2 () + { + IsLegacy = false; + } + + internal ApplicationV2 (IComponentFactory componentFactory) { - _netInputFactory = netInputFactory; - _netOutputFactory = netOutputFactory; - _winInputFactory = winInputFactory; - _winOutputFactory = winOutputFactory; + _componentFactory = componentFactory; IsLegacy = false; } @@ -92,8 +81,8 @@ private void CreateDriver (string? driverName) { PlatformID p = Environment.OSVersion.Platform; - bool definetlyWin = driverName?.Contains ("win") ?? false; - bool definetlyNet = driverName?.Contains ("net") ?? false; + bool definetlyWin = (driverName?.Contains ("win") ?? false )|| _componentFactory is IComponentFactory; + bool definetlyNet = (driverName?.Contains ("net") ?? false ) || _componentFactory is IComponentFactory; if (definetlyWin) { @@ -125,13 +114,21 @@ private IMainLoopCoordinator CreateWindowsSubcomponents () ConcurrentQueue inputBuffer = new (); MainLoop loop = new (); - return new MainLoopCoordinator ( - _timedEvents, - _winInputFactory, + IComponentFactory cf; + + if (_componentFactory != null) + { + cf = (IComponentFactory)_componentFactory; + } + else + { + cf = new WindowsComponentFactory (); + } + + return new MainLoopCoordinator (_timedEvents, inputBuffer, - new WindowsInputProcessor (inputBuffer), - _winOutputFactory, - loop); + loop, + cf); } private IMainLoopCoordinator CreateNetSubcomponents () @@ -139,13 +136,22 @@ private IMainLoopCoordinator CreateNetSubcomponents () ConcurrentQueue inputBuffer = new (); MainLoop loop = new (); + IComponentFactory cf; + + if (_componentFactory != null) + { + cf = (IComponentFactory)_componentFactory; + } + else + { + cf = new NetComponentFactory (); + } + return new MainLoopCoordinator ( _timedEvents, - _netInputFactory, inputBuffer, - new NetInputProcessor (inputBuffer), - _netOutputFactory, - loop); + loop, + cf); } /// @@ -171,6 +177,12 @@ public override void Run (Toplevel view, Func? errorHandler = n throw new NotInitializedException (nameof (Run)); } + if (Application.Driver == null) + { + // See Run_T_Init_Driver_Cleared_with_TestTopLevel_Throws + throw new InvalidOperationException ("Driver was inexplicably null when trying to Run view"); + } + Application.Top = view; RunState rs = Application.Begin (view); @@ -258,4 +270,4 @@ public override void LayoutAndDraw (bool forceDraw) Application.Top?.SetNeedsDraw(); Application.Top?.SetNeedsLayout (); } -} +} \ No newline at end of file diff --git a/Terminal.Gui/Drivers/V2/ComponentFactory.cs b/Terminal.Gui/Drivers/V2/ComponentFactory.cs new file mode 100644 index 0000000000..3c5adddd2a --- /dev/null +++ b/Terminal.Gui/Drivers/V2/ComponentFactory.cs @@ -0,0 +1,26 @@ +#nullable enable +using System.Collections.Concurrent; + +namespace Terminal.Gui.Drivers; + +/// +/// Abstract base class implementation of +/// +/// +public abstract class ComponentFactory : IComponentFactory +{ + /// + public abstract IConsoleInput CreateInput (); + + /// + public abstract IInputProcessor CreateInputProcessor (ConcurrentQueue inputBuffer); + + /// + public virtual IWindowSizeMonitor CreateWindowSizeMonitor (IConsoleOutput consoleOutput, IOutputBuffer outputBuffer) + { + return new WindowSizeMonitor (consoleOutput, outputBuffer); + } + + /// + public abstract IConsoleOutput CreateOutput (); +} diff --git a/Terminal.Gui/Drivers/V2/ConsoleDriverFacade.cs b/Terminal.Gui/Drivers/V2/ConsoleDriverFacade.cs index c89c63965c..c57f67841a 100644 --- a/Terminal.Gui/Drivers/V2/ConsoleDriverFacade.cs +++ b/Terminal.Gui/Drivers/V2/ConsoleDriverFacade.cs @@ -14,6 +14,10 @@ internal class ConsoleDriverFacade : IConsoleDriver, IConsoleDriverFacade public event EventHandler SizeChanged; public IInputProcessor InputProcessor { get; } + public IOutputBuffer OutputBuffer => _outputBuffer; + + public IWindowSizeMonitor WindowSizeMonitor { get; } + public ConsoleDriverFacade ( IInputProcessor inputProcessor, @@ -36,7 +40,8 @@ IWindowSizeMonitor windowSizeMonitor MouseEvent?.Invoke (s, e); }; - windowSizeMonitor.SizeChanging += (_, e) => SizeChanged?.Invoke (this, e); + WindowSizeMonitor = windowSizeMonitor; + windowSizeMonitor.SizeChanging += (_,e) => SizeChanged?.Invoke (this, e); CreateClipboard (); } @@ -68,7 +73,7 @@ public Rectangle Screen { get { - if (ConsoleDriver.RunningUnitTests) + if (ConsoleDriver.RunningUnitTests && _output is WindowsOutput or NetOutput) { // In unit tests, we don't have a real output, so we return an empty rectangle. return Rectangle.Empty; @@ -384,7 +389,15 @@ public Attribute MakeColor (in Color foreground, in Color background) /// If simulates the Ctrl key being pressed. public void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool ctrl) { - // TODO: implement + ConsoleKeyInfo consoleKeyInfo = new (keyChar, key, shift, alt, ctrl); + + Key k = EscSeqUtils.MapKey (consoleKeyInfo); + + if (InputProcessor.IsValidInput (k, out k)) + { + InputProcessor.OnKeyDown (k); + InputProcessor.OnKeyUp (k); + } } /// diff --git a/Terminal.Gui/Drivers/V2/IComponentFactory.cs b/Terminal.Gui/Drivers/V2/IComponentFactory.cs new file mode 100644 index 0000000000..f4f8767239 --- /dev/null +++ b/Terminal.Gui/Drivers/V2/IComponentFactory.cs @@ -0,0 +1,50 @@ +#nullable enable +using System.Collections.Concurrent; + +namespace Terminal.Gui.Drivers; + +/// +/// Base untyped interface for for methods that are not templated on low level +/// console input type. +/// +public interface IComponentFactory +{ + /// + /// Create the class for the current driver implementation i.e. the class responsible for + /// rendering into the console. + /// + /// + IConsoleOutput CreateOutput (); +} + +/// +/// Creates driver specific subcomponent classes (, etc) for a +/// . +/// +/// +public interface IComponentFactory : IComponentFactory +{ + /// + /// Create class for the current driver implementation i.e. the class responsible for reading + /// user input from the console. + /// + /// + IConsoleInput CreateInput (); + + /// + /// Creates the class for the current driver implementation i.e. the class responsible for + /// translating raw console input into Terminal.Gui common event and . + /// + /// + /// + IInputProcessor CreateInputProcessor (ConcurrentQueue inputBuffer); + + /// + /// Creates class for the current driver implementation i.e. the class responsible for + /// reporting the current size of the terminal window. + /// + /// + /// + /// + IWindowSizeMonitor CreateWindowSizeMonitor (IConsoleOutput consoleOutput, IOutputBuffer outputBuffer); +} diff --git a/Terminal.Gui/Drivers/V2/IConsoleDriverFacade.cs b/Terminal.Gui/Drivers/V2/IConsoleDriverFacade.cs index 2bebf3c9bd..b670a196d3 100644 --- a/Terminal.Gui/Drivers/V2/IConsoleDriverFacade.cs +++ b/Terminal.Gui/Drivers/V2/IConsoleDriverFacade.cs @@ -10,5 +10,16 @@ public interface IConsoleDriverFacade /// e.g. into events /// and detecting and processing ansi escape sequences. /// - public IInputProcessor InputProcessor { get; } + IInputProcessor InputProcessor { get; } + + /// + /// Describes the desired screen state. Data source for . + /// + IOutputBuffer OutputBuffer { get; } + + /// + /// Interface for classes responsible for reporting the current + /// size of the terminal window. + /// + IWindowSizeMonitor WindowSizeMonitor { get; } } diff --git a/Terminal.Gui/Drivers/V2/IInputProcessor.cs b/Terminal.Gui/Drivers/V2/IInputProcessor.cs index 93d5cd7773..2c990db3fc 100644 --- a/Terminal.Gui/Drivers/V2/IInputProcessor.cs +++ b/Terminal.Gui/Drivers/V2/IInputProcessor.cs @@ -58,4 +58,15 @@ public interface IInputProcessor /// /// public IAnsiResponseParser GetParser (); + + /// + /// Handles surrogate pairs in the input stream. + /// + /// The key from input. + /// Get the surrogate pair or the key. + /// + /// if the result is a valid surrogate pair or a valid key, otherwise + /// . + /// + bool IsValidInput (Key key, out Key result); } diff --git a/Terminal.Gui/Drivers/V2/IMainLoop.cs b/Terminal.Gui/Drivers/V2/IMainLoop.cs index 647776cbe3..aee2e381fa 100644 --- a/Terminal.Gui/Drivers/V2/IMainLoop.cs +++ b/Terminal.Gui/Drivers/V2/IMainLoop.cs @@ -48,7 +48,14 @@ public interface IMainLoop : IDisposable /// /// /// - void Initialize (ITimedEvents timedEvents, ConcurrentQueue inputBuffer, IInputProcessor inputProcessor, IConsoleOutput consoleOutput); + /// + void Initialize ( + ITimedEvents timedEvents, + ConcurrentQueue inputBuffer, + IInputProcessor inputProcessor, + IConsoleOutput consoleOutput, + IComponentFactory componentFactory + ); /// /// Perform a single iteration of the main loop then blocks for a fixed length diff --git a/Terminal.Gui/Drivers/V2/IWindowsInput.cs b/Terminal.Gui/Drivers/V2/IWindowsInput.cs index d8431b22fe..17ba0d1774 100644 --- a/Terminal.Gui/Drivers/V2/IWindowsInput.cs +++ b/Terminal.Gui/Drivers/V2/IWindowsInput.cs @@ -1,4 +1,7 @@ namespace Terminal.Gui.Drivers; -internal interface IWindowsInput : IConsoleInput +/// +/// Interface for windows only input which uses low level win32 apis (v2win) +/// +public interface IWindowsInput : IConsoleInput { } diff --git a/Terminal.Gui/Drivers/V2/InputProcessor.cs b/Terminal.Gui/Drivers/V2/InputProcessor.cs index c860ba796e..04a4e3b6c2 100644 --- a/Terminal.Gui/Drivers/V2/InputProcessor.cs +++ b/Terminal.Gui/Drivers/V2/InputProcessor.cs @@ -165,7 +165,8 @@ private IEnumerable ReleaseParserHeldKeysIfStale () internal char _highSurrogate = '\0'; - internal bool IsValidInput (Key key, out Key result) + /// + public bool IsValidInput (Key key, out Key result) { result = key; @@ -179,6 +180,22 @@ internal bool IsValidInput (Key key, out Key result) if (_highSurrogate > 0 && char.IsLowSurrogate ((char)key)) { result = (KeyCode)new Rune (_highSurrogate, (char)key).Value; + + if (key.IsAlt) + { + result = result.WithAlt; + } + + if (key.IsCtrl) + { + result = result.WithCtrl; + } + + if (key.IsShift) + { + result = result.WithShift; + } + _highSurrogate = '\0'; return true; diff --git a/Terminal.Gui/Drivers/V2/MainLoop.cs b/Terminal.Gui/Drivers/V2/MainLoop.cs index 5b6d9fdde7..a429d42310 100644 --- a/Terminal.Gui/Drivers/V2/MainLoop.cs +++ b/Terminal.Gui/Drivers/V2/MainLoop.cs @@ -83,7 +83,14 @@ public IWindowSizeMonitor WindowSizeMonitor /// /// /// - public void Initialize (ITimedEvents timedEvents, ConcurrentQueue inputBuffer, IInputProcessor inputProcessor, IConsoleOutput consoleOutput) + /// + public void Initialize ( + ITimedEvents timedEvents, + ConcurrentQueue inputBuffer, + IInputProcessor inputProcessor, + IConsoleOutput consoleOutput, + IComponentFactory componentFactory + ) { InputBuffer = inputBuffer; Out = consoleOutput; @@ -92,18 +99,22 @@ public void Initialize (ITimedEvents timedEvents, ConcurrentQueue inputBuffer TimedEvents = timedEvents; AnsiRequestScheduler = new (InputProcessor.GetParser ()); - WindowSizeMonitor = new WindowSizeMonitor (Out, OutputBuffer); + WindowSizeMonitor = componentFactory.CreateWindowSizeMonitor (Out, OutputBuffer); } /// public void Iteration () { + + Application.RaiseIteration (); + DateTime dt = Now (); + int timeAllowed = 1000 / Math.Max(1,(int)Application.MaximumIterationsPerSecond); IterationImpl (); TimeSpan took = Now () - dt; - TimeSpan sleepFor = TimeSpan.FromMilliseconds (50) - took; + TimeSpan sleepFor = TimeSpan.FromMilliseconds (timeAllowed) - took; Logging.TotalIterationMetric.Record (took.Milliseconds); @@ -123,7 +134,8 @@ internal void IterationImpl () if (Application.Top != null) { bool needsDrawOrLayout = AnySubViewsNeedDrawn (Application.Popover?.GetActivePopover () as View) - || AnySubViewsNeedDrawn (Application.Top); + || AnySubViewsNeedDrawn (Application.Top) + || (Application.MouseGrabHandler.MouseGrabView != null && AnySubViewsNeedDrawn (Application.MouseGrabHandler.MouseGrabView)); bool sizeChanged = WindowSizeMonitor.Poll (); diff --git a/Terminal.Gui/Drivers/V2/MainLoopCoordinator.cs b/Terminal.Gui/Drivers/V2/MainLoopCoordinator.cs index a70b089567..d1d5153416 100644 --- a/Terminal.Gui/Drivers/V2/MainLoopCoordinator.cs +++ b/Terminal.Gui/Drivers/V2/MainLoopCoordinator.cs @@ -13,12 +13,11 @@ namespace Terminal.Gui.Drivers; /// internal class MainLoopCoordinator : IMainLoopCoordinator { - private readonly Func> _inputFactory; private readonly ConcurrentQueue _inputBuffer; private readonly IInputProcessor _inputProcessor; private readonly IMainLoop _loop; + private readonly IComponentFactory _componentFactory; private readonly CancellationTokenSource _tokenSource = new (); - private readonly Func _outputFactory; private IConsoleInput _input; private IConsoleOutput _output; private readonly object _oLockInitialization = new (); @@ -32,34 +31,22 @@ internal class MainLoopCoordinator : IMainLoopCoordinator /// Creates a new coordinator /// /// - /// - /// Function to create a new input. This must call - /// explicitly and cannot return an existing instance. This requirement arises because Windows - /// console screen buffer APIs are thread-specific for certain operations. - /// /// - /// - /// - /// Function to create a new output. This must call - /// explicitly and cannot return an existing instance. This requirement arises because Windows - /// console screen buffer APIs are thread-specific for certain operations. - /// /// + /// Factory for creating driver components + /// (, etc) public MainLoopCoordinator ( ITimedEvents timedEvents, - Func> inputFactory, ConcurrentQueue inputBuffer, - IInputProcessor inputProcessor, - Func outputFactory, - IMainLoop loop + IMainLoop loop, + IComponentFactory componentFactory ) { _timedEvents = timedEvents; - _inputFactory = inputFactory; _inputBuffer = inputBuffer; - _inputProcessor = inputProcessor; - _outputFactory = outputFactory; + _inputProcessor = componentFactory.CreateInputProcessor (_inputBuffer); _loop = loop; + _componentFactory = componentFactory; } /// @@ -89,7 +76,7 @@ public async Task StartAsync () throw _inputTask.Exception; } - throw new ("Input loop exited during startup instead of entering read loop properly (i.e. and blocking)"); + Logging.Logger.LogCritical("Input loop exited during startup instead of entering read loop properly (i.e. and blocking)"); } Logging.Logger.LogInformation ("Main Loop Coordinator booting complete"); @@ -102,7 +89,7 @@ private void RunInput () lock (_oLockInitialization) { // Instance must be constructed on the thread in which it is used. - _input = _inputFactory.Invoke (); + _input = _componentFactory.CreateInput (); _input.Initialize (_inputBuffer); BuildFacadeIfPossible (); @@ -142,8 +129,8 @@ private void BootMainLoop () lock (_oLockInitialization) { // Instance must be constructed on the thread in which it is used. - _output = _outputFactory.Invoke (); - _loop.Initialize (_timedEvents, _inputBuffer, _inputProcessor, _output); + _output = _componentFactory.CreateOutput (); + _loop.Initialize (_timedEvents, _inputBuffer, _inputProcessor, _output,_componentFactory); BuildFacadeIfPossible (); } diff --git a/Terminal.Gui/Drivers/V2/NetComponentFactory.cs b/Terminal.Gui/Drivers/V2/NetComponentFactory.cs new file mode 100644 index 0000000000..3b682d1fc1 --- /dev/null +++ b/Terminal.Gui/Drivers/V2/NetComponentFactory.cs @@ -0,0 +1,29 @@ +#nullable enable +using System.Collections.Concurrent; + +namespace Terminal.Gui.Drivers; + +/// +/// implementation for native csharp console I/O i.e. v2net. +/// This factory creates instances of internal classes , etc. +/// +public class NetComponentFactory : ComponentFactory +{ + /// + public override IConsoleInput CreateInput () + { + return new NetInput (); + } + + /// + public override IConsoleOutput CreateOutput () + { + return new NetOutput (); + } + + /// + public override IInputProcessor CreateInputProcessor (ConcurrentQueue inputBuffer) + { + return new NetInputProcessor (inputBuffer); + } +} diff --git a/Terminal.Gui/Drivers/V2/NetOutput.cs b/Terminal.Gui/Drivers/V2/NetOutput.cs index 17956a3dfa..eea6b3edf2 100644 --- a/Terminal.Gui/Drivers/V2/NetOutput.cs +++ b/Terminal.Gui/Drivers/V2/NetOutput.cs @@ -28,7 +28,11 @@ public NetOutput () } /// - public void Write (ReadOnlySpan text) { Console.Out.Write (text); } + public void Write (ReadOnlySpan text) + { + Console.Out.Write (text); + } + /// public Size GetWindowSize () @@ -67,9 +71,14 @@ protected override void AppendOrWriteAttribute (StringBuilder output, Attribute EscSeqUtils.CSI_AppendTextStyleChange (output, redrawTextStyle, attr.Style); } - /// - protected override void Write (StringBuilder output) { Console.Out.Write (output); } + /// + protected override void Write (StringBuilder output) + { + Console.Out.Write (output); + } + + /// protected override bool SetCursorPositionImpl (int col, int row) { if (_lastCursorPosition is { } && _lastCursorPosition.Value.X == col && _lastCursorPosition.Value.Y == row) @@ -102,9 +111,12 @@ protected override bool SetCursorPositionImpl (int col, int row) } /// - public void Dispose () { } + public void Dispose () + { + } - /// + + /// public override void SetCursorVisibility (CursorVisibility visibility) { Console.Out.Write (visibility == CursorVisibility.Default ? EscSeqUtils.CSI_ShowCursor : EscSeqUtils.CSI_HideCursor); diff --git a/Terminal.Gui/Drivers/V2/OutputBase.cs b/Terminal.Gui/Drivers/V2/OutputBase.cs index b28551e4bd..6be2e2b89e 100644 --- a/Terminal.Gui/Drivers/V2/OutputBase.cs +++ b/Terminal.Gui/Drivers/V2/OutputBase.cs @@ -1,5 +1,14 @@ -namespace Terminal.Gui.Drivers; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +namespace Terminal.Gui.Drivers; + +/// +/// Abstract base class to assist with implementing . +/// public abstract class OutputBase { private CursorVisibility? _cachedCursorVisibility; @@ -7,7 +16,7 @@ public abstract class OutputBase // Last text style used, for updating style with EscSeqUtils.CSI_AppendTextStyleChange(). private TextStyle _redrawTextStyle = TextStyle.None; - /// + /// public virtual void Write (IOutputBuffer buffer) { if (ConsoleDriver.RunningUnitTests) @@ -144,6 +153,14 @@ public virtual void Write (IOutputBuffer buffer) _cachedCursorVisibility = savedVisibility; } + /// + /// Changes the color and text style of the console to the given and . + /// If command can be buffered in line with other output (e.g. CSI sequence) then it should be appended to + /// otherwise the relevant output state should be flushed directly (e.g. by calling relevant win 32 API method) + /// + /// + /// + /// protected abstract void AppendOrWriteAttribute (StringBuilder output, Attribute attr, TextStyle redrawTextStyle); private void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int outputWidth) @@ -155,9 +172,24 @@ private void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref outputWidth = 0; } + /// + /// Output the contents of the to the console. + /// + /// protected abstract void Write (StringBuilder output); + /// + /// When overriden in derived class, positions the terminal output cursor to the specified point on the screen. + /// + /// Column to move cursor to + /// Row to move cursor to + /// protected abstract bool SetCursorPositionImpl (int screenPositionX, int screenPositionY); + /// + /// Changes the visibility of the cursor in the terminal to the specified e.g. + /// the flashing indicator, invisible, box indicator etc. + /// + /// public abstract void SetCursorVisibility (CursorVisibility visibility); } diff --git a/Terminal.Gui/Drivers/V2/OutputBuffer.cs b/Terminal.Gui/Drivers/V2/OutputBuffer.cs index fa44d56309..a424bbfd9b 100644 --- a/Terminal.Gui/Drivers/V2/OutputBuffer.cs +++ b/Terminal.Gui/Drivers/V2/OutputBuffer.cs @@ -141,6 +141,8 @@ public void AddRune (Rune rune) return; } + Clip ??= new Region (Screen); + Rectangle clipRect = Clip!.GetBounds (); if (validLocation) diff --git a/Terminal.Gui/Drivers/V2/ToplevelTransitionManager.cs b/Terminal.Gui/Drivers/V2/ToplevelTransitionManager.cs index 6a12f0861c..4e5937ac32 100644 --- a/Terminal.Gui/Drivers/V2/ToplevelTransitionManager.cs +++ b/Terminal.Gui/Drivers/V2/ToplevelTransitionManager.cs @@ -20,6 +20,9 @@ public void RaiseReadyEventIfNeeded () { top.OnReady (); _readiedTopLevels.Add (top); + + // Views can be closed and opened and run again multiple times, see End_Does_Not_Dispose + top.Closed += (s, e) => _readiedTopLevels.Remove (top); } } diff --git a/Terminal.Gui/Drivers/V2/WindowsComponentFactory.cs b/Terminal.Gui/Drivers/V2/WindowsComponentFactory.cs new file mode 100644 index 0000000000..6436ddc834 --- /dev/null +++ b/Terminal.Gui/Drivers/V2/WindowsComponentFactory.cs @@ -0,0 +1,29 @@ +#nullable enable +using System.Collections.Concurrent; + +namespace Terminal.Gui.Drivers; + +/// +/// implementation for win32 windows only I/O i.e. v2win. +/// This factory creates instances of internal classes , etc. +/// +public class WindowsComponentFactory : ComponentFactory +{ + /// + public override IConsoleInput CreateInput () + { + return new WindowsInput (); + } + + /// + public override IInputProcessor CreateInputProcessor (ConcurrentQueue inputBuffer) + { + return new WindowsInputProcessor (inputBuffer); + } + + /// + public override IConsoleOutput CreateOutput () + { + return new WindowsOutput (); + } +} diff --git a/Terminal.Gui/Drivers/V2/WindowsOutput.cs b/Terminal.Gui/Drivers/V2/WindowsOutput.cs index 5152d3a238..2e42ae3fc4 100644 --- a/Terminal.Gui/Drivers/V2/WindowsOutput.cs +++ b/Terminal.Gui/Drivers/V2/WindowsOutput.cs @@ -431,7 +431,7 @@ protected override bool SetCursorPositionImpl (int screenPositionX, int screenPo return true; } - /// + /// public override void SetCursorVisibility (CursorVisibility visibility) { if (ConsoleDriver.RunningUnitTests) diff --git a/Terminal.Gui/Drivers/WindowsDriver/WindowsConsole.cs b/Terminal.Gui/Drivers/WindowsDriver/WindowsConsole.cs index 445ba1410e..ba3dee5993 100644 --- a/Terminal.Gui/Drivers/WindowsDriver/WindowsConsole.cs +++ b/Terminal.Gui/Drivers/WindowsDriver/WindowsConsole.cs @@ -5,7 +5,7 @@ namespace Terminal.Gui.Drivers; -internal partial class WindowsConsole +public partial class WindowsConsole { private CancellationTokenSource? _inputReadyCancellationTokenSource; private readonly BlockingCollection _inputQueue = new (new ConcurrentQueue ()); diff --git a/Terminal.Gui/ViewBase/Adornment/ShadowView.cs b/Terminal.Gui/ViewBase/Adornment/ShadowView.cs index 12f2e08d93..a2d2eb5773 100644 --- a/Terminal.Gui/ViewBase/Adornment/ShadowView.cs +++ b/Terminal.Gui/ViewBase/Adornment/ShadowView.cs @@ -151,6 +151,13 @@ private Attribute GetAttributeUnderLocation (Point location) return Attribute.Default; } + if (Driver?.Contents == null || + location.Y < 0 || location.Y >= Driver.Contents.GetLength (0) || + location.X < 0 || location.X >= Driver.Contents.GetLength (1)) + { + return Attribute.Default; + } + Attribute attr = Driver!.Contents! [location.Y, location.X].Attribute!.Value; var newAttribute = diff --git a/Terminal.Gui/Views/Dialog.cs b/Terminal.Gui/Views/Dialog.cs index 88f2af02c7..41f6ac7ab9 100644 --- a/Terminal.Gui/Views/Dialog.cs +++ b/Terminal.Gui/Views/Dialog.cs @@ -109,12 +109,6 @@ public bool Canceled { get { -#if DEBUG_IDISPOSABLE - if (EnableDebugIDisposableAsserts && WasDisposed) - { - throw new ObjectDisposedException (GetType ().FullName); - } -#endif return _canceled; } set diff --git a/Tests/IntegrationTests/FluentTests/BasicFluentAssertionTests.cs b/Tests/IntegrationTests/FluentTests/BasicFluentAssertionTests.cs index 0796d5f005..24cb509201 100644 --- a/Tests/IntegrationTests/FluentTests/BasicFluentAssertionTests.cs +++ b/Tests/IntegrationTests/FluentTests/BasicFluentAssertionTests.cs @@ -1,4 +1,5 @@ using TerminalGuiFluentTesting; +using TerminalGuiFluentTestingXunit; using Xunit.Abstractions; namespace IntegrationTests.FluentTests; @@ -7,16 +8,13 @@ public class BasicFluentAssertionTests { private readonly TextWriter _out; - public BasicFluentAssertionTests (ITestOutputHelper outputHelper) - { - _out = new TestOutputWriter (outputHelper); - } + public BasicFluentAssertionTests (ITestOutputHelper outputHelper) { _out = new TestOutputWriter (outputHelper); } [Theory] [ClassData (typeof (V2TestDrivers))] public void GuiTestContext_NewInstance_Runs (V2TestDriver d) { - using GuiTestContext context = With.A (40, 10, d); + using GuiTestContext context = With.A (40, 10, d, _out); Assert.True (Application.Top!.Running); context.WriteOutLogs (_out); @@ -34,9 +32,6 @@ public void GuiTestContext_QuitKey_Stops (V2TestDriver d) context.RaiseKeyDownEvent (Application.QuitKey); Assert.False (top!.Running); - Application.Top?.Dispose (); - Application.Shutdown (); - context.WriteOutLogs (_out); context.Stop (); } @@ -69,9 +64,10 @@ public void TestWindowsResize (V2TestDriver d) using GuiTestContext c = With.A (40, 10, d) .Add (lbl) - .Then (() => Assert.Equal (38, lbl.Frame.Width)) // Window has 2 border + .AssertEqual (38, lbl.Frame.Width) // Window has 2 border .ResizeConsole (20, 20) - .Then (() => Assert.Equal (18, lbl.Frame.Width)) + .WaitIteration () + .AssertEqual (18, lbl.Frame.Width) .WriteOutLogs (_out) .Stop (); } @@ -85,7 +81,7 @@ public void ContextMenu_CrashesOnRight (V2TestDriver d) MenuItemv2 [] menuItems = [new ("_New File", string.Empty, () => { clicked = true; })]; using GuiTestContext c = With.A (40, 10, d) - .WithContextMenu (new PopoverMenu (menuItems)) + .WithContextMenu (new (menuItems)) .ScreenShot ("Before open menu", _out) // Click in main area inside border @@ -98,7 +94,6 @@ public void ContextMenu_CrashesOnRight (V2TestDriver d) Assert.NotNull (popover); var popoverMenu = popover as PopoverMenu; popoverMenu!.Root!.BorderStyle = LineStyle.Single; - }) .WaitIteration () .ScreenShot ("After open menu", _out) @@ -114,26 +109,30 @@ public void ContextMenu_OpenSubmenu (V2TestDriver d) { var clicked = false; - MenuItemv2 [] menuItems = [ - new ("One", "", null), - new ("Two", "", null), - new ("Three", "", null), - new ("Four", "", new ( - [ - new ("SubMenu1", "", null), - new ("SubMenu2", "", ()=>clicked=true), - new ("SubMenu3", "", null), - new ("SubMenu4", "", null), - new ("SubMenu5", "", null), - new ("SubMenu6", "", null), - new ("SubMenu7", "", null) - ])), - new ("Five", "", null), - new ("Six", "", null) - ]; + MenuItemv2 [] menuItems = + [ + new ("One", "", null), + new ("Two", "", null), + new ("Three", "", null), + new ( + "Four", + "", + new ( + [ + new ("SubMenu1", "", null), + new ("SubMenu2", "", () => clicked = true), + new ("SubMenu3", "", null), + new ("SubMenu4", "", null), + new ("SubMenu5", "", null), + new ("SubMenu6", "", null), + new ("SubMenu7", "", null) + ])), + new ("Five", "", null), + new ("Six", "", null) + ]; using GuiTestContext c = With.A (40, 10, d) - .WithContextMenu (new PopoverMenu (menuItems)) + .WithContextMenu (new (menuItems)) .ScreenShot ("Before open menu", _out) // Click in main area inside border @@ -177,43 +176,43 @@ public void Toplevel_TabGroup_Forward_Backward (V2TestDriver d) Application.Top!.Add (w1, w2, w3); }) .WaitIteration () - .Then (() => Assert.True (v5.HasFocus)) + .AssertTrue (v5.HasFocus) .RaiseKeyDownEvent (Key.F6) - .Then (() => Assert.True (v1.HasFocus)) + .AssertTrue (v1.HasFocus) .RaiseKeyDownEvent (Key.F6) - .Then (() => Assert.True (v3.HasFocus)) + .AssertTrue (v3.HasFocus) .RaiseKeyDownEvent (Key.F6.WithShift) - .Then (() => Assert.True (v1.HasFocus)) + .AssertTrue (v1.HasFocus) .RaiseKeyDownEvent (Key.F6.WithShift) - .Then (() => Assert.True (v5.HasFocus)) + .AssertTrue (v5.HasFocus) .RaiseKeyDownEvent (Key.F6.WithShift) - .Then (() => Assert.True (v3.HasFocus)) + .AssertTrue (v3.HasFocus) .RaiseKeyDownEvent (Key.F6) - .Then (() => Assert.True (v5.HasFocus)) + .AssertTrue (v5.HasFocus) .RaiseKeyDownEvent (Key.F6) - .Then (() => Assert.True (v1.HasFocus)) + .AssertTrue (v1.HasFocus) .RaiseKeyDownEvent (Key.F6) - .Then (() => Assert.True (v3.HasFocus)) + .AssertTrue (v3.HasFocus) .RaiseKeyDownEvent (Key.F6.WithShift) - .Then (() => Assert.True (v1.HasFocus)) + .AssertTrue (v1.HasFocus) .RaiseKeyDownEvent (Key.F6.WithShift) - .Then (() => Assert.True (v5.HasFocus)) + .AssertTrue (v5.HasFocus) .RaiseKeyDownEvent (Key.F6.WithShift) - .Then (() => Assert.True (v3.HasFocus)) + .AssertTrue (v3.HasFocus) .RaiseKeyDownEvent (Key.Tab) - .Then (() => Assert.True (v4.HasFocus)) + .AssertTrue (v4.HasFocus) .RaiseKeyDownEvent (Key.F6) - .Then (() => Assert.True (v5.HasFocus)) + .AssertTrue (v5.HasFocus) .RaiseKeyDownEvent (Key.F6) - .Then (() => Assert.True (v1.HasFocus)) + .AssertTrue (v1.HasFocus) .RaiseKeyDownEvent (Key.F6.WithShift) - .Then (() => Assert.True (v5.HasFocus)) + .AssertTrue (v5.HasFocus) .RaiseKeyDownEvent (Key.Tab) - .Then (() => Assert.True (v6.HasFocus)) + .AssertTrue (v6.HasFocus) .RaiseKeyDownEvent (Key.F6.WithShift) - .Then (() => Assert.True (v4.HasFocus)) + .AssertTrue (v4.HasFocus) .RaiseKeyDownEvent (Key.F6) - .Then (() => Assert.True (v6.HasFocus)) + .AssertTrue (v6.HasFocus) .WriteOutLogs (_out) .Stop (); Assert.False (v1.HasFocus); @@ -221,6 +220,5 @@ public void Toplevel_TabGroup_Forward_Backward (V2TestDriver d) Assert.False (v3.HasFocus); Assert.False (v4.HasFocus); Assert.False (v5.HasFocus); - Assert.False (v6.HasFocus); } } diff --git a/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs b/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs index 47a819fc78..c8fea9d15e 100644 --- a/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs +++ b/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs @@ -41,15 +41,28 @@ private MockFileSystem CreateExampleFileSystem () return mockFileSystem; } + private Toplevel NewSaveDialog (out SaveDialog sd, bool modal = true) + { + return NewSaveDialog (out sd, out _, modal); + } + + private Toplevel NewSaveDialog (out SaveDialog sd, out MockFileSystem fs,bool modal = true) + { + fs = CreateExampleFileSystem (); + sd = new SaveDialog (fs) { Modal = modal }; + return sd; + } + + [Theory] [ClassData (typeof (V2TestDrivers))] public void CancelFileDialog_UsingEscape (V2TestDriver d) { - var sd = new SaveDialog (CreateExampleFileSystem ()); - using var c = With.A (sd, 100, 20, d) + SaveDialog? sd = null; + using var c = With.A (()=>NewSaveDialog(out sd), 100, 20, d) .ScreenShot ("Save dialog", _out) .Escape () - .Then (() => Assert.True (sd.Canceled)) + .AssertTrue (sd!.Canceled) .Stop (); } @@ -57,11 +70,11 @@ public void CancelFileDialog_UsingEscape (V2TestDriver d) [ClassData (typeof (V2TestDrivers))] public void CancelFileDialog_UsingCancelButton_TabThenEnter (V2TestDriver d) { - var sd = new SaveDialog (CreateExampleFileSystem ()) { Modal = false }; - using var c = With.A (sd, 100, 20, d) + SaveDialog? sd = null; + using var c = With.A (() => NewSaveDialog (out sd,modal:false), 100, 20, d) .ScreenShot ("Save dialog", _out) .Focus