StatusBar.cs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  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.Position.X >= pos && me.Position.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? OnInvokingKeyBindings (Key keyEvent)
  187. {
  188. // This is a bit of a hack. We want to handle the key bindings for status bar but
  189. // InvokeKeyBindings doesn't pass any context so we can't tell which item it is for.
  190. // So before we call the base class we set SelectedItem appropriately.
  191. Key key = new (keyEvent);
  192. if (KeyBindings.TryGet (key, out _))
  193. {
  194. // Search RadioLabels
  195. foreach (StatusItem item in Items)
  196. {
  197. if (item.Shortcut == key)
  198. {
  199. _itemToInvoke = item;
  200. //keyEvent.Scope = KeyBindingScope.HotKey;
  201. break;
  202. }
  203. }
  204. }
  205. return base.OnInvokingKeyBindings (keyEvent);
  206. }
  207. /// <summary>Removes a <see cref="StatusItem"/> at specified index of <see cref="Items"/>.</summary>
  208. /// <param name="index">The zero-based index of the item to remove.</param>
  209. /// <returns>The <see cref="StatusItem"/> removed.</returns>
  210. public StatusItem RemoveItem (int index)
  211. {
  212. List<StatusItem> itemsList = new (Items);
  213. StatusItem item = itemsList [index];
  214. itemsList.RemoveAt (index);
  215. Items = itemsList.ToArray ();
  216. SetNeedsDisplay ();
  217. return item;
  218. }
  219. private Attribute DetermineColorSchemeFor (StatusItem item)
  220. {
  221. if (item is { })
  222. {
  223. if (item.IsEnabled ())
  224. {
  225. return GetNormalColor ();
  226. }
  227. return ColorScheme.Disabled;
  228. }
  229. return GetNormalColor ();
  230. }
  231. private int GetItemTitleLength (string title)
  232. {
  233. var len = 0;
  234. foreach (char ch in title)
  235. {
  236. if (ch == '~')
  237. {
  238. continue;
  239. }
  240. len++;
  241. }
  242. return len;
  243. }
  244. private bool? InvokeItem ()
  245. {
  246. if (_itemToInvoke is { Action: { } })
  247. {
  248. _itemToInvoke.Action.Invoke ();
  249. return true;
  250. }
  251. return false;
  252. }
  253. private void Run (Action action)
  254. {
  255. if (action is null)
  256. {
  257. return;
  258. }
  259. Application.MainLoop.AddIdle (
  260. () =>
  261. {
  262. action ();
  263. return false;
  264. }
  265. );
  266. }
  267. private Attribute ToggleScheme (Attribute scheme)
  268. {
  269. Attribute result = scheme == ColorScheme.Normal ? ColorScheme.HotNormal : ColorScheme.Normal;
  270. Driver.SetAttribute (result);
  271. return result;
  272. }
  273. }