Split more WindowDriver classes and #nullable enable.

This commit is contained in:
BDisp
2024-11-07 14:46:27 +00:00
parent 3e952df5f7
commit 44d5997460
4 changed files with 492 additions and 483 deletions

View File

@@ -1,4 +1,4 @@
// TODO: #nullable enable
#nullable enable
//
// WindowsDriver.cs: Windows specific driver
//
@@ -9,14 +9,13 @@
// 2) The values provided during Init (and the first WindowsConsole.EventType.WindowBufferSize) are not correct.
//
// If HACK_CHECK_WINCHANGED is defined then we ignore WindowsConsole.EventType.WindowBufferSize events
// and instead check the console size every 500ms in a thread in WidowsMainLoop.
// As of Windows 11 23H2 25947.1000 and/or WT 1.19.2682 tearing no longer occurs when using
// and instead check the console size every 500ms in a thread in WidowsMainLoop.
// As of Windows 11 23H2 25947.1000 and/or WT 1.19.2682 tearing no longer occurs when using
// the WindowsConsole.EventType.WindowBufferSize event. However, on Init the window size is
// still incorrect so we still need this hack.
//#define HACK_CHECK_WINCHANGED
using System.Collections.Concurrent;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
@@ -35,7 +34,7 @@ internal class WindowsDriver : ConsoleDriver
private bool _isOneFingerDoubleClicked;
private WindowsConsole.ButtonState? _lastMouseButtonPressed;
private WindowsMainLoop _mainLoopDriver;
private WindowsMainLoop? _mainLoopDriver;
private WindowsConsole.ExtendedCharInfo [] _outputBuffer;
private Point? _point;
private Point _pointMove;
@@ -45,7 +44,7 @@ internal class WindowsDriver : ConsoleDriver
{
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
{
WinConsole = new WindowsConsole ();
WinConsole = new ();
// otherwise we're probably running in unit tests
Clipboard = new WindowsClipboard ();
@@ -68,7 +67,7 @@ internal class WindowsDriver : ConsoleDriver
public override bool SupportsTrueColor => RunningUnitTests || (Environment.OSVersion.Version.Build >= 14931 && _isWindowsTerminal);
public WindowsConsole WinConsole { get; private set; }
public WindowsConsole? WinConsole { get; private set; }
public WindowsConsole.KeyEventRecord FromVKPacketToKeyEventRecord (WindowsConsole.KeyEventRecord keyEvent)
{
@@ -202,7 +201,7 @@ internal class WindowsDriver : ConsoleDriver
private readonly CancellationTokenSource _ansiResponseTokenSource = new ();
/// <inheritdoc/>
public override string WriteAnsiRequest (AnsiEscapeSequenceRequest ansiRequest)
public override string? WriteAnsiRequest (AnsiEscapeSequenceRequest ansiRequest)
{
if (_mainLoopDriver is null)
{
@@ -242,12 +241,12 @@ internal class WindowsDriver : ConsoleDriver
{
_mainLoopDriver._forceRead = false;
if (_mainLoopDriver.EscSeqRequests.Statuses.TryPeek (out AnsiEscapeSequenceRequestStatus request))
if (_mainLoopDriver.EscSeqRequests.Statuses.TryPeek (out AnsiEscapeSequenceRequestStatus? request))
{
if (_mainLoopDriver.EscSeqRequests.Statuses.Count > 0
&& string.IsNullOrEmpty (request.AnsiRequest.Response))
{
lock (request!.AnsiRequest._responseLock)
lock (request.AnsiRequest._responseLock)
{
// Bad request or no response at all
_mainLoopDriver.EscSeqRequests.Statuses.TryDequeue (out _);
@@ -404,7 +403,7 @@ internal class WindowsDriver : ConsoleDriver
for (var row = 0; row < Rows; row++)
{
if (!_dirtyLines [row])
if (!_dirtyLines! [row])
{
continue;
}
@@ -414,7 +413,7 @@ internal class WindowsDriver : ConsoleDriver
for (var col = 0; col < Cols; col++)
{
int position = row * Cols + col;
_outputBuffer [position].Attribute = Contents [row, col].Attribute.GetValueOrDefault ();
_outputBuffer [position].Attribute = Contents! [row, col].Attribute.GetValueOrDefault ();
if (Contents [row, col].IsDirty == false)
{
@@ -504,7 +503,7 @@ internal class WindowsDriver : ConsoleDriver
{
// BUGBUG: The results from GetConsoleOutputWindow are incorrect when called from Init.
// Our thread in WindowsMainLoop.CheckWin will get the correct results. See #if HACK_CHECK_WINCHANGED
Size winSize = WinConsole.GetConsoleOutputWindow (out Point pos);
Size winSize = WinConsole.GetConsoleOutputWindow (out Point _);
Cols = winSize.Width;
Rows = winSize.Height;
}
@@ -592,7 +591,7 @@ internal class WindowsDriver : ConsoleDriver
case WindowsConsole.EventType.Mouse:
MouseEventArgs me = ToDriverMouse (inputEvent.MouseEvent);
if (me is null || me.Flags == MouseFlags.None)
if (me.Flags == MouseFlags.None)
{
break;
}
@@ -717,7 +716,7 @@ internal class WindowsDriver : ConsoleDriver
if (mapResult == 0)
{
// There is no mapping - this should not happen
Debug.Assert (mapResult != 0, $@"Unable to map the virtual key code {keyInfo.Key}.");
Debug.Assert (true, $@"Unable to map the virtual key code {keyInfo.Key}.");
return KeyCode.Null;
}
@@ -727,13 +726,13 @@ internal class WindowsDriver : ConsoleDriver
if (keyInfo.KeyChar == 0)
{
// If the keyChar is 0, keyInfo.Key value is not a printable character.
// If the keyChar is 0, keyInfo.Key value is not a printable character.
// Dead keys (diacritics) are indicated by setting the top bit of the return value.
// Dead keys (diacritics) are indicated by setting the top bit of the return value.
if ((mapResult & 0x80000000) != 0)
{
// Dead key (e.g. Oem2 '~'/'^' on POR keyboard)
// Option 1: Throw it out.
// Option 1: Throw it out.
// - Apps will never see the dead keys
// - If user presses a key that can be combined with the dead key ('a'), the right thing happens (app will see '<27>').
// - NOTE: With Dead Keys, KeyDown != KeyUp. The KeyUp event will have just the base char ('a').
@@ -754,7 +753,7 @@ internal class WindowsDriver : ConsoleDriver
if (keyInfo.Modifiers != 0)
{
// These Oem keys have well-defined chars. We ensure the representative char is used.
// If we don't do this, then on some keyboard layouts the wrong char is
// If we don't do this, then on some keyboard layouts the wrong char is
// returned (e.g. on ENG OemPlus un-shifted is =, not +). This is important
// for key persistence ("Ctrl++" vs. "Ctrl+=").
mappedChar = keyInfo.Key switch
@@ -925,25 +924,25 @@ internal class WindowsDriver : ConsoleDriver
{
// When a user presses-and-holds, start generating pressed events every `startDelay`
// After `iterationsUntilFast` iterations, speed them up to `fastDelay` ms
const int startDelay = 500;
const int iterationsUntilFast = 4;
const int fastDelay = 50;
const int START_DELAY = 500;
const int ITERATIONS_UNTIL_FAST = 4;
const int FAST_DELAY = 50;
int iterations = 0;
int delay = startDelay;
int delay = START_DELAY;
while (_isButtonPressed)
{
// TODO: This makes ConsoleDriver dependent on Application, which is not ideal. This should be moved to Application.
View view = Application.WantContinuousButtonPressedView;
View? view = Application.WantContinuousButtonPressedView;
if (view is null)
{
break;
}
if (iterations++ >= iterationsUntilFast)
if (iterations++ >= ITERATIONS_UNTIL_FAST)
{
delay = fastDelay;
delay = FAST_DELAY;
}
await Task.Delay (delay);
@@ -1012,13 +1011,13 @@ internal class WindowsDriver : ConsoleDriver
if (_isButtonDoubleClicked || _isOneFingerDoubleClicked)
{
// TODO: This makes ConsoleDriver dependent on Application, which is not ideal. This should be moved to Application.
Application.MainLoop.AddIdle (
() =>
{
Task.Run (async () => await ProcessButtonDoubleClickedAsync ());
Application.MainLoop!.AddIdle (
() =>
{
Task.Run (async () => await ProcessButtonDoubleClickedAsync ());
return false;
});
return false;
});
}
// The ButtonState member of the MouseEvent structure has bit corresponding to each mouse button.
@@ -1084,13 +1083,13 @@ internal class WindowsDriver : ConsoleDriver
if ((mouseFlag & MouseFlags.ReportMousePosition) == 0)
{
// TODO: This makes ConsoleDriver dependent on Application, which is not ideal. This should be moved to Application.
Application.MainLoop.AddIdle (
() =>
{
Task.Run (async () => await ProcessContinuousButtonPressedAsync (mouseFlag));
Application.MainLoop!.AddIdle (
() =>
{
Task.Run (async () => await ProcessContinuousButtonPressedAsync (mouseFlag));
return false;
});
return false;
});
}
}
else if (_lastMouseButtonPressed != null
@@ -1254,420 +1253,3 @@ internal class WindowsDriver : ConsoleDriver
};
}
}
/// <summary>
/// Mainloop intended to be used with the <see cref="WindowsDriver"/>, and can
/// only be used on Windows.
/// </summary>
/// <remarks>
/// This implementation is used for WindowsDriver.
/// </remarks>
internal class WindowsMainLoop : IMainLoopDriver
{
/// <summary>
/// Invoked when the window is changed.
/// </summary>
public EventHandler<SizeChangedEventArgs> WinChanged;
private readonly ConsoleDriver _consoleDriver;
private readonly ManualResetEventSlim _eventReady = new (false);
// The records that we keep fetching
private readonly ConcurrentQueue<WindowsConsole.InputRecord []> _resultQueue = new ();
internal readonly ManualResetEventSlim _waitForProbe = new (false);
private readonly WindowsConsole _winConsole;
private CancellationTokenSource _eventReadyTokenSource = new ();
private readonly CancellationTokenSource _inputHandlerTokenSource = new ();
private MainLoop _mainLoop;
public WindowsMainLoop (ConsoleDriver consoleDriver = null)
{
_consoleDriver = consoleDriver ?? throw new ArgumentNullException (nameof (consoleDriver));
if (!ConsoleDriver.RunningUnitTests)
{
_winConsole = ((WindowsDriver)consoleDriver).WinConsole;
_winConsole._mainLoop = this;
}
}
public AnsiEscapeSequenceRequests EscSeqRequests { get; } = new ();
void IMainLoopDriver.Setup (MainLoop mainLoop)
{
_mainLoop = mainLoop;
if (ConsoleDriver.RunningUnitTests)
{
return;
}
Task.Run (WindowsInputHandler, _inputHandlerTokenSource.Token);
#if HACK_CHECK_WINCHANGED
Task.Run (CheckWinChange);
#endif
}
void IMainLoopDriver.Wakeup () { _eventReady.Set (); }
bool IMainLoopDriver.EventsPending ()
{
_waitForProbe.Set ();
#if HACK_CHECK_WINCHANGED
_winChange.Set ();
#endif
if (_mainLoop.CheckTimersAndIdleHandlers (out int waitTimeout))
{
return true;
}
try
{
if (!_eventReadyTokenSource.IsCancellationRequested)
{
// Note: ManualResetEventSlim.Wait will wait indefinitely if the timeout is -1. The timeout is -1 when there
// are no timers, but there IS an idle handler waiting.
_eventReady.Wait (waitTimeout, _eventReadyTokenSource.Token);
}
}
catch (OperationCanceledException)
{
return true;
}
finally
{
_eventReady.Reset ();
}
if (!_eventReadyTokenSource.IsCancellationRequested)
{
#if HACK_CHECK_WINCHANGED
return _resultQueue.Count > 0 || _mainLoop.CheckTimersAndIdleHandlers (out _) || _winChanged;
#else
return _resultQueue.Count > 0 || _mainLoop.CheckTimersAndIdleHandlers (out _);
#endif
}
_eventReadyTokenSource.Dispose ();
_eventReadyTokenSource = new CancellationTokenSource ();
return true;
}
void IMainLoopDriver.Iteration ()
{
while (_resultQueue.Count > 0)
{
if (_resultQueue.TryDequeue (out WindowsConsole.InputRecord [] inputRecords))
{
if (inputRecords is { Length: > 0 })
{
((WindowsDriver)_consoleDriver).ProcessInput (inputRecords [0]);
}
}
}
#if HACK_CHECK_WINCHANGED
if (_winChanged)
{
_winChanged = false;
WinChanged?.Invoke (this, new SizeChangedEventArgs (_windowSize));
}
#endif
}
void IMainLoopDriver.TearDown ()
{
_inputHandlerTokenSource?.Cancel ();
_inputHandlerTokenSource?.Dispose ();
if (_winConsole is { })
{
var numOfEvents = _winConsole.GetNumberOfConsoleInputEvents ();
if (numOfEvents > 0)
{
_winConsole.FlushConsoleInputBuffer ();
//Debug.WriteLine ($"Flushed {numOfEvents} events.");
}
}
_waitForProbe?.Dispose ();
_resultQueue?.Clear ();
_eventReadyTokenSource?.Cancel ();
_eventReadyTokenSource?.Dispose ();
_eventReady?.Dispose ();
#if HACK_CHECK_WINCHANGED
_winChange?.Dispose ();
#endif
_mainLoop = null;
}
internal bool _forceRead;
private void WindowsInputHandler ()
{
while (_mainLoop is { })
{
try
{
if (!_inputHandlerTokenSource.IsCancellationRequested && !_forceRead)
{
_waitForProbe.Wait (_inputHandlerTokenSource.Token);
}
}
catch (OperationCanceledException)
{
// Wakes the _waitForProbe if it's waiting
_waitForProbe.Set ();
return;
}
finally
{
// If IsCancellationRequested is true the code after
// the `finally` block will not be executed.
if (!_inputHandlerTokenSource.IsCancellationRequested)
{
_waitForProbe.Reset ();
}
}
if (_resultQueue?.Count == 0 || _forceRead)
{
while (!_inputHandlerTokenSource.IsCancellationRequested)
{
WindowsConsole.InputRecord [] inpRec = _winConsole.ReadConsoleInput ();
if (inpRec is { })
{
_resultQueue!.Enqueue (inpRec);
break;
}
if (!_forceRead)
{
try
{
Task.Delay (100, _inputHandlerTokenSource.Token).Wait (_inputHandlerTokenSource.Token);
}
catch (OperationCanceledException)
{ }
}
}
}
_eventReady.Set ();
}
}
#if HACK_CHECK_WINCHANGED
private readonly ManualResetEventSlim _winChange = new (false);
private bool _winChanged;
private Size _windowSize;
private void CheckWinChange ()
{
while (_mainLoop is { })
{
_winChange.Wait ();
_winChange.Reset ();
// Check if the window size changed every half second.
// We do this to minimize the weird tearing seen on Windows when resizing the console
while (_mainLoop is { })
{
Task.Delay (500).Wait ();
_windowSize = _winConsole.GetConsoleBufferWindow (out _);
if (_windowSize != Size.Empty
&& (_windowSize.Width != _consoleDriver.Cols
|| _windowSize.Height != _consoleDriver.Rows))
{
break;
}
}
_winChanged = true;
_eventReady.Set ();
}
}
#endif
}
internal class WindowsClipboard : ClipboardBase
{
private const uint CF_UNICODE_TEXT = 13;
public override bool IsSupported { get; } = CheckClipboardIsAvailable ();
private static bool CheckClipboardIsAvailable ()
{
// Attempt to open the clipboard
if (OpenClipboard (nint.Zero))
{
// Clipboard is available
// Close the clipboard after use
CloseClipboard ();
return true;
}
// Clipboard is not available
return false;
}
protected override string GetClipboardDataImpl ()
{
try
{
if (!OpenClipboard (nint.Zero))
{
return string.Empty;
}
nint handle = GetClipboardData (CF_UNICODE_TEXT);
if (handle == nint.Zero)
{
return string.Empty;
}
nint pointer = nint.Zero;
try
{
pointer = GlobalLock (handle);
if (pointer == nint.Zero)
{
return string.Empty;
}
int size = GlobalSize (handle);
var buff = new byte [size];
Marshal.Copy (pointer, buff, 0, size);
return Encoding.Unicode.GetString (buff).TrimEnd ('\0');
}
finally
{
if (pointer != nint.Zero)
{
GlobalUnlock (handle);
}
}
}
finally
{
CloseClipboard ();
}
}
protected override void SetClipboardDataImpl (string text)
{
OpenClipboard ();
EmptyClipboard ();
nint hGlobal = default;
try
{
int bytes = (text.Length + 1) * 2;
hGlobal = Marshal.AllocHGlobal (bytes);
if (hGlobal == default (nint))
{
ThrowWin32 ();
}
nint target = GlobalLock (hGlobal);
if (target == default (nint))
{
ThrowWin32 ();
}
try
{
Marshal.Copy (text.ToCharArray (), 0, target, text.Length);
}
finally
{
GlobalUnlock (target);
}
if (SetClipboardData (CF_UNICODE_TEXT, hGlobal) == default (nint))
{
ThrowWin32 ();
}
hGlobal = default (nint);
}
finally
{
if (hGlobal != default (nint))
{
Marshal.FreeHGlobal (hGlobal);
}
CloseClipboard ();
}
}
[DllImport ("user32.dll", SetLastError = true)]
[return: MarshalAs (UnmanagedType.Bool)]
private static extern bool CloseClipboard ();
[DllImport ("user32.dll")]
private static extern bool EmptyClipboard ();
[DllImport ("user32.dll", SetLastError = true)]
private static extern nint GetClipboardData (uint uFormat);
[DllImport ("kernel32.dll", SetLastError = true)]
private static extern nint GlobalLock (nint hMem);
[DllImport ("kernel32.dll", SetLastError = true)]
private static extern int GlobalSize (nint handle);
[DllImport ("kernel32.dll", SetLastError = true)]
[return: MarshalAs (UnmanagedType.Bool)]
private static extern bool GlobalUnlock (nint hMem);
[DllImport ("User32.dll", SetLastError = true)]
[return: MarshalAs (UnmanagedType.Bool)]
private static extern bool IsClipboardFormatAvailable (uint format);
private void OpenClipboard ()
{
var num = 10;
while (true)
{
if (OpenClipboard (default (nint)))
{
break;
}
if (--num == 0)
{
ThrowWin32 ();
}
Thread.Sleep (100);
}
}
[DllImport ("user32.dll", SetLastError = true)]
[return: MarshalAs (UnmanagedType.Bool)]
private static extern bool OpenClipboard (nint hWndNewOwner);
[DllImport ("user32.dll", SetLastError = true)]
private static extern nint SetClipboardData (uint uFormat, nint data);
private void ThrowWin32 () { throw new Win32Exception (Marshal.GetLastWin32Error ()); }
}