StatusBar.cs 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Text;
  4. namespace Terminal.Gui;
  5. /// <summary>
  6. /// <see cref="StatusItem"/> objects are contained by <see cref="StatusBar"/> <see cref="View"/>s.
  7. /// Each <see cref="StatusItem"/> has a title, a shortcut (hotkey), and an <see cref="Command"/> that will be invoked when the
  8. /// <see cref="StatusItem.Shortcut"/> is pressed.
  9. /// The <see cref="StatusItem.Shortcut"/> will be a global hotkey for the application in the current context of the screen.
  10. /// The color of the <see cref="StatusItem.Title"/> will be changed after each ~.
  11. /// A <see cref="StatusItem.Title"/> set to `~F1~ Help` will render as *F1* using <see cref="ColorScheme.HotNormal"/> and
  12. /// *Help* as <see cref="ColorScheme.HotNormal"/>.
  13. /// </summary>
  14. public class StatusItem {
  15. /// <summary>
  16. /// Initializes a new <see cref="StatusItem"/>.
  17. /// </summary>
  18. /// <param name="shortcut">Shortcut to activate the <see cref="StatusItem"/>.</param>
  19. /// <param name="title">Title for the <see cref="StatusItem"/>.</param>
  20. /// <param name="action">Action to invoke when the <see cref="StatusItem"/> is activated.</param>
  21. /// <param name="canExecute">Function to determine if the action can currently be executed.</param>
  22. public StatusItem (Key shortcut, string title, Action action, Func<bool> canExecute = null)
  23. {
  24. Title = title ?? "";
  25. Shortcut = shortcut;
  26. Action = action;
  27. CanExecute = canExecute;
  28. }
  29. /// <summary>
  30. /// Gets the global shortcut to invoke the action on the menu.
  31. /// </summary>
  32. public Key Shortcut { get; set; }
  33. /// <summary>
  34. /// Gets or sets the title.
  35. /// </summary>
  36. /// <value>The title.</value>
  37. /// <remarks>
  38. /// The colour of the <see cref="StatusItem.Title"/> will be changed after each ~.
  39. /// A <see cref="StatusItem.Title"/> set to `~F1~ Help` will render as *F1* using <see cref="ColorScheme.HotNormal"/> and
  40. /// *Help* as <see cref="ColorScheme.HotNormal"/>.
  41. /// </remarks>
  42. public string Title { get; set; }
  43. /// <summary>
  44. /// Gets or sets the action to be invoked when the statusbar item is triggered
  45. /// </summary>
  46. /// <value>Action to invoke.</value>
  47. public Action Action { get; set; }
  48. /// <summary>
  49. /// Gets or sets the action to be invoked to determine if the <see cref="StatusItem"/> can be triggered.
  50. /// If <see cref="CanExecute"/> returns <see langword="true"/> the status item will be enabled. Otherwise, it will be disabled.
  51. /// </summary>
  52. /// <value>Function to determine if the action is can be executed or not.</value>
  53. public Func<bool> CanExecute { get; set; }
  54. /// <summary>
  55. /// Returns <see langword="true"/> if the status item is enabled. This method is a wrapper around <see cref="CanExecute"/>.
  56. /// </summary>
  57. public bool IsEnabled ()
  58. {
  59. return CanExecute?.Invoke () ?? true;
  60. }
  61. /// <summary>
  62. /// Gets or sets arbitrary data for the status item.
  63. /// </summary>
  64. /// <remarks>This property is not used internally.</remarks>
  65. public object Data { get; set; }
  66. };
  67. /// <summary>
  68. /// A status bar is a <see cref="View"/> that snaps to the bottom of a <see cref="Toplevel"/> displaying set of <see cref="StatusItem"/>s.
  69. /// The <see cref="StatusBar"/> should be context sensitive. This means, if the main menu and an open text editor are visible, the items probably shown will
  70. /// be ~F1~ Help ~F2~ Save ~F3~ Load. While a dialog to ask a file to load is executed, the remaining commands will probably be ~F1~ Help.
  71. /// So for each context must be a new instance of a status bar.
  72. /// </summary>
  73. public class StatusBar : View {
  74. /// <summary>
  75. /// The items that compose the <see cref="StatusBar"/>
  76. /// </summary>
  77. public StatusItem [] Items {
  78. get => _items;
  79. set {
  80. foreach (var item in _items) {
  81. KeyBindings.Remove ((KeyCode)item.Shortcut);
  82. }
  83. _items = value;
  84. foreach (var item in _items) {
  85. KeyBindings.Add ((KeyCode)item.Shortcut, KeyBindingScope.HotKey, Command.Accept);
  86. }
  87. }
  88. }
  89. /// <summary>
  90. /// Initializes a new instance of the <see cref="StatusBar"/> class.
  91. /// </summary>
  92. public StatusBar () : this (items: new StatusItem [] { }) { }
  93. /// <summary>
  94. /// Initializes a new instance of the <see cref="StatusBar"/> class with the specified set of <see cref="StatusItem"/>s.
  95. /// The <see cref="StatusBar"/> will be drawn on the lowest line of the terminal or <see cref="View.SuperView"/> (if not null).
  96. /// </summary>
  97. /// <param name="items">A list of status bar items.</param>
  98. public StatusBar (StatusItem [] items) : base ()
  99. {
  100. if (items != null) {
  101. Items = items;
  102. }
  103. CanFocus = false;
  104. ColorScheme = Colors.ColorSchemes ["Menu"];
  105. X = 0;
  106. Y = Pos.AnchorEnd (1);
  107. Width = Dim.Fill ();
  108. Height = 1;
  109. AddCommand (Command.Accept, InvokeItem);
  110. }
  111. StatusItem _itemToInvoke;
  112. bool? InvokeItem ()
  113. {
  114. if (_itemToInvoke is { Action: not null }) {
  115. _itemToInvoke.Action.Invoke ();
  116. return true;
  117. }
  118. return false;
  119. }
  120. /// <inheritdoc/>
  121. public override bool? OnInvokingKeyBindings (Key keyEvent)
  122. {
  123. // This is a bit of a hack. We want to handle the key bindings for status bar but
  124. // InvokeKeyBindings doesn't pass any context so we can't tell which item it is for.
  125. // So before we call the base class we set SelectedItem appropriately.
  126. var key = keyEvent.KeyCode;
  127. if (KeyBindings.TryGet(key, out _)) {
  128. // Search RadioLabels
  129. foreach (var item in Items) {
  130. if (item.Shortcut == key) {
  131. _itemToInvoke = item;
  132. keyEvent.Scope = KeyBindingScope.HotKey;
  133. break;
  134. }
  135. }
  136. }
  137. return base.OnInvokingKeyBindings (keyEvent);
  138. }
  139. static Rune _shortcutDelimiter = (Rune)'=';
  140. StatusItem [] _items = new StatusItem [] { };
  141. /// <summary>
  142. /// Gets or sets shortcut delimiter separator. The default is "-".
  143. /// </summary>
  144. public static Rune ShortcutDelimiter {
  145. get => _shortcutDelimiter;
  146. set {
  147. if (_shortcutDelimiter != value) {
  148. _shortcutDelimiter = value == default ? (Rune)'=' : value;
  149. }
  150. }
  151. }
  152. Attribute ToggleScheme (Attribute scheme)
  153. {
  154. var result = scheme == ColorScheme.Normal ? ColorScheme.HotNormal : ColorScheme.Normal;
  155. Driver.SetAttribute (result);
  156. return result;
  157. }
  158. Attribute DetermineColorSchemeFor (StatusItem item)
  159. {
  160. if (item != null) {
  161. if (item.IsEnabled ()) {
  162. return GetNormalColor ();
  163. }
  164. return ColorScheme.Disabled;
  165. }
  166. return GetNormalColor ();
  167. }
  168. ///<inheritdoc/>
  169. public override void OnDrawContent (Rect contentArea)
  170. {
  171. Move (0, 0);
  172. Driver.SetAttribute (GetNormalColor ());
  173. for (int i = 0; i < Frame.Width; i++) {
  174. Driver.AddRune ((Rune)' ');
  175. }
  176. Move (1, 0);
  177. var scheme = GetNormalColor ();
  178. Driver.SetAttribute (scheme);
  179. for (int i = 0; i < Items.Length; i++) {
  180. var title = Items [i].Title;
  181. Driver.SetAttribute (DetermineColorSchemeFor (Items [i]));
  182. for (int n = 0; n < Items [i].Title.GetRuneCount (); n++) {
  183. if (title [n] == '~') {
  184. if (Items [i].IsEnabled ()) {
  185. scheme = ToggleScheme (scheme);
  186. }
  187. continue;
  188. }
  189. Driver.AddRune ((Rune)title [n]);
  190. }
  191. if (i + 1 < Items.Length) {
  192. Driver.AddRune ((Rune)' ');
  193. Driver.AddRune (CM.Glyphs.VLine);
  194. Driver.AddRune ((Rune)' ');
  195. }
  196. }
  197. }
  198. ///<inheritdoc/>
  199. public override bool MouseEvent (MouseEvent me)
  200. {
  201. if (me.Flags != MouseFlags.Button1Clicked)
  202. return false;
  203. int pos = 1;
  204. for (int i = 0; i < Items.Length; i++) {
  205. if (me.X >= pos && me.X < pos + GetItemTitleLength (Items [i].Title)) {
  206. var item = Items [i];
  207. if (item.IsEnabled ()) {
  208. Run (item.Action);
  209. }
  210. break;
  211. }
  212. pos += GetItemTitleLength (Items [i].Title) + 3;
  213. }
  214. return true;
  215. }
  216. int GetItemTitleLength (string title)
  217. {
  218. int len = 0;
  219. foreach (var ch in title) {
  220. if (ch == '~')
  221. continue;
  222. len++;
  223. }
  224. return len;
  225. }
  226. void Run (Action action)
  227. {
  228. if (action == null)
  229. return;
  230. Application.MainLoop.AddIdle (() => {
  231. action ();
  232. return false;
  233. });
  234. }
  235. ///<inheritdoc/>
  236. public override bool OnEnter (View view)
  237. {
  238. Application.Driver.SetCursorVisibility (CursorVisibility.Invisible);
  239. return base.OnEnter (view);
  240. }
  241. /// <summary>
  242. /// Inserts a <see cref="StatusItem"/> in the specified index of <see cref="Items"/>.
  243. /// </summary>
  244. /// <param name="index">The zero-based index at which item should be inserted.</param>
  245. /// <param name="item">The item to insert.</param>
  246. public void AddItemAt (int index, StatusItem item)
  247. {
  248. var itemsList = new List<StatusItem> (Items);
  249. itemsList.Insert (index, item);
  250. Items = itemsList.ToArray ();
  251. SetNeedsDisplay ();
  252. }
  253. /// <summary>
  254. /// Removes a <see cref="StatusItem"/> at specified index of <see cref="Items"/>.
  255. /// </summary>
  256. /// <param name="index">The zero-based index of the item to remove.</param>
  257. /// <returns>The <see cref="StatusItem"/> removed.</returns>
  258. public StatusItem RemoveItem (int index)
  259. {
  260. var itemsList = new List<StatusItem> (Items);
  261. var item = itemsList [index];
  262. itemsList.RemoveAt (index);
  263. Items = itemsList.ToArray ();
  264. SetNeedsDisplay ();
  265. return item;
  266. }
  267. }