Menu.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  1. //
  2. // Menu.cs: application menus and submenus
  3. //
  4. // Authors:
  5. // Miguel de Icaza ([email protected])
  6. //
  7. // TODO:
  8. // Add accelerator support, but should also support chords (ShortCut in MenuItem)
  9. // Allow menus inside menus
  10. using System;
  11. using NStack;
  12. using System.Linq;
  13. namespace Terminal.Gui {
  14. /// <summary>
  15. /// A menu item has a title, an associated help text, and an action to execute on activation.
  16. /// </summary>
  17. public class MenuItem {
  18. /// <summary>
  19. /// Initializes a new <see cref="T:Terminal.Gui.MenuItem"/>.
  20. /// </summary>
  21. /// <param name="title">Title for the menu item.</param>
  22. /// <param name="help">Help text to display.</param>
  23. /// <param name="action">Action to invoke when the menu item is activated.</param>
  24. public MenuItem (ustring title, string help, Action action)
  25. {
  26. Title = title ?? "";
  27. Help = help ?? "";
  28. Action = action;
  29. bool nextIsHot = false;
  30. foreach (var x in Title) {
  31. if (x == '_')
  32. nextIsHot = true;
  33. else {
  34. if (nextIsHot) {
  35. HotKey = Char.ToUpper ((char)x);
  36. break;
  37. }
  38. nextIsHot = false;
  39. }
  40. }
  41. }
  42. //
  43. //
  44. /// <summary>
  45. /// The hotkey is used when the menu is active, the shortcut can be triggered when the menu is not active.
  46. /// For example HotKey would be "N" when the File Menu is open (assuming there is a "_New" entry
  47. /// if the ShortCut is set to "Control-N", this would be a global hotkey that would trigger as well
  48. /// </summary>
  49. public Rune HotKey;
  50. /// <summary>
  51. /// This is the global setting that can be used as a global shortcut to invoke the action on the menu.
  52. /// </summary>
  53. public Key ShortCut;
  54. /// <summary>
  55. /// Gets or sets the title.
  56. /// </summary>
  57. /// <value>The title.</value>
  58. public ustring Title { get; set; }
  59. /// <summary>
  60. /// Gets or sets the help text for the menu item.
  61. /// </summary>
  62. /// <value>The help text.</value>
  63. public ustring Help { get; set; }
  64. /// <summary>
  65. /// Gets or sets the action to be invoked when the menu is triggered
  66. /// </summary>
  67. /// <value>Method to invoke.</value>
  68. public Action Action { get; set; }
  69. internal int Width => Title.Length + Help.Length + 1 + 2;
  70. }
  71. /// <summary>
  72. /// A menu bar item contains other menu items.
  73. /// </summary>
  74. public class MenuBarItem {
  75. public MenuBarItem (ustring title, MenuItem [] children)
  76. {
  77. SetTitle (title ?? "");
  78. Children = children;
  79. }
  80. void SetTitle (ustring title)
  81. {
  82. if (title == null)
  83. title = "";
  84. Title = title;
  85. int len = 0;
  86. foreach (var ch in Title) {
  87. if (ch == '_')
  88. continue;
  89. len++;
  90. }
  91. TitleLength = len;
  92. }
  93. /// <summary>
  94. /// Gets or sets the title to display.
  95. /// </summary>
  96. /// <value>The title.</value>
  97. public ustring Title { get; set; }
  98. /// <summary>
  99. /// Gets or sets the children for this MenuBarItem
  100. /// </summary>
  101. /// <value>The children.</value>
  102. public MenuItem [] Children { get; set; }
  103. internal int TitleLength { get; private set; }
  104. }
  105. class Menu : View {
  106. MenuBarItem barItems;
  107. MenuBar host;
  108. int current;
  109. static Rect MakeFrame (int x, int y, MenuItem [] items)
  110. {
  111. int maxW = 0;
  112. foreach (var item in items) {
  113. var l = item.Width;
  114. maxW = Math.Max (l, maxW);
  115. }
  116. return new Rect (x, y, maxW + 2, items.Length + 2);
  117. }
  118. public Menu (MenuBar host, int x, int y, MenuBarItem barItems) : base (MakeFrame (x, y, barItems.Children))
  119. {
  120. this.barItems = barItems;
  121. this.host = host;
  122. current = -1;
  123. for (int i = 0; i < barItems.Children.Length; i++) {
  124. if (barItems.Children[i] != null) {
  125. current = i;
  126. break;
  127. }
  128. }
  129. ColorScheme = Colors.Menu;
  130. CanFocus = true;
  131. }
  132. public override void Redraw (Rect region)
  133. {
  134. Driver.SetAttribute (ColorScheme.Normal);
  135. DrawFrame (region, padding: 0, fill: true);
  136. for (int i = 0; i < barItems.Children.Length; i++){
  137. var item = barItems.Children [i];
  138. Move (1, i+1);
  139. Driver.SetAttribute (item == null ? Colors.Base.Focus : i == current ? ColorScheme.Focus : ColorScheme.Normal);
  140. for (int p = 0; p < Frame.Width-2; p++)
  141. if (item == null)
  142. Driver.AddRune (Driver.HLine);
  143. else
  144. Driver.AddRune (' ');
  145. if (item == null)
  146. continue;
  147. Move (2, i + 1);
  148. DrawHotString (item.Title,
  149. i == current? ColorScheme.HotFocus : ColorScheme.HotNormal,
  150. i == current ? ColorScheme.Focus : ColorScheme.Normal);
  151. // The help string
  152. var l = item.Help.Length;
  153. Move (Frame.Width - l - 2, 1 + i);
  154. Driver.AddStr (item.Help);
  155. }
  156. }
  157. public override void PositionCursor ()
  158. {
  159. Move (2, 1 + current);
  160. }
  161. void Run (Action action)
  162. {
  163. if (action == null)
  164. return;
  165. Application.MainLoop.AddIdle (() => {
  166. action ();
  167. return false;
  168. });
  169. }
  170. public override bool ProcessKey (KeyEvent kb)
  171. {
  172. switch (kb.Key) {
  173. case Key.CursorUp:
  174. if (current == -1)
  175. break;
  176. do {
  177. current--;
  178. if (current < 0)
  179. current = barItems.Children.Length - 1;
  180. } while (barItems.Children [current] == null);
  181. SetNeedsDisplay ();
  182. break;
  183. case Key.CursorDown:
  184. do {
  185. current++;
  186. if (current == barItems.Children.Length)
  187. current = 0;
  188. } while (barItems.Children [current] == null);
  189. SetNeedsDisplay ();
  190. break;
  191. case Key.CursorLeft:
  192. host.PreviousMenu ();
  193. break;
  194. case Key.CursorRight:
  195. host.NextMenu ();
  196. break;
  197. case Key.Esc:
  198. host.CloseMenu ();
  199. break;
  200. case Key.Enter:
  201. host.CloseMenu ();
  202. Run (barItems.Children [current].Action);
  203. break;
  204. default:
  205. // TODO: rune-ify
  206. if (Char.IsLetterOrDigit ((char)kb.KeyValue)) {
  207. var x = Char.ToUpper ((char)kb.KeyValue);
  208. foreach (var item in barItems.Children) {
  209. if (item.HotKey == x) {
  210. host.CloseMenu ();
  211. Run (item.Action);
  212. return true;
  213. }
  214. }
  215. }
  216. break;
  217. }
  218. return true;
  219. }
  220. public override bool MouseEvent(MouseEvent me)
  221. {
  222. if (me.Flags == MouseFlags.Button1Clicked || me.Flags == MouseFlags.Button1Released) {
  223. if (me.Y < 1)
  224. return true;
  225. var item = me.Y - 1;
  226. if (item >= barItems.Children.Length)
  227. return true;
  228. host.CloseMenu ();
  229. Run (barItems.Children [item].Action);
  230. return true;
  231. }
  232. if (me.Flags == MouseFlags.Button1Pressed) {
  233. if (me.Y < 1)
  234. return true;
  235. if (me.Y - 1 >= barItems.Children.Length)
  236. return true;
  237. current = me.Y - 1;
  238. SetNeedsDisplay ();
  239. return true;
  240. }
  241. return false;
  242. }
  243. }
  244. /// <summary>
  245. /// A menu bar for your application.
  246. /// </summary>
  247. public class MenuBar : View {
  248. /// <summary>
  249. /// The menus that were defined when the menubar was created. This can be updated if the menu is not currently visible.
  250. /// </summary>
  251. /// <value>The menu array.</value>
  252. public MenuBarItem [] Menus { get; set; }
  253. int selected;
  254. Action action;
  255. /// <summary>
  256. /// Initializes a new instance of the <see cref="T:Terminal.Gui.MenuBar"/> class with the specified set of toplevel menu items.
  257. /// </summary>
  258. /// <param name="menus">Individual menu items, if one of those contains a null, then a separator is drawn.</param>
  259. public MenuBar (MenuBarItem [] menus) : base ()
  260. {
  261. X = 0;
  262. Y = 0;
  263. Width = Dim.Fill ();
  264. Height = 1;
  265. Menus = menus;
  266. CanFocus = false;
  267. selected = -1;
  268. ColorScheme = Colors.Menu;
  269. }
  270. public override void Redraw (Rect region)
  271. {
  272. Move (0, 0);
  273. Driver.SetAttribute (Colors.Base.Focus);
  274. for (int i = 0; i < Frame.Width; i++)
  275. Driver.AddRune (' ');
  276. Move (1, 0);
  277. int pos = 1;
  278. for (int i = 0; i < Menus.Length; i++) {
  279. var menu = Menus [i];
  280. Move (pos, 0);
  281. Attribute hotColor, normalColor;
  282. if (i == selected){
  283. hotColor = i == selected ? ColorScheme.HotFocus : ColorScheme.HotNormal;
  284. normalColor = i == selected ? ColorScheme.Focus : ColorScheme.Normal;
  285. } else {
  286. hotColor = Colors.Base.Focus;
  287. normalColor = Colors.Base.Focus;
  288. }
  289. DrawHotString (" " + menu.Title + " " + " ", hotColor, normalColor);
  290. pos += menu.TitleLength+ 3;
  291. }
  292. PositionCursor ();
  293. }
  294. public override void PositionCursor ()
  295. {
  296. int pos = 0;
  297. for (int i = 0; i < Menus.Length; i++) {
  298. if (i == selected) {
  299. pos++;
  300. Move (pos, 0);
  301. return;
  302. } else {
  303. pos += Menus [i].TitleLength + 4;
  304. }
  305. }
  306. Move (0, 0);
  307. }
  308. void Selected (MenuItem item)
  309. {
  310. // TODO: Running = false;
  311. action = item.Action;
  312. }
  313. public event EventHandler OnOpenMenu;
  314. Menu openMenu;
  315. View previousFocused;
  316. void OpenMenu (int index)
  317. {
  318. OnOpenMenu?.Invoke(this, null);
  319. if (openMenu != null)
  320. SuperView.Remove (openMenu);
  321. int pos = 0;
  322. for (int i = 0; i < index; i++)
  323. pos += Menus [i].Title.Length + 3;
  324. openMenu = new Menu (this, pos, 1, Menus [index]);
  325. SuperView.Add (openMenu);
  326. SuperView.SetFocus (openMenu);
  327. }
  328. // Starts the menu from a hotkey
  329. void StartMenu ()
  330. {
  331. if (openMenu != null)
  332. return;
  333. selected = 0;
  334. SetNeedsDisplay ();
  335. previousFocused = SuperView.Focused;
  336. OpenMenu (selected);
  337. }
  338. // Activates the menu, handles either first focus, or activating an entry when it was already active
  339. // For mouse events.
  340. void Activate (int idx)
  341. {
  342. selected = idx;
  343. if (openMenu == null)
  344. previousFocused = SuperView.Focused;
  345. OpenMenu (idx);
  346. SetNeedsDisplay ();
  347. }
  348. internal void CloseMenu ()
  349. {
  350. selected = -1;
  351. SetNeedsDisplay ();
  352. SuperView.Remove (openMenu);
  353. previousFocused?.SuperView?.SetFocus (previousFocused);
  354. openMenu = null;
  355. }
  356. internal void PreviousMenu ()
  357. {
  358. if (selected <= 0)
  359. selected = Menus.Length - 1;
  360. else
  361. selected--;
  362. OpenMenu (selected);
  363. }
  364. internal void NextMenu ()
  365. {
  366. if (selected == -1)
  367. selected = 0;
  368. else if (selected + 1 == Menus.Length)
  369. selected = 0;
  370. else
  371. selected++;
  372. OpenMenu (selected);
  373. }
  374. internal bool FindAndOpenMenuByHotkey(KeyEvent kb)
  375. {
  376. int pos = 0;
  377. var c = ((uint)kb.Key & (uint)Key.CharMask);
  378. for (int i = 0; i < Menus.Length; i++)
  379. {
  380. // TODO: this code is duplicated, hotkey should be part of the MenuBarItem
  381. var mi = Menus[i];
  382. int p = mi.Title.IndexOf('_');
  383. if (p != -1 && p + 1 < mi.Title.Length) {
  384. if (mi.Title[p + 1] == c) {
  385. OpenMenu(i);
  386. return true;
  387. }
  388. }
  389. }
  390. return false;
  391. }
  392. public override bool ProcessHotKey (KeyEvent kb)
  393. {
  394. if (kb.Key == Key.F9) {
  395. StartMenu ();
  396. return true;
  397. }
  398. if (kb.IsAlt)
  399. {
  400. if (FindAndOpenMenuByHotkey(kb)) return true;
  401. }
  402. var kc = kb.KeyValue;
  403. return base.ProcessHotKey (kb);
  404. }
  405. public override bool ProcessKey (KeyEvent kb)
  406. {
  407. switch (kb.Key) {
  408. case Key.CursorLeft:
  409. selected--;
  410. if (selected < 0)
  411. selected = Menus.Length - 1;
  412. break;
  413. case Key.CursorRight:
  414. selected = (selected + 1) % Menus.Length;
  415. break;
  416. case Key.Esc:
  417. case Key.ControlC:
  418. //TODO: Running = false;
  419. break;
  420. default:
  421. var key = kb.KeyValue;
  422. if ((key >= 'a' && key <= 'z') || (key >= 'A' && key <= 'Z') || (key >= '0' && key <= '9')) {
  423. char c = Char.ToUpper ((char)key);
  424. if (Menus [selected].Children == null)
  425. return false;
  426. foreach (var mi in Menus [selected].Children) {
  427. int p = mi.Title.IndexOf ('_');
  428. if (p != -1 && p + 1 < mi.Title.Length) {
  429. if (mi.Title [p + 1] == c) {
  430. Selected (mi);
  431. return true;
  432. }
  433. }
  434. }
  435. }
  436. return false;
  437. }
  438. SetNeedsDisplay ();
  439. return true;
  440. }
  441. public override bool MouseEvent(MouseEvent me)
  442. {
  443. if (me.Flags == MouseFlags.Button1Clicked) {
  444. int pos = 1;
  445. int cx = me.X;
  446. for (int i = 0; i < Menus.Length; i++) {
  447. if (cx > pos && me.X < pos + 1 + Menus [i].TitleLength) {
  448. Activate (i);
  449. return true;
  450. }
  451. pos += 2 + Menus [i].TitleLength + 1;
  452. }
  453. }
  454. return false;
  455. }
  456. }
  457. }