StatusBar.cs 11 KB

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