diff --git a/Examples/UICatalog/Scenarios/SendKeys.cs b/Examples/UICatalog/Scenarios/SendKeys.cs index 04a57d4e4..4e5591559 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 class SendKeys : Scenario txtResult.KeyDown += (s, e) => { - rKeys += (char)e.KeyCode; + rKeys += e.ToString (); if (!IsShift && e.IsShift) { @@ -81,17 +81,15 @@ public class SendKeys : Scenario 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 32127a49c..756290cc2 100644 --- a/Terminal.Gui/App/Application.Run.cs +++ b/Terminal.Gui/App/Application.Run.cs @@ -460,7 +460,7 @@ public static partial class Application // Run (Begin, Run, End, Stop) /// 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 partial class Application // Run (Begin, Run, End, Stop) 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 7741b12b1..e1012c85c 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 ca94ebe57..a3964328f 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 () { - _netInputFactory = netInputFactory; - _netOutputFactory = netOutputFactory; - _winInputFactory = winInputFactory; - _winOutputFactory = winOutputFactory; + IsLegacy = false; + } + + internal ApplicationV2 (IComponentFactory componentFactory) + { + _componentFactory = componentFactory; IsLegacy = false; } @@ -92,8 +81,8 @@ public class ApplicationV2 : ApplicationImpl { 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 @@ public class ApplicationV2 : ApplicationImpl 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 @@ public class ApplicationV2 : ApplicationImpl 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 class ApplicationV2 : ApplicationImpl 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 class ApplicationV2 : ApplicationImpl 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 000000000..3c5adddd2 --- /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 c89c63965..c57f67841 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 @@ internal class ConsoleDriverFacade : IConsoleDriver, IConsoleDriverFacade 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 @@ internal class ConsoleDriverFacade : IConsoleDriver, IConsoleDriverFacade { 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 @@ internal class ConsoleDriverFacade : IConsoleDriver, IConsoleDriverFacade /// 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 000000000..f4f876723 --- /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 2bebf3c9b..b670a196d 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 93d5cd777..2c990db3f 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 647776cbe..aee2e381f 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 d8431b22f..17ba0d177 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 c860ba796..04a4e3b6c 100644 --- a/Terminal.Gui/Drivers/V2/InputProcessor.cs +++ b/Terminal.Gui/Drivers/V2/InputProcessor.cs @@ -165,7 +165,8 @@ public abstract class InputProcessor : IInputProcessor 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 @@ public abstract class InputProcessor : IInputProcessor 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 5b6d9fdde..a429d4231 100644 --- a/Terminal.Gui/Drivers/V2/MainLoop.cs +++ b/Terminal.Gui/Drivers/V2/MainLoop.cs @@ -83,7 +83,14 @@ public class MainLoop : IMainLoop /// /// /// - 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 class MainLoop : IMainLoop 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 @@ public class MainLoop : IMainLoop 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 a70b08956..d1d515341 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 @@ internal class MainLoopCoordinator : IMainLoopCoordinator 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 @@ internal class MainLoopCoordinator : IMainLoopCoordinator 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 @@ internal class MainLoopCoordinator : IMainLoopCoordinator 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 000000000..3b682d1fc --- /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 17956a3df..eea6b3edf 100644 --- a/Terminal.Gui/Drivers/V2/NetOutput.cs +++ b/Terminal.Gui/Drivers/V2/NetOutput.cs @@ -28,7 +28,11 @@ public class NetOutput : OutputBase, IConsoleOutput } /// - 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 @@ public class NetOutput : OutputBase, IConsoleOutput 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 @@ public class NetOutput : OutputBase, IConsoleOutput } /// - 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 b28551e4b..6be2e2b89 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 abstract class OutputBase _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 @@ public abstract class OutputBase 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 fa44d5630..a424bbfd9 100644 --- a/Terminal.Gui/Drivers/V2/OutputBuffer.cs +++ b/Terminal.Gui/Drivers/V2/OutputBuffer.cs @@ -141,6 +141,8 @@ public class OutputBuffer : IOutputBuffer 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 6a12f0861..4e5937ac3 100644 --- a/Terminal.Gui/Drivers/V2/ToplevelTransitionManager.cs +++ b/Terminal.Gui/Drivers/V2/ToplevelTransitionManager.cs @@ -20,6 +20,9 @@ public class ToplevelTransitionManager : IToplevelTransitionManager { 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 000000000..6436ddc83 --- /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 5152d3a23..2e42ae3fc 100644 --- a/Terminal.Gui/Drivers/V2/WindowsOutput.cs +++ b/Terminal.Gui/Drivers/V2/WindowsOutput.cs @@ -431,7 +431,7 @@ internal partial class WindowsOutput : OutputBase, IConsoleOutput 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 445ba1410..ba3dee599 100644 --- a/Terminal.Gui/Drivers/WindowsDriver/WindowsConsole.cs +++ b/Terminal.Gui/Drivers/WindowsDriver/WindowsConsole.cs @@ -5,7 +5,7 @@ using System.Runtime.InteropServices; 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 12f2e08d9..a2d2eb577 100644 --- a/Terminal.Gui/ViewBase/Adornment/ShadowView.cs +++ b/Terminal.Gui/ViewBase/Adornment/ShadowView.cs @@ -151,6 +151,13 @@ internal class ShadowView : View 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 88f2af02c..41f6ac7ab 100644 --- a/Terminal.Gui/Views/Dialog.cs +++ b/Terminal.Gui/Views/Dialog.cs @@ -109,12 +109,6 @@ public class Dialog : Window { 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 0796d5f00..24cb50920 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 class BasicFluentAssertionTests context.RaiseKeyDownEvent (Application.QuitKey); Assert.False (top!.Running); - Application.Top?.Dispose (); - Application.Shutdown (); - context.WriteOutLogs (_out); context.Stop (); } @@ -69,9 +64,10 @@ public class BasicFluentAssertionTests 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 class BasicFluentAssertionTests 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 class BasicFluentAssertionTests Assert.NotNull (popover); var popoverMenu = popover as PopoverMenu; popoverMenu!.Root!.BorderStyle = LineStyle.Single; - }) .WaitIteration () .ScreenShot ("After open menu", _out) @@ -114,26 +109,30 @@ public class BasicFluentAssertionTests { 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 class BasicFluentAssertionTests 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 class BasicFluentAssertionTests 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 47a819fc7..c8fea9d15 100644 --- a/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs +++ b/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs @@ -41,15 +41,28 @@ public class FileDialogFluentTests 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 class FileDialogFluentTests [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