WindowsDriver.cs 43 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255
  1. #nullable enable
  2. //
  3. // WindowsDriver.cs: Windows specific driver
  4. //
  5. // HACK:
  6. // WindowsConsole/Terminal has two issues:
  7. // 1) Tearing can occur when the console is resized.
  8. // 2) The values provided during Init (and the first WindowsConsole.EventType.WindowBufferSize) are not correct.
  9. //
  10. // If HACK_CHECK_WINCHANGED is defined then we ignore WindowsConsole.EventType.WindowBufferSize events
  11. // and instead check the console size every 500ms in a thread in WidowsMainLoop.
  12. // As of Windows 11 23H2 25947.1000 and/or WT 1.19.2682 tearing no longer occurs when using
  13. // the WindowsConsole.EventType.WindowBufferSize event. However, on Init the window size is
  14. // still incorrect so we still need this hack.
  15. //#define HACK_CHECK_WINCHANGED
  16. using System.ComponentModel;
  17. using System.Diagnostics;
  18. using System.Runtime.InteropServices;
  19. using static Terminal.Gui.ConsoleDrivers.ConsoleKeyMapping;
  20. namespace Terminal.Gui;
  21. internal class WindowsDriver : ConsoleDriver
  22. {
  23. private readonly bool _isWindowsTerminal;
  24. private WindowsConsole.SmallRect _damageRegion;
  25. private bool _isButtonDoubleClicked;
  26. private bool _isButtonPressed;
  27. private bool _isButtonReleased;
  28. private bool _isOneFingerDoubleClicked;
  29. private WindowsConsole.ButtonState? _lastMouseButtonPressed;
  30. private WindowsMainLoop? _mainLoopDriver;
  31. private WindowsConsole.ExtendedCharInfo [] _outputBuffer;
  32. private Point? _point;
  33. private Point _pointMove;
  34. private bool _processButtonClick;
  35. public WindowsDriver ()
  36. {
  37. if (Environment.OSVersion.Platform == PlatformID.Win32NT)
  38. {
  39. WinConsole = new ();
  40. // otherwise we're probably running in unit tests
  41. Clipboard = new WindowsClipboard ();
  42. }
  43. else
  44. {
  45. Clipboard = new FakeDriver.FakeClipboard ();
  46. }
  47. // TODO: if some other Windows-based terminal supports true color, update this logic to not
  48. // force 16color mode (.e.g ConEmu which really doesn't work well at all).
  49. _isWindowsTerminal = _isWindowsTerminal =
  50. Environment.GetEnvironmentVariable ("WT_SESSION") is { } || Environment.GetEnvironmentVariable ("VSAPPIDNAME") != null;
  51. if (!_isWindowsTerminal)
  52. {
  53. Force16Colors = true;
  54. }
  55. }
  56. public override bool SupportsTrueColor => RunningUnitTests || (Environment.OSVersion.Version.Build >= 14931 && _isWindowsTerminal);
  57. public WindowsConsole? WinConsole { get; private set; }
  58. public WindowsConsole.KeyEventRecord FromVKPacketToKeyEventRecord (WindowsConsole.KeyEventRecord keyEvent)
  59. {
  60. if (keyEvent.wVirtualKeyCode != (VK)ConsoleKey.Packet)
  61. {
  62. return keyEvent;
  63. }
  64. var mod = new ConsoleModifiers ();
  65. if (keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.ShiftPressed))
  66. {
  67. mod |= ConsoleModifiers.Shift;
  68. }
  69. if (keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.RightAltPressed)
  70. || keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.LeftAltPressed))
  71. {
  72. mod |= ConsoleModifiers.Alt;
  73. }
  74. if (keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.LeftControlPressed)
  75. || keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.RightControlPressed))
  76. {
  77. mod |= ConsoleModifiers.Control;
  78. }
  79. var cKeyInfo = new ConsoleKeyInfo (
  80. keyEvent.UnicodeChar,
  81. (ConsoleKey)keyEvent.wVirtualKeyCode,
  82. mod.HasFlag (ConsoleModifiers.Shift),
  83. mod.HasFlag (ConsoleModifiers.Alt),
  84. mod.HasFlag (ConsoleModifiers.Control));
  85. cKeyInfo = DecodeVKPacketToKConsoleKeyInfo (cKeyInfo);
  86. uint scanCode = GetScanCodeFromConsoleKeyInfo (cKeyInfo);
  87. return new WindowsConsole.KeyEventRecord
  88. {
  89. UnicodeChar = cKeyInfo.KeyChar,
  90. bKeyDown = keyEvent.bKeyDown,
  91. dwControlKeyState = keyEvent.dwControlKeyState,
  92. wRepeatCount = keyEvent.wRepeatCount,
  93. wVirtualKeyCode = (VK)cKeyInfo.Key,
  94. wVirtualScanCode = (ushort)scanCode
  95. };
  96. }
  97. public override bool IsRuneSupported (Rune rune) { return base.IsRuneSupported (rune) && rune.IsBmp; }
  98. public override void Refresh ()
  99. {
  100. if (!RunningUnitTests)
  101. {
  102. UpdateScreen ();
  103. //WinConsole?.SetInitialCursorVisibility ();
  104. UpdateCursor ();
  105. }
  106. }
  107. public override void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool control)
  108. {
  109. var input = new WindowsConsole.InputRecord
  110. {
  111. EventType = WindowsConsole.EventType.Key
  112. };
  113. var keyEvent = new WindowsConsole.KeyEventRecord
  114. {
  115. bKeyDown = true
  116. };
  117. var controlKey = new WindowsConsole.ControlKeyState ();
  118. if (shift)
  119. {
  120. controlKey |= WindowsConsole.ControlKeyState.ShiftPressed;
  121. keyEvent.UnicodeChar = '\0';
  122. keyEvent.wVirtualKeyCode = VK.SHIFT;
  123. }
  124. if (alt)
  125. {
  126. controlKey |= WindowsConsole.ControlKeyState.LeftAltPressed;
  127. controlKey |= WindowsConsole.ControlKeyState.RightAltPressed;
  128. keyEvent.UnicodeChar = '\0';
  129. keyEvent.wVirtualKeyCode = VK.MENU;
  130. }
  131. if (control)
  132. {
  133. controlKey |= WindowsConsole.ControlKeyState.LeftControlPressed;
  134. controlKey |= WindowsConsole.ControlKeyState.RightControlPressed;
  135. keyEvent.UnicodeChar = '\0';
  136. keyEvent.wVirtualKeyCode = VK.CONTROL;
  137. }
  138. keyEvent.dwControlKeyState = controlKey;
  139. input.KeyEvent = keyEvent;
  140. if (shift || alt || control)
  141. {
  142. ProcessInput (input);
  143. }
  144. keyEvent.UnicodeChar = keyChar;
  145. //if ((uint)key < 255) {
  146. // keyEvent.wVirtualKeyCode = (ushort)key;
  147. //} else {
  148. // keyEvent.wVirtualKeyCode = '\0';
  149. //}
  150. keyEvent.wVirtualKeyCode = (VK)key;
  151. input.KeyEvent = keyEvent;
  152. try
  153. {
  154. ProcessInput (input);
  155. }
  156. catch (OverflowException)
  157. { }
  158. finally
  159. {
  160. keyEvent.bKeyDown = false;
  161. input.KeyEvent = keyEvent;
  162. ProcessInput (input);
  163. }
  164. }
  165. private readonly ManualResetEventSlim _waitAnsiResponse = new (false);
  166. private readonly CancellationTokenSource _ansiResponseTokenSource = new ();
  167. /// <inheritdoc/>
  168. public override string? WriteAnsiRequest (AnsiEscapeSequenceRequest ansiRequest)
  169. {
  170. if (_mainLoopDriver is null)
  171. {
  172. return string.Empty;
  173. }
  174. try
  175. {
  176. lock (ansiRequest._responseLock)
  177. {
  178. ansiRequest.ResponseFromInput += (s, e) =>
  179. {
  180. Debug.Assert (s == ansiRequest);
  181. Debug.Assert (e == ansiRequest.Response);
  182. _waitAnsiResponse.Set ();
  183. };
  184. _mainLoopDriver.EscSeqRequests.Add (ansiRequest, this);
  185. _mainLoopDriver._forceRead = true;
  186. }
  187. if (!_ansiResponseTokenSource.IsCancellationRequested)
  188. {
  189. _mainLoopDriver._waitForProbe.Set ();
  190. _waitAnsiResponse.Wait (_ansiResponseTokenSource.Token);
  191. }
  192. }
  193. catch (OperationCanceledException)
  194. {
  195. return string.Empty;
  196. }
  197. lock (ansiRequest._responseLock)
  198. {
  199. _mainLoopDriver._forceRead = false;
  200. if (_mainLoopDriver.EscSeqRequests.Statuses.TryPeek (out AnsiEscapeSequenceRequestStatus? request))
  201. {
  202. if (_mainLoopDriver.EscSeqRequests.Statuses.Count > 0
  203. && string.IsNullOrEmpty (request.AnsiRequest.Response))
  204. {
  205. lock (request.AnsiRequest._responseLock)
  206. {
  207. // Bad request or no response at all
  208. _mainLoopDriver.EscSeqRequests.Statuses.TryDequeue (out _);
  209. }
  210. }
  211. }
  212. _waitAnsiResponse.Reset ();
  213. return ansiRequest.Response;
  214. }
  215. }
  216. public override void WriteRaw (string ansi)
  217. {
  218. WinConsole?.WriteANSI (ansi);
  219. }
  220. #region Not Implemented
  221. public override void Suspend () { throw new NotImplementedException (); }
  222. #endregion
  223. public WindowsConsole.ConsoleKeyInfoEx ToConsoleKeyInfoEx (WindowsConsole.KeyEventRecord keyEvent)
  224. {
  225. WindowsConsole.ControlKeyState state = keyEvent.dwControlKeyState;
  226. bool shift = (state & WindowsConsole.ControlKeyState.ShiftPressed) != 0;
  227. bool alt = (state & (WindowsConsole.ControlKeyState.LeftAltPressed | WindowsConsole.ControlKeyState.RightAltPressed)) != 0;
  228. bool control = (state & (WindowsConsole.ControlKeyState.LeftControlPressed | WindowsConsole.ControlKeyState.RightControlPressed)) != 0;
  229. bool capslock = (state & WindowsConsole.ControlKeyState.CapslockOn) != 0;
  230. bool numlock = (state & WindowsConsole.ControlKeyState.NumlockOn) != 0;
  231. bool scrolllock = (state & WindowsConsole.ControlKeyState.ScrolllockOn) != 0;
  232. var cki = new ConsoleKeyInfo (keyEvent.UnicodeChar, (ConsoleKey)keyEvent.wVirtualKeyCode, shift, alt, control);
  233. return new WindowsConsole.ConsoleKeyInfoEx (cki, capslock, numlock, scrolllock);
  234. }
  235. #region Cursor Handling
  236. private CursorVisibility? _cachedCursorVisibility;
  237. public override void UpdateCursor ()
  238. {
  239. if (Col < 0 || Row < 0 || Col >= Cols || Row >= Rows)
  240. {
  241. GetCursorVisibility (out CursorVisibility cursorVisibility);
  242. _cachedCursorVisibility = cursorVisibility;
  243. SetCursorVisibility (CursorVisibility.Invisible);
  244. return;
  245. }
  246. var position = new WindowsConsole.Coord
  247. {
  248. X = (short)Col,
  249. Y = (short)Row
  250. };
  251. if (Force16Colors)
  252. {
  253. WinConsole?.SetCursorPosition (position);
  254. }
  255. else
  256. {
  257. var sb = new StringBuilder ();
  258. sb.Append (AnsiEscapeSequenceRequestUtils.CSI_SetCursorPosition (position.Y + 1, position.X + 1));
  259. WinConsole?.WriteANSI (sb.ToString ());
  260. }
  261. if (_cachedCursorVisibility is { })
  262. {
  263. SetCursorVisibility (_cachedCursorVisibility.Value);
  264. }
  265. //EnsureCursorVisibility ();
  266. }
  267. /// <inheritdoc/>
  268. public override bool GetCursorVisibility (out CursorVisibility visibility)
  269. {
  270. if (WinConsole is { })
  271. {
  272. return WinConsole.GetCursorVisibility (out visibility);
  273. }
  274. visibility = _cachedCursorVisibility ?? CursorVisibility.Default;
  275. return true;
  276. }
  277. /// <inheritdoc/>
  278. public override bool SetCursorVisibility (CursorVisibility visibility)
  279. {
  280. _cachedCursorVisibility = visibility;
  281. if (Force16Colors)
  282. {
  283. return WinConsole is null || WinConsole.SetCursorVisibility (visibility);
  284. }
  285. else
  286. {
  287. var sb = new StringBuilder ();
  288. sb.Append (visibility != CursorVisibility.Invisible ? AnsiEscapeSequenceRequestUtils.CSI_ShowCursor : AnsiEscapeSequenceRequestUtils.CSI_HideCursor);
  289. return WinConsole?.WriteANSI (sb.ToString ()) ?? false;
  290. }
  291. }
  292. /// <inheritdoc/>
  293. public override bool EnsureCursorVisibility ()
  294. {
  295. if (Force16Colors)
  296. {
  297. return WinConsole is null || WinConsole.EnsureCursorVisibility ();
  298. }
  299. else
  300. {
  301. var sb = new StringBuilder ();
  302. sb.Append (_cachedCursorVisibility != CursorVisibility.Invisible ? AnsiEscapeSequenceRequestUtils.CSI_ShowCursor : AnsiEscapeSequenceRequestUtils.CSI_HideCursor);
  303. return WinConsole?.WriteANSI (sb.ToString ()) ?? false;
  304. }
  305. //if (!(Col >= 0 && Row >= 0 && Col < Cols && Row < Rows))
  306. //{
  307. // GetCursorVisibility (out CursorVisibility cursorVisibility);
  308. // _cachedCursorVisibility = cursorVisibility;
  309. // SetCursorVisibility (CursorVisibility.Invisible);
  310. // return false;
  311. //}
  312. //SetCursorVisibility (_cachedCursorVisibility ?? CursorVisibility.Default);
  313. //return _cachedCursorVisibility == CursorVisibility.Default;
  314. }
  315. #endregion Cursor Handling
  316. public override void UpdateScreen ()
  317. {
  318. Size windowSize = WinConsole?.GetConsoleOutputWindow (out Point _) ?? new Size (Cols, Rows);
  319. if (!windowSize.IsEmpty && (windowSize.Width != Cols || windowSize.Height != Rows))
  320. {
  321. return;
  322. }
  323. var bufferCoords = new WindowsConsole.Coord
  324. {
  325. X = (short)Cols, //Clip.Width,
  326. Y = (short)Rows, //Clip.Height
  327. };
  328. for (var row = 0; row < Rows; row++)
  329. {
  330. if (!_dirtyLines! [row])
  331. {
  332. continue;
  333. }
  334. _dirtyLines [row] = false;
  335. for (var col = 0; col < Cols; col++)
  336. {
  337. int position = row * Cols + col;
  338. _outputBuffer [position].Attribute = Contents! [row, col].Attribute.GetValueOrDefault ();
  339. if (Contents [row, col].IsDirty == false)
  340. {
  341. _outputBuffer [position].Empty = true;
  342. _outputBuffer [position].Char = (char)Rune.ReplacementChar.Value;
  343. continue;
  344. }
  345. _outputBuffer [position].Empty = false;
  346. if (Contents [row, col].Rune.IsBmp)
  347. {
  348. _outputBuffer [position].Char = (char)Contents [row, col].Rune.Value;
  349. }
  350. else
  351. {
  352. //_outputBuffer [position].Empty = true;
  353. _outputBuffer [position].Char = (char)Rune.ReplacementChar.Value;
  354. if (Contents [row, col].Rune.GetColumns () > 1 && col + 1 < Cols)
  355. {
  356. // TODO: This is a hack to deal with non-BMP and wide characters.
  357. col++;
  358. position = row * Cols + col;
  359. _outputBuffer [position].Empty = false;
  360. _outputBuffer [position].Char = ' ';
  361. }
  362. }
  363. }
  364. }
  365. _damageRegion = new WindowsConsole.SmallRect
  366. {
  367. Top = 0,
  368. Left = 0,
  369. Bottom = (short)Rows,
  370. Right = (short)Cols
  371. };
  372. if (!RunningUnitTests
  373. && WinConsole != null
  374. && !WinConsole.WriteToConsole (new (Cols, Rows), _outputBuffer, bufferCoords, _damageRegion, Force16Colors))
  375. {
  376. int err = Marshal.GetLastWin32Error ();
  377. if (err != 0)
  378. {
  379. throw new Win32Exception (err);
  380. }
  381. }
  382. WindowsConsole.SmallRect.MakeEmpty (ref _damageRegion);
  383. }
  384. internal override void End ()
  385. {
  386. if (_mainLoopDriver is { })
  387. {
  388. #if HACK_CHECK_WINCHANGED
  389. _mainLoopDriver.WinChanged -= ChangeWin;
  390. #endif
  391. }
  392. _mainLoopDriver = null;
  393. WinConsole?.Cleanup ();
  394. WinConsole = null;
  395. if (!RunningUnitTests && _isWindowsTerminal)
  396. {
  397. // Disable alternative screen buffer.
  398. Console.Out.Write (AnsiEscapeSequenceRequestUtils.CSI_RestoreCursorAndRestoreAltBufferWithBackscroll);
  399. }
  400. }
  401. internal override MainLoop Init ()
  402. {
  403. _mainLoopDriver = new WindowsMainLoop (this);
  404. if (!RunningUnitTests)
  405. {
  406. try
  407. {
  408. if (WinConsole is { })
  409. {
  410. // BUGBUG: The results from GetConsoleOutputWindow are incorrect when called from Init.
  411. // Our thread in WindowsMainLoop.CheckWin will get the correct results. See #if HACK_CHECK_WINCHANGED
  412. Size winSize = WinConsole.GetConsoleOutputWindow (out Point _);
  413. Cols = winSize.Width;
  414. Rows = winSize.Height;
  415. }
  416. WindowsConsole.SmallRect.MakeEmpty (ref _damageRegion);
  417. if (_isWindowsTerminal)
  418. {
  419. Console.Out.Write (AnsiEscapeSequenceRequestUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll);
  420. }
  421. }
  422. catch (Win32Exception e)
  423. {
  424. // We are being run in an environment that does not support a console
  425. // such as a unit test, or a pipe.
  426. Debug.WriteLine ($"Likely running unit tests. Setting WinConsole to null so we can test it elsewhere. Exception: {e}");
  427. WinConsole = null;
  428. }
  429. }
  430. CurrentAttribute = new Attribute (Color.White, Color.Black);
  431. _outputBuffer = new WindowsConsole.ExtendedCharInfo [Rows * Cols];
  432. // CONCURRENCY: Unsynchronized access to Clip is not safe.
  433. Clip = new (0, 0, Cols, Rows);
  434. _damageRegion = new WindowsConsole.SmallRect
  435. {
  436. Top = 0,
  437. Left = 0,
  438. Bottom = (short)Rows,
  439. Right = (short)Cols
  440. };
  441. ClearContents ();
  442. #if HACK_CHECK_WINCHANGED
  443. _mainLoopDriver.WinChanged = ChangeWin;
  444. #endif
  445. if (!RunningUnitTests)
  446. {
  447. WinConsole?.SetInitialCursorVisibility ();
  448. }
  449. return new MainLoop (_mainLoopDriver);
  450. }
  451. internal void ProcessInput (WindowsConsole.InputRecord inputEvent)
  452. {
  453. switch (inputEvent.EventType)
  454. {
  455. case WindowsConsole.EventType.Key:
  456. if (inputEvent.KeyEvent.wVirtualKeyCode == (VK)ConsoleKey.Packet)
  457. {
  458. // Used to pass Unicode characters as if they were keystrokes.
  459. // The VK_PACKET key is the low word of a 32-bit
  460. // Virtual Key value used for non-keyboard input methods.
  461. inputEvent.KeyEvent = FromVKPacketToKeyEventRecord (inputEvent.KeyEvent);
  462. }
  463. WindowsConsole.ConsoleKeyInfoEx keyInfo = ToConsoleKeyInfoEx (inputEvent.KeyEvent);
  464. //Debug.WriteLine ($"event: KBD: {GetKeyboardLayoutName()} {inputEvent.ToString ()} {keyInfo.ToString (keyInfo)}");
  465. KeyCode map = MapKey (keyInfo);
  466. if (map == KeyCode.Null)
  467. {
  468. break;
  469. }
  470. if (inputEvent.KeyEvent.bKeyDown)
  471. {
  472. // Avoid sending repeat key down events
  473. OnKeyDown (new Key (map));
  474. }
  475. else
  476. {
  477. OnKeyUp (new Key (map));
  478. }
  479. break;
  480. case WindowsConsole.EventType.Mouse:
  481. MouseEventArgs me = ToDriverMouse (inputEvent.MouseEvent);
  482. if (me.Flags == MouseFlags.None)
  483. {
  484. break;
  485. }
  486. OnMouseEvent (me);
  487. if (_processButtonClick)
  488. {
  489. OnMouseEvent (new ()
  490. {
  491. Position = me.Position,
  492. Flags = ProcessButtonClick (inputEvent.MouseEvent)
  493. });
  494. }
  495. break;
  496. case WindowsConsole.EventType.Focus:
  497. break;
  498. #if !HACK_CHECK_WINCHANGED
  499. case WindowsConsole.EventType.WindowBufferSize:
  500. Cols = inputEvent.WindowBufferSizeEvent._size.X;
  501. Rows = inputEvent.WindowBufferSizeEvent._size.Y;
  502. ResizeScreen ();
  503. ClearContents ();
  504. Application.Top?.SetNeedsLayout ();
  505. Application.Refresh ();
  506. break;
  507. #endif
  508. }
  509. }
  510. #if HACK_CHECK_WINCHANGED
  511. private void ChangeWin (object s, SizeChangedEventArgs e)
  512. {
  513. if (e.Size is null)
  514. {
  515. return;
  516. }
  517. int w = e.Size.Value.Width;
  518. if (w == Cols - 3 && e.Size.Value.Height < Rows)
  519. {
  520. w += 3;
  521. }
  522. Left = 0;
  523. Top = 0;
  524. Cols = e.Size.Value.Width;
  525. Rows = e.Size.Value.Height;
  526. if (!RunningUnitTests)
  527. {
  528. Size newSize = WinConsole.SetConsoleWindow (
  529. (short)Math.Max (w, 16),
  530. (short)Math.Max (e.Size.Value.Height, 0));
  531. Cols = newSize.Width;
  532. Rows = newSize.Height;
  533. }
  534. ResizeScreen ();
  535. ClearContents ();
  536. OnSizeChanged (new SizeChangedEventArgs (new (Cols, Rows)));
  537. }
  538. #endif
  539. private KeyCode MapKey (WindowsConsole.ConsoleKeyInfoEx keyInfoEx)
  540. {
  541. ConsoleKeyInfo keyInfo = keyInfoEx.ConsoleKeyInfo;
  542. switch (keyInfo.Key)
  543. {
  544. case ConsoleKey.D0:
  545. case ConsoleKey.D1:
  546. case ConsoleKey.D2:
  547. case ConsoleKey.D3:
  548. case ConsoleKey.D4:
  549. case ConsoleKey.D5:
  550. case ConsoleKey.D6:
  551. case ConsoleKey.D7:
  552. case ConsoleKey.D8:
  553. case ConsoleKey.D9:
  554. case ConsoleKey.NumPad0:
  555. case ConsoleKey.NumPad1:
  556. case ConsoleKey.NumPad2:
  557. case ConsoleKey.NumPad3:
  558. case ConsoleKey.NumPad4:
  559. case ConsoleKey.NumPad5:
  560. case ConsoleKey.NumPad6:
  561. case ConsoleKey.NumPad7:
  562. case ConsoleKey.NumPad8:
  563. case ConsoleKey.NumPad9:
  564. case ConsoleKey.Oem1:
  565. case ConsoleKey.Oem2:
  566. case ConsoleKey.Oem3:
  567. case ConsoleKey.Oem4:
  568. case ConsoleKey.Oem5:
  569. case ConsoleKey.Oem6:
  570. case ConsoleKey.Oem7:
  571. case ConsoleKey.Oem8:
  572. case ConsoleKey.Oem102:
  573. case ConsoleKey.Multiply:
  574. case ConsoleKey.Add:
  575. case ConsoleKey.Separator:
  576. case ConsoleKey.Subtract:
  577. case ConsoleKey.Decimal:
  578. case ConsoleKey.Divide:
  579. case ConsoleKey.OemPeriod:
  580. case ConsoleKey.OemComma:
  581. case ConsoleKey.OemPlus:
  582. case ConsoleKey.OemMinus:
  583. // These virtual key codes are mapped differently depending on the keyboard layout in use.
  584. // We use the Win32 API to map them to the correct character.
  585. uint mapResult = MapVKtoChar ((VK)keyInfo.Key);
  586. if (mapResult == 0)
  587. {
  588. // There is no mapping - this should not happen
  589. Debug.Assert (true, $@"Unable to map the virtual key code {keyInfo.Key}.");
  590. return KeyCode.Null;
  591. }
  592. // An un-shifted character value is in the low order word of the return value.
  593. var mappedChar = (char)(mapResult & 0x0000FFFF);
  594. if (keyInfo.KeyChar == 0)
  595. {
  596. // If the keyChar is 0, keyInfo.Key value is not a printable character.
  597. // Dead keys (diacritics) are indicated by setting the top bit of the return value.
  598. if ((mapResult & 0x80000000) != 0)
  599. {
  600. // Dead key (e.g. Oem2 '~'/'^' on POR keyboard)
  601. // Option 1: Throw it out.
  602. // - Apps will never see the dead keys
  603. // - If user presses a key that can be combined with the dead key ('a'), the right thing happens (app will see '�').
  604. // - NOTE: With Dead Keys, KeyDown != KeyUp. The KeyUp event will have just the base char ('a').
  605. // - If user presses dead key again, the right thing happens (app will see `~~`)
  606. // - This is what Notepad etc... appear to do
  607. // Option 2: Expand the API to indicate the KeyCode is a dead key
  608. // - Enables apps to do their own dead key processing
  609. // - Adds complexity; no dev has asked for this (yet).
  610. // We choose Option 1 for now.
  611. return KeyCode.Null;
  612. // Note: Ctrl-Deadkey (like Oem3 '`'/'~` on ENG) can't be supported.
  613. // Sadly, the charVal is just the deadkey and subsequent key events do not contain
  614. // any info that the previous event was a deadkey.
  615. // Note WT does not support Ctrl-Deadkey either.
  616. }
  617. if (keyInfo.Modifiers != 0)
  618. {
  619. // These Oem keys have well-defined chars. We ensure the representative char is used.
  620. // If we don't do this, then on some keyboard layouts the wrong char is
  621. // returned (e.g. on ENG OemPlus un-shifted is =, not +). This is important
  622. // for key persistence ("Ctrl++" vs. "Ctrl+=").
  623. mappedChar = keyInfo.Key switch
  624. {
  625. ConsoleKey.OemPeriod => '.',
  626. ConsoleKey.OemComma => ',',
  627. ConsoleKey.OemPlus => '+',
  628. ConsoleKey.OemMinus => '-',
  629. _ => mappedChar
  630. };
  631. }
  632. // Return the mappedChar with modifiers. Because mappedChar is un-shifted, if Shift was down
  633. // we should keep it
  634. return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)mappedChar);
  635. }
  636. // KeyChar is printable
  637. if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt) && keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control))
  638. {
  639. // AltGr support - AltGr is equivalent to Ctrl+Alt - the correct char is in KeyChar
  640. return (KeyCode)keyInfo.KeyChar;
  641. }
  642. if (keyInfo.Modifiers != ConsoleModifiers.Shift)
  643. {
  644. // If Shift wasn't down we don't need to do anything but return the mappedChar
  645. return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)mappedChar);
  646. }
  647. // Strip off Shift - We got here because they KeyChar from Windows is the shifted char (e.g. "�")
  648. // and passing on Shift would be redundant.
  649. return MapToKeyCodeModifiers (keyInfo.Modifiers & ~ConsoleModifiers.Shift, (KeyCode)keyInfo.KeyChar);
  650. }
  651. // A..Z are special cased:
  652. // - Alone, they represent lowercase a...z
  653. // - With ShiftMask they are A..Z
  654. // - If CapsLock is on the above is reversed.
  655. // - If Alt and/or Ctrl are present, treat as upper case
  656. if (keyInfo.Key is >= ConsoleKey.A and <= ConsoleKey.Z)
  657. {
  658. if (keyInfo.KeyChar == 0)
  659. {
  660. // KeyChar is not printable - possibly an AltGr key?
  661. // AltGr support - AltGr is equivalent to Ctrl+Alt
  662. if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt) && keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control))
  663. {
  664. return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)(uint)keyInfo.Key);
  665. }
  666. }
  667. if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt) || keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control))
  668. {
  669. return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)(uint)keyInfo.Key);
  670. }
  671. if ((keyInfo.Modifiers == ConsoleModifiers.Shift) ^ keyInfoEx.CapsLock)
  672. {
  673. // If (ShiftMask is on and CapsLock is off) or (ShiftMask is off and CapsLock is on) add the ShiftMask
  674. if (char.IsUpper (keyInfo.KeyChar))
  675. {
  676. if (keyInfo.KeyChar <= 'Z')
  677. {
  678. return (KeyCode)keyInfo.Key | KeyCode.ShiftMask;
  679. }
  680. // Always return the KeyChar because it may be an Á, À with Oem1, etc
  681. return (KeyCode)keyInfo.KeyChar;
  682. }
  683. }
  684. if (keyInfo.KeyChar <= 'z')
  685. {
  686. return (KeyCode)keyInfo.Key;
  687. }
  688. // Always return the KeyChar because it may be an á, à with Oem1, etc
  689. return (KeyCode)keyInfo.KeyChar;
  690. }
  691. // Handle control keys whose VK codes match the related ASCII value (those below ASCII 33) like ESC
  692. if (Enum.IsDefined (typeof (KeyCode), (uint)keyInfo.Key))
  693. {
  694. // If the key is JUST a modifier, return it as just that key
  695. if (keyInfo.Key == (ConsoleKey)VK.SHIFT)
  696. { // Shift 16
  697. return KeyCode.ShiftMask;
  698. }
  699. if (keyInfo.Key == (ConsoleKey)VK.CONTROL)
  700. { // Ctrl 17
  701. return KeyCode.CtrlMask;
  702. }
  703. if (keyInfo.Key == (ConsoleKey)VK.MENU)
  704. { // Alt 18
  705. return KeyCode.AltMask;
  706. }
  707. if (keyInfo.KeyChar == 0)
  708. {
  709. return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.KeyChar);
  710. }
  711. if (keyInfo.Key != ConsoleKey.None)
  712. {
  713. return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.KeyChar);
  714. }
  715. return MapToKeyCodeModifiers (keyInfo.Modifiers & ~ConsoleModifiers.Shift, (KeyCode)keyInfo.KeyChar);
  716. }
  717. // Handle control keys (e.g. CursorUp)
  718. if (Enum.IsDefined (typeof (KeyCode), (uint)keyInfo.Key + (uint)KeyCode.MaxCodePoint))
  719. {
  720. return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)((uint)keyInfo.Key + (uint)KeyCode.MaxCodePoint));
  721. }
  722. return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.KeyChar);
  723. }
  724. private MouseFlags ProcessButtonClick (WindowsConsole.MouseEventRecord mouseEvent)
  725. {
  726. MouseFlags mouseFlag = 0;
  727. switch (_lastMouseButtonPressed)
  728. {
  729. case WindowsConsole.ButtonState.Button1Pressed:
  730. mouseFlag = MouseFlags.Button1Clicked;
  731. break;
  732. case WindowsConsole.ButtonState.Button2Pressed:
  733. mouseFlag = MouseFlags.Button2Clicked;
  734. break;
  735. case WindowsConsole.ButtonState.RightmostButtonPressed:
  736. mouseFlag = MouseFlags.Button3Clicked;
  737. break;
  738. }
  739. _point = new Point
  740. {
  741. X = mouseEvent.MousePosition.X,
  742. Y = mouseEvent.MousePosition.Y
  743. };
  744. _lastMouseButtonPressed = null;
  745. _isButtonReleased = false;
  746. _processButtonClick = false;
  747. _point = null;
  748. return mouseFlag;
  749. }
  750. private async Task ProcessButtonDoubleClickedAsync ()
  751. {
  752. await Task.Delay (200);
  753. _isButtonDoubleClicked = false;
  754. _isOneFingerDoubleClicked = false;
  755. //buttonPressedCount = 0;
  756. }
  757. private async Task ProcessContinuousButtonPressedAsync (MouseFlags mouseFlag)
  758. {
  759. // When a user presses-and-holds, start generating pressed events every `startDelay`
  760. // After `iterationsUntilFast` iterations, speed them up to `fastDelay` ms
  761. const int START_DELAY = 500;
  762. const int ITERATIONS_UNTIL_FAST = 4;
  763. const int FAST_DELAY = 50;
  764. int iterations = 0;
  765. int delay = START_DELAY;
  766. while (_isButtonPressed)
  767. {
  768. // TODO: This makes ConsoleDriver dependent on Application, which is not ideal. This should be moved to Application.
  769. View? view = Application.WantContinuousButtonPressedView;
  770. if (view is null)
  771. {
  772. break;
  773. }
  774. if (iterations++ >= ITERATIONS_UNTIL_FAST)
  775. {
  776. delay = FAST_DELAY;
  777. }
  778. await Task.Delay (delay);
  779. var me = new MouseEventArgs
  780. {
  781. ScreenPosition = _pointMove,
  782. Flags = mouseFlag
  783. };
  784. //Debug.WriteLine($"ProcessContinuousButtonPressedAsync: {view}");
  785. if (_isButtonPressed && (mouseFlag & MouseFlags.ReportMousePosition) == 0)
  786. {
  787. // TODO: This makes ConsoleDriver dependent on Application, which is not ideal. This should be moved to Application.
  788. Application.Invoke (() => OnMouseEvent (me));
  789. }
  790. }
  791. }
  792. private void ResizeScreen ()
  793. {
  794. _outputBuffer = new WindowsConsole.ExtendedCharInfo [Rows * Cols];
  795. // CONCURRENCY: Unsynchronized access to Clip is not safe.
  796. Clip = new (0, 0, Cols, Rows);
  797. _damageRegion = new WindowsConsole.SmallRect
  798. {
  799. Top = 0,
  800. Left = 0,
  801. Bottom = (short)Rows,
  802. Right = (short)Cols
  803. };
  804. _dirtyLines = new bool [Rows];
  805. WinConsole?.ForceRefreshCursorVisibility ();
  806. }
  807. private static MouseFlags SetControlKeyStates (WindowsConsole.MouseEventRecord mouseEvent, MouseFlags mouseFlag)
  808. {
  809. if (mouseEvent.ControlKeyState.HasFlag (WindowsConsole.ControlKeyState.RightControlPressed)
  810. || mouseEvent.ControlKeyState.HasFlag (WindowsConsole.ControlKeyState.LeftControlPressed))
  811. {
  812. mouseFlag |= MouseFlags.ButtonCtrl;
  813. }
  814. if (mouseEvent.ControlKeyState.HasFlag (WindowsConsole.ControlKeyState.ShiftPressed))
  815. {
  816. mouseFlag |= MouseFlags.ButtonShift;
  817. }
  818. if (mouseEvent.ControlKeyState.HasFlag (WindowsConsole.ControlKeyState.RightAltPressed)
  819. || mouseEvent.ControlKeyState.HasFlag (WindowsConsole.ControlKeyState.LeftAltPressed))
  820. {
  821. mouseFlag |= MouseFlags.ButtonAlt;
  822. }
  823. return mouseFlag;
  824. }
  825. [CanBeNull]
  826. private MouseEventArgs ToDriverMouse (WindowsConsole.MouseEventRecord mouseEvent)
  827. {
  828. var mouseFlag = MouseFlags.AllEvents;
  829. //Debug.WriteLine ($"ToDriverMouse: {mouseEvent}");
  830. if (_isButtonDoubleClicked || _isOneFingerDoubleClicked)
  831. {
  832. // TODO: This makes ConsoleDriver dependent on Application, which is not ideal. This should be moved to Application.
  833. Application.MainLoop!.AddIdle (
  834. () =>
  835. {
  836. Task.Run (async () => await ProcessButtonDoubleClickedAsync ());
  837. return false;
  838. });
  839. }
  840. // The ButtonState member of the MouseEvent structure has bit corresponding to each mouse button.
  841. // This will tell when a mouse button is pressed. When the button is released this event will
  842. // be fired with its bit set to 0. So when the button is up ButtonState will be 0.
  843. // To map to the correct driver events we save the last pressed mouse button, so we can
  844. // map to the correct clicked event.
  845. if ((_lastMouseButtonPressed is { } || _isButtonReleased) && mouseEvent.ButtonState != 0)
  846. {
  847. _lastMouseButtonPressed = null;
  848. //isButtonPressed = false;
  849. _isButtonReleased = false;
  850. }
  851. var p = new Point
  852. {
  853. X = mouseEvent.MousePosition.X,
  854. Y = mouseEvent.MousePosition.Y
  855. };
  856. if ((mouseEvent.ButtonState != 0 && mouseEvent.EventFlags == 0 && _lastMouseButtonPressed is null && !_isButtonDoubleClicked)
  857. || (_lastMouseButtonPressed == null
  858. && mouseEvent.EventFlags.HasFlag (WindowsConsole.EventFlags.MouseMoved)
  859. && mouseEvent.ButtonState != 0
  860. && !_isButtonReleased
  861. && !_isButtonDoubleClicked))
  862. {
  863. switch (mouseEvent.ButtonState)
  864. {
  865. case WindowsConsole.ButtonState.Button1Pressed:
  866. mouseFlag = MouseFlags.Button1Pressed;
  867. break;
  868. case WindowsConsole.ButtonState.Button2Pressed:
  869. mouseFlag = MouseFlags.Button2Pressed;
  870. break;
  871. case WindowsConsole.ButtonState.RightmostButtonPressed:
  872. mouseFlag = MouseFlags.Button3Pressed;
  873. break;
  874. }
  875. if (_point is null)
  876. {
  877. _point = p;
  878. }
  879. if (mouseEvent.EventFlags.HasFlag (WindowsConsole.EventFlags.MouseMoved))
  880. {
  881. _pointMove = p;
  882. mouseFlag |= MouseFlags.ReportMousePosition;
  883. _isButtonReleased = false;
  884. _processButtonClick = false;
  885. }
  886. _lastMouseButtonPressed = mouseEvent.ButtonState;
  887. _isButtonPressed = true;
  888. if ((mouseFlag & MouseFlags.ReportMousePosition) == 0)
  889. {
  890. // TODO: This makes ConsoleDriver dependent on Application, which is not ideal. This should be moved to Application.
  891. Application.MainLoop!.AddIdle (
  892. () =>
  893. {
  894. Task.Run (async () => await ProcessContinuousButtonPressedAsync (mouseFlag));
  895. return false;
  896. });
  897. }
  898. }
  899. else if (_lastMouseButtonPressed != null
  900. && mouseEvent.EventFlags == 0
  901. && !_isButtonReleased
  902. && !_isButtonDoubleClicked
  903. && !_isOneFingerDoubleClicked)
  904. {
  905. switch (_lastMouseButtonPressed)
  906. {
  907. case WindowsConsole.ButtonState.Button1Pressed:
  908. mouseFlag = MouseFlags.Button1Released;
  909. break;
  910. case WindowsConsole.ButtonState.Button2Pressed:
  911. mouseFlag = MouseFlags.Button2Released;
  912. break;
  913. case WindowsConsole.ButtonState.RightmostButtonPressed:
  914. mouseFlag = MouseFlags.Button3Released;
  915. break;
  916. }
  917. _isButtonPressed = false;
  918. _isButtonReleased = true;
  919. if (_point is { } && ((Point)_point).X == mouseEvent.MousePosition.X && ((Point)_point).Y == mouseEvent.MousePosition.Y)
  920. {
  921. _processButtonClick = true;
  922. }
  923. else
  924. {
  925. _point = null;
  926. }
  927. _processButtonClick = true;
  928. }
  929. else if (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseMoved
  930. && !_isOneFingerDoubleClicked
  931. && _isButtonReleased
  932. && p == _point)
  933. {
  934. mouseFlag = ProcessButtonClick (mouseEvent);
  935. }
  936. else if (mouseEvent.EventFlags.HasFlag (WindowsConsole.EventFlags.DoubleClick))
  937. {
  938. switch (mouseEvent.ButtonState)
  939. {
  940. case WindowsConsole.ButtonState.Button1Pressed:
  941. mouseFlag = MouseFlags.Button1DoubleClicked;
  942. break;
  943. case WindowsConsole.ButtonState.Button2Pressed:
  944. mouseFlag = MouseFlags.Button2DoubleClicked;
  945. break;
  946. case WindowsConsole.ButtonState.RightmostButtonPressed:
  947. mouseFlag = MouseFlags.Button3DoubleClicked;
  948. break;
  949. }
  950. _isButtonDoubleClicked = true;
  951. }
  952. else if (mouseEvent.EventFlags == 0 && mouseEvent.ButtonState != 0 && _isButtonDoubleClicked)
  953. {
  954. switch (mouseEvent.ButtonState)
  955. {
  956. case WindowsConsole.ButtonState.Button1Pressed:
  957. mouseFlag = MouseFlags.Button1TripleClicked;
  958. break;
  959. case WindowsConsole.ButtonState.Button2Pressed:
  960. mouseFlag = MouseFlags.Button2TripleClicked;
  961. break;
  962. case WindowsConsole.ButtonState.RightmostButtonPressed:
  963. mouseFlag = MouseFlags.Button3TripleClicked;
  964. break;
  965. }
  966. _isButtonDoubleClicked = false;
  967. }
  968. else if (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseWheeled)
  969. {
  970. switch ((int)mouseEvent.ButtonState)
  971. {
  972. case int v when v > 0:
  973. mouseFlag = MouseFlags.WheeledUp;
  974. break;
  975. case int v when v < 0:
  976. mouseFlag = MouseFlags.WheeledDown;
  977. break;
  978. }
  979. }
  980. else if (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseWheeled && mouseEvent.ControlKeyState == WindowsConsole.ControlKeyState.ShiftPressed)
  981. {
  982. switch ((int)mouseEvent.ButtonState)
  983. {
  984. case int v when v > 0:
  985. mouseFlag = MouseFlags.WheeledLeft;
  986. break;
  987. case int v when v < 0:
  988. mouseFlag = MouseFlags.WheeledRight;
  989. break;
  990. }
  991. }
  992. else if (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseHorizontalWheeled)
  993. {
  994. switch ((int)mouseEvent.ButtonState)
  995. {
  996. case int v when v < 0:
  997. mouseFlag = MouseFlags.WheeledLeft;
  998. break;
  999. case int v when v > 0:
  1000. mouseFlag = MouseFlags.WheeledRight;
  1001. break;
  1002. }
  1003. }
  1004. else if (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseMoved)
  1005. {
  1006. mouseFlag = MouseFlags.ReportMousePosition;
  1007. if (mouseEvent.MousePosition.X != _pointMove.X || mouseEvent.MousePosition.Y != _pointMove.Y)
  1008. {
  1009. _pointMove = new Point (mouseEvent.MousePosition.X, mouseEvent.MousePosition.Y);
  1010. }
  1011. }
  1012. else if (mouseEvent is { ButtonState: 0, EventFlags: 0 })
  1013. {
  1014. // This happens on a double or triple click event.
  1015. mouseFlag = MouseFlags.None;
  1016. }
  1017. mouseFlag = SetControlKeyStates (mouseEvent, mouseFlag);
  1018. //System.Diagnostics.Debug.WriteLine (
  1019. // $"point.X:{(point is { } ? ((Point)point).X : -1)};point.Y:{(point is { } ? ((Point)point).Y : -1)}");
  1020. return new MouseEventArgs
  1021. {
  1022. Position = new (mouseEvent.MousePosition.X, mouseEvent.MousePosition.Y),
  1023. Flags = mouseFlag
  1024. };
  1025. }
  1026. }