Core.cs 26 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084
  1. //
  2. // Core.cs: The core engine for gui.cs
  3. //
  4. // Authors:
  5. // Miguel de Icaza ([email protected])
  6. //
  7. // Pending:
  8. // - Check for NeedDisplay on the hierarchy and repaint
  9. // - Layout support
  10. // - "Colors" type or "Attributes" type?
  11. // - What to surface as "BackgroundCOlor" when clearing a window, an attribute or colors?
  12. //
  13. // Optimziations
  14. // - Add rendering limitation to the exposed area
  15. using System;
  16. using System.Collections;
  17. using System.Collections.Generic;
  18. using System.Threading;
  19. using System.Linq;
  20. namespace Terminal {
  21. public class Responder {
  22. public virtual bool CanFocus { get; set; }
  23. public virtual bool HasFocus { get; internal set; }
  24. // Key handling
  25. /// <summary>
  26. /// This method can be overwritten by view that
  27. /// want to provide accelerator functionality
  28. /// (Alt-key for example).
  29. /// </summary>
  30. /// <remarks>
  31. /// <para>
  32. /// Before keys are sent to the subview on the
  33. /// current view, all the views are
  34. /// processed and the key is passed to the widgets
  35. /// to allow some of them to process the keystroke
  36. /// as a hot-key. </para>
  37. /// <para>
  38. /// For example, if you implement a button that
  39. /// has a hotkey ok "o", you would catch the
  40. /// combination Alt-o here. If the event is
  41. /// caught, you must return true to stop the
  42. /// keystroke from being dispatched to other
  43. /// views.
  44. /// </para>
  45. /// </remarks>
  46. public virtual bool ProcessHotKey (KeyEvent kb)
  47. {
  48. return false;
  49. }
  50. /// <summary>
  51. /// If the view is focused, gives the view a
  52. /// chance to process the keystroke.
  53. /// </summary>
  54. /// <remarks>
  55. /// <para>
  56. /// Views can override this method if they are
  57. /// interested in processing the given keystroke.
  58. /// If they consume the keystroke, they must
  59. /// return true to stop the keystroke from being
  60. /// processed by other widgets or consumed by the
  61. /// widget engine. If they return false, the
  62. /// keystroke will be passed using the ProcessColdKey
  63. /// method to other views to process.
  64. /// </para>
  65. /// <para>
  66. /// The View implementation does nothing but return false,
  67. /// so it is not necessary to call base.ProcessKey if you
  68. /// derive directly from View, but you should if you derive
  69. /// other View subclasses.
  70. /// </para>
  71. /// </remarks>
  72. public virtual bool ProcessKey (KeyEvent kb)
  73. {
  74. return false;
  75. }
  76. /// <summary>
  77. /// This method can be overwritten by views that
  78. /// want to provide accelerator functionality
  79. /// (Alt-key for example), but without
  80. /// interefering with normal ProcessKey behavior.
  81. /// </summary>
  82. /// <remarks>
  83. /// <para>
  84. /// After keys are sent to the subviews on the
  85. /// current view, all the view are
  86. /// processed and the key is passed to the views
  87. /// to allow some of them to process the keystroke
  88. /// as a cold-key. </para>
  89. /// <para>
  90. /// This functionality is used, for example, by
  91. /// default buttons to act on the enter key.
  92. /// Processing this as a hot-key would prevent
  93. /// non-default buttons from consuming the enter
  94. /// keypress when they have the focus.
  95. /// </para>
  96. /// </remarks>
  97. public virtual bool ProcessColdKey (KeyEvent kb)
  98. {
  99. return false;
  100. }
  101. // Mouse events
  102. public virtual void MouseEvent (Event.Mouse me) { }
  103. }
  104. public class View : Responder, IEnumerable {
  105. string id = "";
  106. View container = null;
  107. View focused = null;
  108. public static ConsoleDriver Driver = Application.Driver;
  109. public static IList<View> empty = new List<View> (0).AsReadOnly ();
  110. List<View> subviews;
  111. public IList<View> Subviews => subviews == null ? empty : subviews.AsReadOnly ();
  112. internal Rect NeedDisplay { get; private set; } = Rect.Empty;
  113. // The frame for the object
  114. Rect frame;
  115. public string Id {
  116. get => id;
  117. set {
  118. id = value;
  119. }
  120. }
  121. // The frame for this view
  122. public Rect Frame {
  123. get => frame;
  124. set {
  125. if (SuperView != null) {
  126. SuperView.SetNeedsDisplay (frame);
  127. SuperView.SetNeedsDisplay (value);
  128. }
  129. frame = value;
  130. SetNeedsDisplay (frame);
  131. }
  132. }
  133. public IEnumerator GetEnumerator ()
  134. {
  135. foreach (var v in subviews)
  136. yield return v;
  137. }
  138. public Rect Bounds {
  139. get => new Rect (Point.Empty, Frame.Size);
  140. set {
  141. Frame = new Rect (frame.Location, value.Size);
  142. }
  143. }
  144. public View SuperView => container;
  145. public View (Rect frame)
  146. {
  147. this.Frame = frame;
  148. CanFocus = false;
  149. }
  150. /// <summary>
  151. /// Invoke to flag that this view needs to be redisplayed, by any code
  152. /// that alters the state of the view.
  153. /// </summary>
  154. public void SetNeedsDisplay ()
  155. {
  156. SetNeedsDisplay (Frame);
  157. }
  158. public void SetNeedsDisplay (Rect region)
  159. {
  160. if (NeedDisplay.IsEmpty)
  161. NeedDisplay = region;
  162. else {
  163. var x = Math.Min (NeedDisplay.X, region.X);
  164. var y = Math.Min (NeedDisplay.Y, region.Y);
  165. var w = Math.Max (NeedDisplay.Width, region.Width);
  166. var h = Math.Max (NeedDisplay.Height, region.Height);
  167. NeedDisplay = new Rect (x, y, w, h);
  168. }
  169. if (container != null)
  170. container.ChildNeedsDisplay ();
  171. if (subviews == null)
  172. return;
  173. foreach (var view in subviews)
  174. if (view.Frame.IntersectsWith (region)) {
  175. view.SetNeedsDisplay (Rect.Intersect (view.Frame, region));
  176. }
  177. }
  178. internal bool childNeedsDisplay;
  179. public void ChildNeedsDisplay ()
  180. {
  181. childNeedsDisplay = true;
  182. if (container != null)
  183. container.ChildNeedsDisplay ();
  184. }
  185. /// <summary>
  186. /// Adds a subview to this view.
  187. /// </summary>
  188. /// <remarks>
  189. /// </remarks>
  190. public virtual void Add (View view)
  191. {
  192. if (view == null)
  193. return;
  194. if (subviews == null)
  195. subviews = new List<View> ();
  196. subviews.Add (view);
  197. view.container = this;
  198. if (view.CanFocus)
  199. CanFocus = true;
  200. SetNeedsDisplay ();
  201. }
  202. public void Add (params View [] views)
  203. {
  204. if (views == null)
  205. return;
  206. foreach (var view in views)
  207. Add (view);
  208. }
  209. /// <summary>
  210. /// Removes all the widgets from this container.
  211. /// </summary>
  212. /// <remarks>
  213. /// </remarks>
  214. public virtual void RemoveAll ()
  215. {
  216. if (subviews == null)
  217. return;
  218. while (subviews.Count > 0) {
  219. var view = subviews [0];
  220. Remove (view);
  221. subviews.RemoveAt (0);
  222. }
  223. }
  224. /// <summary>
  225. /// Removes a widget from this container.
  226. /// </summary>
  227. /// <remarks>
  228. /// </remarks>
  229. public virtual void Remove (View view)
  230. {
  231. if (view == null)
  232. return;
  233. SetNeedsDisplay ();
  234. var touched = view.Frame;
  235. subviews.Remove (view);
  236. view.container = null;
  237. if (subviews.Count < 1)
  238. this.CanFocus = false;
  239. foreach (var v in subviews) {
  240. if (v.Frame.IntersectsWith (touched))
  241. view.SetNeedsDisplay ();
  242. }
  243. }
  244. /// <summary>
  245. /// Clears the view region with the current color.
  246. /// </summary>
  247. /// <remarks>
  248. /// <para>
  249. /// This clears the entire region used by this view.
  250. /// </para>
  251. /// </remarks>
  252. public void Clear ()
  253. {
  254. var h = Frame.Height;
  255. var w = Frame.Width;
  256. for (int line = 0; line < h; line++) {
  257. Move (0, line);
  258. for (int col = 0; col < w; col++)
  259. Driver.AddCh (' ');
  260. }
  261. }
  262. /// <summary>
  263. /// Converts the (col,row) position from the view into a screen (col,row). The values are clamped to (0..ScreenDim-1)
  264. /// </summary>
  265. /// <param name="col">View-based column.</param>
  266. /// <param name="row">View-based row.</param>
  267. /// <param name="rcol">Absolute column, display relative.</param>
  268. /// <param name="rrow">Absolute row, display relative.</param>
  269. internal void ViewToScreen (int col, int row, out int rcol, out int rrow, bool clipped = true)
  270. {
  271. // Computes the real row, col relative to the screen.
  272. rrow = row + frame.Y;
  273. rcol = col + frame.X;
  274. var ccontainer = container;
  275. while (ccontainer != null) {
  276. rrow += ccontainer.frame.Y;
  277. rcol += ccontainer.frame.X;
  278. ccontainer = ccontainer.container;
  279. }
  280. // The following ensures that the cursor is always in the screen boundaries.
  281. if (clipped) {
  282. rrow = Math.Max (0, Math.Min (rrow, Driver.Rows - 1));
  283. rcol = Math.Max (0, Math.Min (rcol, Driver.Cols - 1));
  284. }
  285. }
  286. // Converts a rectangle in view coordinates to screen coordinates.
  287. Rect RectToScreen (Rect rect)
  288. {
  289. ViewToScreen (rect.X, rect.Y, out var x, out var y, clipped: false);
  290. return new Rect (x, y, rect.Width, rect.Height);
  291. }
  292. // Clips a rectangle in screen coordinates to the dimensions currently available on the screen
  293. Rect ScreenClip (Rect rect)
  294. {
  295. var x = rect.X < 0 ? 0 : rect.X;
  296. var y = rect.Y < 0 ? 0 : rect.Y;
  297. var w = rect.X + rect.Width >= Driver.Cols ? Driver.Cols - rect.X : rect.Width;
  298. var h = rect.Y + rect.Height >= Driver.Rows ? Driver.Rows - rect.Y : rect.Height;
  299. return new Rect (x, y, w, h);
  300. }
  301. /// <summary>
  302. /// Draws a frame in the current view, clipped by the boundary of this view
  303. /// </summary>
  304. /// <param name="rect">Rectangular region for the frame to be drawn.</param>
  305. /// <param name="fill">If set to <c>true</c> it fill will the contents.</param>
  306. public void DrawFrame (Rect rect, bool fill = false)
  307. {
  308. var scrRect = RectToScreen (rect);
  309. var savedClip = Driver.Clip;
  310. Driver.Clip = ScreenClip (RectToScreen (Bounds));
  311. Driver.DrawFrame (scrRect, fill);
  312. Driver.Clip = savedClip;
  313. }
  314. /// <summary>
  315. /// Utility function to draw strings that contain a hotkey
  316. /// </summary>
  317. /// <param name="text">String to display, the underscoore before a letter flags the next letter as the hotkey.</param>
  318. /// <param name="hotColor">Hot color.</param>
  319. /// <param name="normalColor">Normal color.</param>
  320. public void DrawHotString (string text, Attribute hotColor, Attribute normalColor)
  321. {
  322. Driver.SetAttribute (normalColor);
  323. foreach (var c in text) {
  324. if (c == '_') {
  325. Driver.SetAttribute (hotColor);
  326. continue;
  327. }
  328. Driver.AddCh (c);
  329. Driver.SetAttribute (normalColor);
  330. }
  331. }
  332. /// <summary>
  333. /// Utility function to draw strings that contains a hotkey using a colorscheme and the "focused" state.
  334. /// </summary>
  335. /// <param name="text">String to display, the underscoore before a letter flags the next letter as the hotkey.</param>
  336. /// <param name="focused">If set to <c>true</c> this uses the focused colors from the color scheme, otherwise the regular ones.</param>
  337. /// <param name="scheme">The color scheme to use.</param>
  338. public void DrawHotString (string text, bool focused, ColorScheme scheme)
  339. {
  340. if (focused)
  341. DrawHotString (text, scheme.HotFocus, scheme.Focus);
  342. else
  343. DrawHotString (text, scheme.HotNormal, scheme.Normal);
  344. }
  345. /// <summary>
  346. /// This moves the cursor to the specified column and row in the view.
  347. /// </summary>
  348. /// <returns>The move.</returns>
  349. /// <param name="col">Col.</param>
  350. /// <param name="row">Row.</param>
  351. public void Move (int col, int row)
  352. {
  353. ViewToScreen (col, row, out var rcol, out var rrow);
  354. Driver.Move (rcol, rrow);
  355. }
  356. /// <summary>
  357. /// Positions the cursor in the right position based on the currently focused view in the chain.
  358. /// </summary>
  359. public virtual void PositionCursor ()
  360. {
  361. if (focused != null)
  362. focused.PositionCursor ();
  363. else
  364. Move (frame.X, frame.Y);
  365. }
  366. public override bool HasFocus {
  367. get {
  368. return base.HasFocus;
  369. }
  370. internal set {
  371. if (base.HasFocus != value)
  372. SetNeedsDisplay ();
  373. base.HasFocus = value;
  374. }
  375. }
  376. /// <summary>
  377. /// Returns the currently focused view inside this view, or null if nothing is focused.
  378. /// </summary>
  379. /// <value>The focused.</value>
  380. public View Focused => focused;
  381. public View MostFocused {
  382. get {
  383. if (Focused == null)
  384. return null;
  385. var most = Focused.MostFocused;
  386. if (most != null)
  387. return most;
  388. return Focused;
  389. }
  390. }
  391. /// <summary>
  392. /// Displays the specified character in the specified column and row.
  393. /// </summary>
  394. /// <param name="col">Col.</param>
  395. /// <param name="row">Row.</param>
  396. /// <param name="ch">Ch.</param>
  397. public void AddCh (int col, int row, int ch)
  398. {
  399. if (row < 0 || col < 0)
  400. return;
  401. if (row > frame.Height - 1 || col > frame.Width - 1)
  402. return;
  403. Move (col, row);
  404. Driver.AddCh (ch);
  405. }
  406. protected void ClearNeedsDisplay ()
  407. {
  408. NeedDisplay = Rect.Empty;
  409. childNeedsDisplay = false;
  410. }
  411. /// <summary>
  412. /// Performs a redraw of this view and its subviews, only redraws the views that have been flagged for a re-display.
  413. /// </summary>
  414. /// <remarks>
  415. /// The region argument is relative to the view itself.
  416. /// </remarks>
  417. public virtual void Redraw (Rect region)
  418. {
  419. var clipRect = new Rect (Point.Empty, frame.Size);
  420. if (subviews != null) {
  421. foreach (var view in subviews) {
  422. if (!view.NeedDisplay.IsEmpty || view.childNeedsDisplay) {
  423. if (view.Frame.IntersectsWith (clipRect) && view.Frame.IntersectsWith (region)) {
  424. // TODO: optimize this by computing the intersection of region and view.Bounds
  425. view.Redraw (view.Bounds);
  426. }
  427. view.NeedDisplay = Rect.Empty;
  428. view.childNeedsDisplay = false;
  429. }
  430. }
  431. }
  432. ClearNeedsDisplay ();
  433. }
  434. /// <summary>
  435. /// Focuses the specified sub-view.
  436. /// </summary>
  437. /// <param name="view">View.</param>
  438. public void SetFocus (View view)
  439. {
  440. if (view == null)
  441. return;
  442. //Console.WriteLine ($"Request to focus {view}");
  443. if (!view.CanFocus)
  444. return;
  445. if (focused == view)
  446. return;
  447. // Make sure that this view is a subview
  448. View c;
  449. for (c = view.container; c != null; c = c.container)
  450. if (c == this)
  451. break;
  452. if (c == null)
  453. throw new ArgumentException ("the specified view is not part of the hierarchy of this view");
  454. if (focused != null)
  455. focused.HasFocus = false;
  456. focused = view;
  457. focused.HasFocus = true;
  458. focused.EnsureFocus ();
  459. }
  460. public override bool ProcessKey (KeyEvent kb)
  461. {
  462. if (Focused?.ProcessKey (kb) == true)
  463. return true;
  464. return false;
  465. }
  466. public override bool ProcessHotKey (KeyEvent kb)
  467. {
  468. if (subviews == null || subviews.Count == 0)
  469. return false;
  470. foreach (var view in subviews)
  471. if (view.ProcessHotKey (kb))
  472. return true;
  473. return false;
  474. }
  475. public override bool ProcessColdKey (KeyEvent kb)
  476. {
  477. if (subviews == null || subviews.Count == 0)
  478. return false;
  479. foreach (var view in subviews)
  480. if (view.ProcessHotKey (kb))
  481. return true;
  482. return false;
  483. }
  484. /// <summary>
  485. /// Finds the first view in the hierarchy that wants to get the focus if nothing is currently focused, otherwise, it does nothing.
  486. /// </summary>
  487. public void EnsureFocus ()
  488. {
  489. if (focused == null)
  490. FocusFirst ();
  491. }
  492. /// <summary>
  493. /// Focuses the first focusable subview if one exists.
  494. /// </summary>
  495. public void FocusFirst ()
  496. {
  497. if (subviews == null) {
  498. SuperView.SetFocus (this);
  499. return;
  500. }
  501. foreach (var view in subviews) {
  502. if (view.CanFocus) {
  503. SetFocus (view);
  504. return;
  505. }
  506. }
  507. }
  508. /// <summary>
  509. /// Focuses the last focusable subview if one exists.
  510. /// </summary>
  511. public void FocusLast ()
  512. {
  513. if (subviews == null)
  514. return;
  515. for (int i = subviews.Count; i > 0;) {
  516. i--;
  517. View v = subviews [i];
  518. if (v.CanFocus) {
  519. SetFocus (v);
  520. return;
  521. }
  522. }
  523. }
  524. /// <summary>
  525. /// Focuses the previous view.
  526. /// </summary>
  527. /// <returns><c>true</c>, if previous was focused, <c>false</c> otherwise.</returns>
  528. public bool FocusPrev ()
  529. {
  530. if (subviews == null || subviews.Count == 0)
  531. return false;
  532. if (focused == null) {
  533. FocusLast ();
  534. return true;
  535. }
  536. int focused_idx = -1;
  537. for (int i = subviews.Count; i > 0;) {
  538. i--;
  539. View w = subviews [i];
  540. if (w.HasFocus) {
  541. if (w.FocusPrev ())
  542. return true;
  543. focused_idx = i;
  544. continue;
  545. }
  546. if (w.CanFocus && focused_idx != -1) {
  547. focused.HasFocus = false;
  548. if (w.CanFocus)
  549. w.FocusLast ();
  550. SetFocus (w);
  551. return true;
  552. }
  553. }
  554. if (focused_idx != -1) {
  555. FocusLast ();
  556. return true;
  557. }
  558. if (focused != null) {
  559. focused.HasFocus = false;
  560. focused = null;
  561. }
  562. return false;
  563. }
  564. /// <summary>
  565. /// Focuses the next view.
  566. /// </summary>
  567. /// <returns><c>true</c>, if next was focused, <c>false</c> otherwise.</returns>
  568. public bool FocusNext ()
  569. {
  570. if (subviews == null || subviews.Count == 0)
  571. return false;
  572. if (focused == null) {
  573. FocusFirst ();
  574. return focused != null;
  575. }
  576. int n = subviews.Count;
  577. int focused_idx = -1;
  578. for (int i = 0; i < n; i++) {
  579. View w = subviews [i];
  580. if (w.HasFocus) {
  581. if (w.FocusNext ())
  582. return true;
  583. focused_idx = i;
  584. continue;
  585. }
  586. if (w.CanFocus && focused_idx != -1) {
  587. focused.HasFocus = false;
  588. if (w != null && w.CanFocus)
  589. w.FocusFirst ();
  590. SetFocus (w);
  591. return true;
  592. }
  593. }
  594. if (focused != null) {
  595. focused.HasFocus = false;
  596. focused = null;
  597. }
  598. return false;
  599. }
  600. public virtual void LayoutSubviews ()
  601. {
  602. }
  603. public override string ToString ()
  604. {
  605. return $"{GetType ().Name}({id})({Frame})";
  606. }
  607. }
  608. /// <summary>
  609. /// Toplevel views can be modally executed.
  610. /// </summary>
  611. /// <remarks>
  612. /// <para>
  613. /// Toplevels can be modally executing views, and they return control
  614. /// to the caller when the "Running" property is set to false.
  615. /// </para>
  616. /// </remarks>
  617. public class Toplevel : View {
  618. public bool Running;
  619. public Toplevel (Rect frame) : base (frame)
  620. {
  621. }
  622. public static Toplevel Create ()
  623. {
  624. return new Toplevel (new Rect (0, 0, Driver.Cols, Driver.Rows));
  625. }
  626. public override bool CanFocus {
  627. get => true;
  628. }
  629. public override bool ProcessKey (KeyEvent kb)
  630. {
  631. if (base.ProcessKey (kb))
  632. return true;
  633. switch (kb.Key) {
  634. case Key.ControlC:
  635. // TODO: stop current execution of this container
  636. break;
  637. case Key.ControlZ:
  638. // TODO: should suspend
  639. // console_csharp_send_sigtstp ();
  640. break;
  641. case Key.Tab:
  642. var old = Focused;
  643. if (!FocusNext ())
  644. FocusNext ();
  645. if (old != Focused) {
  646. old?.SetNeedsDisplay ();
  647. Focused?.SetNeedsDisplay ();
  648. }
  649. return true;
  650. case Key.BackTab:
  651. old = Focused;
  652. if (!FocusPrev ())
  653. FocusPrev ();
  654. if (old != Focused) {
  655. old?.SetNeedsDisplay ();
  656. Focused?.SetNeedsDisplay ();
  657. }
  658. return true;
  659. case Key.ControlL:
  660. Application.Refresh ();
  661. return true;
  662. }
  663. return false;
  664. }
  665. }
  666. /// <summary>
  667. /// A toplevel view that draws a frame around its region and has a "ContentView" subview where the contents are added.
  668. /// </summary>
  669. public class Window : Toplevel, IEnumerable {
  670. View contentView;
  671. string title;
  672. public string Title {
  673. get => title;
  674. set {
  675. title = value;
  676. SetNeedsDisplay ();
  677. }
  678. }
  679. class ContentView : View {
  680. public ContentView (Rect frame) : base (frame) { }
  681. }
  682. /// <summary>
  683. /// Initializes a new instance of the <see cref="T:Terminal.Window"/> class with an optioanl title
  684. /// </summary>
  685. /// <param name="frame">Frame.</param>
  686. /// <param name="title">Title.</param>
  687. public Window (Rect frame, string title = null) : base (frame)
  688. {
  689. this.Title = title;
  690. frame.Inflate (-1, -1);
  691. contentView = new ContentView (frame);
  692. base.Add (contentView);
  693. }
  694. /// <summary>
  695. /// Enumerates the various views in the ContentView.
  696. /// </summary>
  697. /// <returns>The enumerator.</returns>
  698. public new IEnumerator GetEnumerator ()
  699. {
  700. return contentView.GetEnumerator ();
  701. }
  702. void DrawFrame ()
  703. {
  704. DrawFrame (new Rect (0, 0, Frame.Width, Frame.Height), true);
  705. }
  706. /// <summary>
  707. /// Add the specified view to the ContentView.
  708. /// </summary>
  709. /// <param name="view">View to add to the window.</param>
  710. public override void Add (View view)
  711. {
  712. contentView.Add (view);
  713. }
  714. public override void Redraw (Rect bounds)
  715. {
  716. if (!NeedDisplay.IsEmpty) {
  717. Driver.SetAttribute (Colors.Base.Normal);
  718. DrawFrame ();
  719. if (HasFocus)
  720. Driver.SetAttribute (Colors.Dialog.Normal);
  721. var width = Frame.Width;
  722. if (Title != null && width > 4) {
  723. Move (1, 0);
  724. Driver.AddCh (' ');
  725. var str = Title.Length > width ? Title.Substring (0, width - 4) : Title;
  726. Driver.AddStr (str);
  727. Driver.AddCh (' ');
  728. }
  729. Driver.SetAttribute (Colors.Dialog.Normal);
  730. }
  731. contentView.Redraw (contentView.Bounds);
  732. ClearNeedsDisplay ();
  733. }
  734. }
  735. /// <summary>
  736. /// The application driver for gui.cs
  737. /// </summary>
  738. /// <remarks>
  739. /// <para>
  740. /// You can hook up to the Iteration event to have your method
  741. /// invoked on each iteration of the mainloop.
  742. /// </para>
  743. /// <para>
  744. /// Creates a mainloop to process input events, handle timers and
  745. /// other sources of data. It is accessible via the MainLoop property.
  746. /// </para>
  747. /// <para>
  748. /// When invoked sets the SynchronizationContext to one that is tied
  749. /// to the mainloop, allowing user code to use async/await.
  750. /// </para>
  751. /// </remarks>
  752. public class Application {
  753. public static ConsoleDriver Driver = new CursesDriver ();
  754. public static Toplevel Top { get; private set; }
  755. public static View Current { get; private set; }
  756. public static Mono.Terminal.MainLoop MainLoop { get; private set; }
  757. static Stack<View> toplevels = new Stack<View> ();
  758. /// <summary>
  759. /// This event is raised on each iteration of the
  760. /// main loop.
  761. /// </summary>
  762. /// <remarks>
  763. /// See also <see cref="Timeout"/>
  764. /// </remarks>
  765. static public event EventHandler Iteration;
  766. /// <summary>
  767. /// Returns a rectangle that is centered in the screen for the provided size.
  768. /// </summary>
  769. /// <returns>The centered rect.</returns>
  770. /// <param name="size">Size for the rectangle.</param>
  771. public static Rect MakeCenteredRect (Size size)
  772. {
  773. return new Rect (new Point ((Driver.Cols - size.Width) / 2, (Driver.Rows - size.Height) / 2), size);
  774. }
  775. //
  776. // provides the sync context set while executing code in gui.cs, to let
  777. // users use async/await on their code
  778. //
  779. class MainLoopSyncContext : SynchronizationContext {
  780. Mono.Terminal.MainLoop mainLoop;
  781. public MainLoopSyncContext (Mono.Terminal.MainLoop mainLoop)
  782. {
  783. this.mainLoop = mainLoop;
  784. }
  785. public override SynchronizationContext CreateCopy ()
  786. {
  787. return new MainLoopSyncContext (MainLoop);
  788. }
  789. public override void Post (SendOrPostCallback d, object state)
  790. {
  791. mainLoop.AddIdle (() => {
  792. d (state);
  793. return false;
  794. });
  795. }
  796. public override void Send (SendOrPostCallback d, object state)
  797. {
  798. mainLoop.Invoke (() => {
  799. d (state);
  800. });
  801. }
  802. }
  803. /// <summary>
  804. /// Initializes the Application
  805. /// </summary>
  806. public static void Init ()
  807. {
  808. if (Top != null)
  809. return;
  810. Driver.Init (TerminalResized);
  811. MainLoop = new Mono.Terminal.MainLoop ();
  812. SynchronizationContext.SetSynchronizationContext (new MainLoopSyncContext (MainLoop));
  813. Top = Toplevel.Create ();
  814. Current = Top;
  815. }
  816. public class RunState : IDisposable {
  817. internal RunState (Toplevel view)
  818. {
  819. Toplevel = view;
  820. }
  821. internal Toplevel Toplevel;
  822. public void Dispose ()
  823. {
  824. Dispose (true);
  825. GC.SuppressFinalize (this);
  826. }
  827. public virtual void Dispose (bool disposing)
  828. {
  829. if (Toplevel != null) {
  830. Application.End (Toplevel);
  831. Toplevel = null;
  832. }
  833. }
  834. }
  835. static void ProcessKeyEvent (KeyEvent ke)
  836. {
  837. if (Current.ProcessHotKey (ke))
  838. return;
  839. if (Current.ProcessKey (ke))
  840. return;
  841. // Process the key normally
  842. if (Current.ProcessColdKey (ke))
  843. return;
  844. }
  845. static public RunState Begin (Toplevel toplevel)
  846. {
  847. if (toplevel == null)
  848. throw new ArgumentNullException (nameof (toplevel));
  849. var rs = new RunState (toplevel);
  850. Init ();
  851. toplevels.Push (toplevel);
  852. Current = toplevel;
  853. Driver.PrepareToRun (MainLoop, ProcessKeyEvent);
  854. toplevel.LayoutSubviews ();
  855. toplevel.FocusFirst ();
  856. Redraw (toplevel);
  857. toplevel.PositionCursor ();
  858. Driver.Refresh ();
  859. return rs;
  860. }
  861. static public void End (RunState rs)
  862. {
  863. if (rs == null)
  864. throw new ArgumentNullException (nameof (rs));
  865. rs.Dispose ();
  866. }
  867. static void Shutdown ()
  868. {
  869. Driver.End ();
  870. }
  871. static void Redraw (View view)
  872. {
  873. view.Redraw (view.Bounds);
  874. Driver.Refresh ();
  875. }
  876. static void Refresh (View view)
  877. {
  878. view.Redraw (view.Bounds);
  879. Driver.Refresh ();
  880. }
  881. /// <summary>
  882. /// Triggers a refresh of the entire display.
  883. /// </summary>
  884. public static void Refresh ()
  885. {
  886. Driver.RedrawTop ();
  887. View last = null;
  888. foreach (var v in toplevels.Reverse ()) {
  889. v.SetNeedsDisplay ();
  890. v.Redraw (v.Bounds);
  891. last = v;
  892. }
  893. last?.PositionCursor ();
  894. Driver.Refresh ();
  895. }
  896. internal static void End (View view)
  897. {
  898. if (toplevels.Peek () != view)
  899. throw new ArgumentException ("The view that you end with must be balanced");
  900. toplevels.Pop ();
  901. if (toplevels.Count == 0)
  902. Shutdown ();
  903. else {
  904. Current = toplevels.Peek ();
  905. Refresh ();
  906. }
  907. }
  908. /// <summary>
  909. /// Runs the main loop for the created dialog
  910. /// </summary>
  911. /// <remarks>
  912. /// Use the wait parameter to control whether this is a
  913. /// blocking or non-blocking call.
  914. /// </remarks>
  915. public static void RunLoop (RunState state, bool wait = true)
  916. {
  917. if (state == null)
  918. throw new ArgumentNullException (nameof (state));
  919. if (state.Toplevel == null)
  920. throw new ObjectDisposedException ("state");
  921. for (state.Toplevel.Running = true; state.Toplevel.Running;) {
  922. if (MainLoop.EventsPending (wait)) {
  923. MainLoop.MainIteration ();
  924. if (Iteration != null)
  925. Iteration (null, EventArgs.Empty);
  926. } else if (wait == false)
  927. return;
  928. if (!state.Toplevel.NeedDisplay.IsEmpty || state.Toplevel.childNeedsDisplay) {
  929. state.Toplevel.Redraw (state.Toplevel.Bounds);
  930. state.Toplevel.PositionCursor ();
  931. Driver.Refresh ();
  932. }
  933. }
  934. }
  935. public static void Run ()
  936. {
  937. Run (Top);
  938. }
  939. /// <summary>
  940. /// Runs the main loop on the given container.
  941. /// </summary>
  942. /// <remarks>
  943. /// <para>
  944. /// This method is used to start processing events
  945. /// for the main application, but it is also used to
  946. /// run modal dialog boxes.
  947. /// </para>
  948. /// <para>
  949. /// To make a toplevel stop execution, set the "Running"
  950. /// property to false.
  951. /// </para>
  952. /// </remarks>
  953. public static void Run (Toplevel view)
  954. {
  955. var runToken = Begin (view);
  956. RunLoop (runToken);
  957. End (runToken);
  958. }
  959. static void TerminalResized ()
  960. {
  961. foreach (var t in toplevels) {
  962. t.Frame = new Rect (0, 0, Driver.Cols, Driver.Rows);
  963. }
  964. }
  965. }
  966. }