Menu.cs 32 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199
  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. using System.Collections.Generic;
  14. using System.Reflection;
  15. namespace Terminal.Gui {
  16. /// <summary>
  17. /// A <see cref="MenuItem"/> has a title, an associated help text, and an action to execute on activation.
  18. /// </summary>
  19. public class MenuItem {
  20. /// <summary>
  21. /// Initializes a new instance of <see cref="MenuItem"/>
  22. /// </summary>
  23. public MenuItem ()
  24. {
  25. Title = "";
  26. Help = "";
  27. }
  28. /// <summary>
  29. /// Initializes a new instance of <see cref="MenuItem"/>.
  30. /// </summary>
  31. /// <param name="title">Title for the menu item.</param>
  32. /// <param name="help">Help text to display.</param>
  33. /// <param name="action">Action to invoke when the menu item is activated.</param>
  34. /// <param name="canExecute">Function to determine if the action can currently be executred.</param>
  35. public MenuItem (ustring title, string help, Action action, Func<bool> canExecute = null)
  36. {
  37. Title = title ?? "";
  38. Help = help ?? "";
  39. Action = action;
  40. CanExecute = canExecute;
  41. bool nextIsHot = false;
  42. foreach (var x in Title) {
  43. if (x == '_')
  44. nextIsHot = true;
  45. else {
  46. if (nextIsHot) {
  47. HotKey = Char.ToUpper ((char)x);
  48. break;
  49. }
  50. nextIsHot = false;
  51. }
  52. }
  53. }
  54. /// <summary>
  55. /// Initializes a new instance of <see cref="MenuItem"/>
  56. /// </summary>
  57. /// <param name="title">Title for the menu item.</param>
  58. /// <param name="subMenu">The menu sub-menu.</param>
  59. public MenuItem (ustring title, MenuBarItem subMenu) : this (title, "", null)
  60. {
  61. SubMenu = subMenu;
  62. IsFromSubMenu = true;
  63. }
  64. /// <summary>
  65. /// The HotKey is used when the menu is active, the shortcut can be triggered when the menu is not active.
  66. /// For example HotKey would be "N" when the File Menu is open (assuming there is a "_New" entry
  67. /// if the ShortCut is set to "Control-N", this would be a global hotkey that would trigger as well
  68. /// </summary>
  69. public Rune HotKey;
  70. /// <summary>
  71. /// This is the global setting that can be used as a global shortcut to invoke the action on the menu.
  72. /// </summary>
  73. public Key ShortCut;
  74. /// <summary>
  75. /// Gets or sets the title.
  76. /// </summary>
  77. /// <value>The title.</value>
  78. public ustring Title { get; set; }
  79. /// <summary>
  80. /// Gets or sets the help text for the menu item.
  81. /// </summary>
  82. /// <value>The help text.</value>
  83. public ustring Help { get; set; }
  84. /// <summary>
  85. /// Gets or sets the action to be invoked when the menu is triggered
  86. /// </summary>
  87. /// <value>Method to invoke.</value>
  88. public Action Action { get; set; }
  89. /// <summary>
  90. /// Gets or sets the action to be invoked if the menu can be triggered
  91. /// </summary>
  92. /// <value>Function to determine if action is ready to be executed.</value>
  93. public Func<bool> CanExecute { get; set; }
  94. /// <summary>
  95. /// Shortcut to check if the menu item is enabled
  96. /// </summary>
  97. public bool IsEnabled ()
  98. {
  99. return CanExecute == null ? true : CanExecute ();
  100. }
  101. internal int Width => Title.Length + Help.Length + 1 + 2;
  102. /// <summary>
  103. /// Gets or sets the parent for this <see cref="MenuItem"/>
  104. /// </summary>
  105. /// <value>The parent.</value>
  106. internal MenuBarItem SubMenu { get; set; }
  107. internal bool IsFromSubMenu { get; set; }
  108. /// <summary>
  109. /// Merely a debugging aid to see the interaction with main
  110. /// </summary>
  111. public MenuItem GetMenuItem ()
  112. {
  113. return this;
  114. }
  115. /// <summary>
  116. /// Merely a debugging aid to see the interaction with main
  117. /// </summary>
  118. public bool GetMenuBarItem ()
  119. {
  120. return IsFromSubMenu;
  121. }
  122. }
  123. /// <summary>
  124. /// A <see cref="MenuBarItem"/> contains <see cref="MenuBarItem"/>s or <see cref="MenuItem"/>s.
  125. /// </summary>
  126. public class MenuBarItem : MenuItem {
  127. /// <summary>
  128. /// Initializes a new <see cref="MenuBarItem"/> as a <see cref="MenuItem"/>.
  129. /// </summary>
  130. /// <param name="title">Title for the menu item.</param>
  131. /// <param name="help">Help text to display.</param>
  132. /// <param name="action">Action to invoke when the menu item is activated.</param>
  133. /// <param name="canExecute">Function to determine if the action can currently be executred.</param>
  134. public MenuBarItem (ustring title, string help, Action action, Func<bool> canExecute = null) : base (title, help, action, canExecute)
  135. {
  136. SetTitle (title ?? "");
  137. Children = null;
  138. }
  139. /// <summary>
  140. /// Initializes a new <see cref="MenuBarItem"/>.
  141. /// </summary>
  142. /// <param name="title">Title for the menu item.</param>
  143. /// <param name="children">The items in the current menu.</param>
  144. public MenuBarItem (ustring title, MenuItem [] children)
  145. {
  146. SetTitle (title ?? "");
  147. Children = children;
  148. }
  149. /// <summary>
  150. /// Initializes a new <see cref="MenuBarItem"/>.
  151. /// </summary>
  152. /// <param name="children">The items in the current menu.</param>
  153. public MenuBarItem (MenuItem [] children) : this (new string (' ', GetMaxTitleLength (children)), children)
  154. {
  155. }
  156. static int GetMaxTitleLength (MenuItem [] children)
  157. {
  158. int maxLength = 0;
  159. foreach (var item in children) {
  160. int len = GetMenuBarItemLength (item.Title);
  161. if (len > maxLength)
  162. maxLength = len;
  163. item.IsFromSubMenu = true;
  164. }
  165. return maxLength;
  166. }
  167. void SetTitle (ustring title)
  168. {
  169. if (title == null)
  170. title = "";
  171. Title = title;
  172. TitleLength = GetMenuBarItemLength (Title);
  173. }
  174. static int GetMenuBarItemLength (ustring title)
  175. {
  176. int len = 0;
  177. foreach (var ch in title) {
  178. if (ch == '_')
  179. continue;
  180. len++;
  181. }
  182. return len;
  183. }
  184. ///// <summary>
  185. ///// Gets or sets the title to display.
  186. ///// </summary>
  187. ///// <value>The title.</value>
  188. //public ustring Title { get; set; }
  189. /// <summary>
  190. /// Gets or sets an array of <see cref="MenuItem"/> objects that are the children of this <see cref="MenuBarItem"/>
  191. /// </summary>
  192. /// <value>The children.</value>
  193. public MenuItem [] Children { get; set; }
  194. internal int TitleLength { get; private set; }
  195. internal bool IsTopLevel { get => (Children == null || Children.Length == 0); }
  196. }
  197. class Menu : View {
  198. internal MenuBarItem barItems;
  199. MenuBar host;
  200. internal int current;
  201. internal View previousSubFocused;
  202. static Rect MakeFrame (int x, int y, MenuItem [] items)
  203. {
  204. if (items == null || items.Length == 0) {
  205. return new Rect ();
  206. }
  207. int maxW = items.Max (z => z?.Width) ?? 0;
  208. return new Rect (x, y, maxW + 2, items.Length + 2);
  209. }
  210. public Menu (MenuBar host, int x, int y, MenuBarItem barItems) : base (MakeFrame (x, y, barItems.Children))
  211. {
  212. this.barItems = barItems;
  213. this.host = host;
  214. if (barItems.IsTopLevel) {
  215. // This is a standalone MenuItem on a MenuBar
  216. ColorScheme = Colors.Menu;
  217. CanFocus = true;
  218. } else {
  219. current = -1;
  220. for (int i = 0; i < barItems.Children.Length; i++) {
  221. if (barItems.Children [i] != null) {
  222. current = i;
  223. break;
  224. }
  225. }
  226. ColorScheme = Colors.Menu;
  227. CanFocus = true;
  228. WantMousePositionReports = host.WantMousePositionReports;
  229. }
  230. }
  231. internal Attribute DetermineColorSchemeFor (MenuItem item, int index)
  232. {
  233. if (item != null) {
  234. if (index == current) return ColorScheme.Focus;
  235. if (!item.IsEnabled ()) return ColorScheme.Disabled;
  236. }
  237. return ColorScheme.Normal;
  238. }
  239. public override void Redraw (Rect region)
  240. {
  241. Driver.SetAttribute (ColorScheme.Normal);
  242. DrawFrame (region, padding: 0, fill: true);
  243. for (int i = 0; i < barItems.Children.Length; i++) {
  244. var item = barItems.Children [i];
  245. Driver.SetAttribute (item == null ? ColorScheme.Normal : i == current ? ColorScheme.Focus : ColorScheme.Normal);
  246. if (item == null) {
  247. Move (0, i + 1);
  248. Driver.AddRune (Driver.LeftTee);
  249. } else
  250. Move (1, i + 1);
  251. Driver.SetAttribute (DetermineColorSchemeFor (item, i));
  252. for (int p = 0; p < Frame.Width - 2; p++)
  253. if (item == null)
  254. Driver.AddRune (Driver.HLine);
  255. else if (p == Frame.Width - 3 && barItems.Children [i].SubMenu != null)
  256. Driver.AddRune ('>');
  257. else
  258. Driver.AddRune (' ');
  259. if (item == null) {
  260. Move (Frame.Right - 1, i + 1);
  261. Driver.AddRune (Driver.RightTee);
  262. continue;
  263. }
  264. Move (2, i + 1);
  265. if (!item.IsEnabled ())
  266. DrawHotString (item.Title, ColorScheme.Disabled, ColorScheme.Disabled);
  267. else
  268. DrawHotString (item.Title,
  269. i == current ? ColorScheme.HotFocus : ColorScheme.HotNormal,
  270. i == current ? ColorScheme.Focus : ColorScheme.Normal);
  271. // The help string
  272. var l = item.Help.Length;
  273. Move (Frame.Width - l - 2, 1 + i);
  274. Driver.AddStr (item.Help);
  275. }
  276. PositionCursor ();
  277. }
  278. public override void PositionCursor ()
  279. {
  280. if (host == null || !host.isMenuClosed)
  281. if (barItems.IsTopLevel) {
  282. host.PositionCursor ();
  283. } else
  284. Move (2, 1 + current);
  285. else
  286. host.PositionCursor ();
  287. }
  288. public void Run (Action action)
  289. {
  290. if (action == null)
  291. return;
  292. Application.UngrabMouse ();
  293. host.CloseAllMenus ();
  294. Application.Refresh ();
  295. Application.MainLoop.AddIdle (() => {
  296. action ();
  297. return false;
  298. });
  299. }
  300. public override bool OnKeyDown (KeyEvent keyEvent)
  301. {
  302. if (keyEvent.IsAlt) {
  303. host.CloseAllMenus ();
  304. return true;
  305. }
  306. return false;
  307. }
  308. public override bool ProcessHotKey (KeyEvent keyEvent)
  309. {
  310. // To ncurses simulate a AltMask key pressing Alt+Space because
  311. // it can�t detect an alone special key down was pressed.
  312. if (keyEvent.IsAlt && keyEvent.Key == Key.AltMask) {
  313. OnKeyDown (keyEvent);
  314. return true;
  315. }
  316. return false;
  317. }
  318. public override bool ProcessKey (KeyEvent kb)
  319. {
  320. bool disabled;
  321. switch (kb.Key) {
  322. case Key.CursorUp:
  323. if (barItems.IsTopLevel || current == -1)
  324. break;
  325. do {
  326. disabled = false;
  327. current--;
  328. if (host.UseKeysUpDownAsKeysLeftRight) {
  329. if (current == -1 && barItems.Children [current + 1].IsFromSubMenu && host.selectedSub > -1) {
  330. current++;
  331. host.PreviousMenu (true);
  332. break;
  333. }
  334. }
  335. if (current < 0)
  336. current = barItems.Children.Length - 1;
  337. var item = barItems.Children [current];
  338. if (item == null || !item.IsEnabled ()) disabled = true;
  339. } while (barItems.Children [current] == null || disabled);
  340. SetNeedsDisplay ();
  341. break;
  342. case Key.CursorDown:
  343. if (barItems.IsTopLevel) {
  344. break;
  345. }
  346. do {
  347. current++;
  348. disabled = false;
  349. if (current == barItems.Children.Length)
  350. current = 0;
  351. var item = barItems.Children [current];
  352. if (item == null || !item.IsEnabled ()) disabled = true;
  353. if (host.UseKeysUpDownAsKeysLeftRight && barItems.Children [current]?.SubMenu != null &&
  354. !disabled && !host.isMenuClosed) {
  355. CheckSubMenu ();
  356. break;
  357. }
  358. if (host.isMenuClosed)
  359. host.OpenMenu (host.selected);
  360. } while (barItems.Children [current] == null || disabled);
  361. SetNeedsDisplay ();
  362. break;
  363. case Key.CursorLeft:
  364. host.PreviousMenu (true);
  365. break;
  366. case Key.CursorRight:
  367. host.NextMenu (barItems.IsTopLevel || barItems.Children [current].IsFromSubMenu ? true : false);
  368. break;
  369. case Key.Esc:
  370. Application.UngrabMouse ();
  371. host.CloseAllMenus ();
  372. break;
  373. case Key.Enter:
  374. if (barItems.IsTopLevel) {
  375. Run (barItems.Action);
  376. } else {
  377. CheckSubMenu ();
  378. Run (barItems.Children [current].Action);
  379. }
  380. break;
  381. default:
  382. // TODO: rune-ify
  383. if (barItems.Children != null && Char.IsLetterOrDigit ((char)kb.KeyValue)) {
  384. var x = Char.ToUpper ((char)kb.KeyValue);
  385. foreach (var item in barItems.Children) {
  386. if (item == null) continue;
  387. if (item.IsEnabled () && item.HotKey == x) {
  388. host.CloseMenu ();
  389. Run (item.Action);
  390. return true;
  391. }
  392. }
  393. }
  394. break;
  395. }
  396. return true;
  397. }
  398. public override bool MouseEvent (MouseEvent me)
  399. {
  400. if (!host.handled && !host.HandleGrabView (me, this)) {
  401. return false;
  402. }
  403. host.handled = false;
  404. bool disabled;
  405. if (me.Flags == MouseFlags.Button1Clicked) {
  406. disabled = false;
  407. if (me.Y < 1)
  408. return true;
  409. var meY = me.Y - 1;
  410. if (meY >= barItems.Children.Length)
  411. return true;
  412. var item = barItems.Children [meY];
  413. if (item == null || !item.IsEnabled ()) disabled = true;
  414. if (item != null && !disabled)
  415. Run (barItems.Children [meY].Action);
  416. return true;
  417. } else if (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked ||
  418. me.Flags == MouseFlags.ReportMousePosition ||
  419. me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition)) {
  420. disabled = false;
  421. if (me.Y < 1)
  422. return true;
  423. if (me.Y - 1 >= barItems.Children.Length)
  424. return true;
  425. var item = barItems.Children [me.Y - 1];
  426. if (item == null || !item.IsEnabled ()) disabled = true;
  427. if (item != null && !disabled)
  428. current = me.Y - 1;
  429. HasFocus = true;
  430. SetNeedsDisplay ();
  431. CheckSubMenu ();
  432. return true;
  433. }
  434. return false;
  435. }
  436. internal void CheckSubMenu ()
  437. {
  438. if (barItems.Children [current] == null)
  439. return;
  440. var subMenu = barItems.Children [current].SubMenu;
  441. if (subMenu != null) {
  442. int pos = -1;
  443. if (host.openSubMenu != null)
  444. pos = host.openSubMenu.FindIndex (o => o?.barItems == subMenu);
  445. host.Activate (host.selected, pos, subMenu);
  446. } else if (host.openSubMenu != null && !barItems.Children [current].IsFromSubMenu)
  447. host.CloseMenu (false, true);
  448. }
  449. int GetSubMenuIndex (MenuBarItem subMenu)
  450. {
  451. int pos = -1;
  452. if (this != null && Subviews.Count > 0) {
  453. Menu v = null;
  454. foreach (var menu in Subviews) {
  455. if (((Menu)menu).barItems == subMenu)
  456. v = (Menu)menu;
  457. }
  458. if (v != null)
  459. pos = Subviews.IndexOf (v);
  460. }
  461. return pos;
  462. }
  463. }
  464. /// <summary>
  465. /// The MenuBar provides a menu for Terminal.Gui applications.
  466. /// </summary>
  467. /// <remarks>
  468. /// <para>
  469. /// The <see cref="MenuBar"/> appears on the first row of the terminal.
  470. /// </para>
  471. /// <para>
  472. /// The <see cref="MenuBar"/> provides global hotkeys for the application.
  473. /// </para>
  474. /// </remarks>
  475. public class MenuBar : View {
  476. /// <summary>
  477. /// Gets or sets the array of <see cref="MenuBarItem"/>s for the menu. Only set this when the <see cref="MenuBar"/> is vislble.
  478. /// </summary>
  479. /// <value>The menu array.</value>
  480. public MenuBarItem [] Menus { get; set; }
  481. internal int selected;
  482. internal int selectedSub;
  483. Action action;
  484. /// <summary>
  485. /// Used for change the navigation key style.
  486. /// </summary>
  487. public bool UseKeysUpDownAsKeysLeftRight { get; set; } = true;
  488. /// <summary>
  489. /// Initializes a new instance of the <see cref="MenuBar"/> class with the specified set of toplevel menu items.
  490. /// </summary>
  491. /// <param name="menus">Individual menu items; a null item will result in a separator being drawn.</param>
  492. public MenuBar (MenuBarItem [] menus) : base ()
  493. {
  494. X = 0;
  495. Y = 0;
  496. Width = Dim.Fill ();
  497. Height = 1;
  498. Menus = menus;
  499. //CanFocus = true;
  500. selected = -1;
  501. selectedSub = -1;
  502. ColorScheme = Colors.Menu;
  503. WantMousePositionReports = true;
  504. isMenuClosed = true;
  505. }
  506. bool openedByAltKey;
  507. ///<inheritdoc cref="OnKeyDown"/>
  508. public override bool OnKeyDown (KeyEvent keyEvent)
  509. {
  510. if (keyEvent.IsAlt) {
  511. openedByAltKey = true;
  512. SetNeedsDisplay ();
  513. openedByHotKey = false;
  514. }
  515. return false;
  516. }
  517. ///<inheritdoc cref="OnKeyUp"/>
  518. public override bool OnKeyUp (KeyEvent keyEvent)
  519. {
  520. if (keyEvent.IsAlt) {
  521. // User pressed Alt - this may be a precursor to a menu accelerator (e.g. Alt-F)
  522. if (!keyEvent.IsCtrl && openedByAltKey && isMenuClosed && openMenu == null && ((uint)keyEvent.Key & (uint)Key.CharMask) == 0) {
  523. // There's no open menu, the first menu item should be highlight.
  524. // The right way to do this is to SetFocus(MenuBar), but for some reason
  525. // that faults.
  526. //Activate (0);
  527. //StartMenu ();
  528. isMenuClosed = false;
  529. selected = 0;
  530. CanFocus = true;
  531. lastFocused = SuperView.MostFocused;
  532. SuperView.SetFocus (this);
  533. SetNeedsDisplay ();
  534. Application.GrabMouse (this);
  535. } else if (!openedByHotKey) {
  536. // There's an open menu. If this Alt key-up is a pre-cursor to an accelerator
  537. // we don't want to close the menu because it'll flash.
  538. // How to deal with that?
  539. if (openMenu != null)
  540. CloseAllMenus ();
  541. openedByAltKey = false;
  542. isMenuClosed = true;
  543. selected = -1;
  544. CanFocus = false;
  545. if (lastFocused != null)
  546. SuperView?.SetFocus (lastFocused);
  547. SetNeedsDisplay ();
  548. Application.UngrabMouse ();
  549. }
  550. return true;
  551. }
  552. return false;
  553. }
  554. ///<inheritdoc cref="Redraw"/>
  555. public override void Redraw (Rect region)
  556. {
  557. Move (0, 0);
  558. Driver.SetAttribute (Colors.Menu.Normal);
  559. for (int i = 0; i < Frame.Width; i++)
  560. Driver.AddRune (' ');
  561. Move (1, 0);
  562. int pos = 1;
  563. for (int i = 0; i < Menus.Length; i++) {
  564. var menu = Menus [i];
  565. Move (pos, 0);
  566. Attribute hotColor, normalColor;
  567. if (i == selected) {
  568. hotColor = i == selected ? ColorScheme.HotFocus : ColorScheme.HotNormal;
  569. normalColor = i == selected ? ColorScheme.Focus : ColorScheme.Normal;
  570. } else if (openedByAltKey) {
  571. hotColor = ColorScheme.HotNormal;
  572. normalColor = ColorScheme.Normal;
  573. } else {
  574. hotColor = ColorScheme.Normal;
  575. normalColor = ColorScheme.Normal;
  576. }
  577. DrawHotString ($" {menu.Title} ", hotColor, normalColor);
  578. pos += 1 + menu.TitleLength + 2;
  579. }
  580. PositionCursor ();
  581. }
  582. ///<inheritdoc cref="PositionCursor"/>
  583. public override void PositionCursor ()
  584. {
  585. int pos = 0;
  586. for (int i = 0; i < Menus.Length; i++) {
  587. if (i == selected) {
  588. pos++;
  589. if (!isMenuClosed)
  590. Move (pos + 1, 0);
  591. else
  592. Move (pos + 1, 0);
  593. return;
  594. } else {
  595. if (!isMenuClosed)
  596. pos += 1 + Menus [i].TitleLength + 2;
  597. else
  598. pos += 2 + Menus [i].TitleLength + 1;
  599. }
  600. }
  601. //Move (0, 0);
  602. }
  603. void Selected (MenuItem item)
  604. {
  605. // TODO: Running = false;
  606. action = item.Action;
  607. }
  608. /// <summary>
  609. /// Raised as a menu is opened.
  610. /// </summary>
  611. public event EventHandler OnOpenMenu;
  612. /// <summary>
  613. /// Raised when a menu is closing.
  614. /// </summary>
  615. public event EventHandler OnCloseMenu;
  616. internal Menu openMenu;
  617. Menu openCurrentMenu;
  618. internal List<Menu> openSubMenu;
  619. View previousFocused;
  620. internal bool isMenuOpening;
  621. internal bool isMenuClosing;
  622. internal bool isMenuClosed;
  623. /// <summary>
  624. /// True of the menu is open; otherwise false.
  625. /// </summary>
  626. public bool MenuOpen { get; set; }
  627. View lastFocused;
  628. /// <summary>
  629. /// Get the lasted focused view before open the menu.
  630. /// </summary>
  631. public View LastFocused { get; private set; }
  632. internal void OpenMenu (int index, int sIndex = -1, MenuBarItem subMenu = null)
  633. {
  634. isMenuOpening = true;
  635. OnOpenMenu?.Invoke (this, null);
  636. int pos = 0;
  637. switch (subMenu) {
  638. case null:
  639. lastFocused = lastFocused ?? SuperView.MostFocused;
  640. if (openSubMenu != null)
  641. CloseMenu (false, true);
  642. if (openMenu != null)
  643. SuperView.Remove (openMenu);
  644. for (int i = 0; i < index; i++)
  645. pos += Menus [i].Title.Length + 2;
  646. openMenu = new Menu (this, pos, 1, Menus [index]);
  647. openCurrentMenu = openMenu;
  648. openCurrentMenu.previousSubFocused = openMenu;
  649. SuperView.Add (openMenu);
  650. SuperView.SetFocus (openMenu);
  651. break;
  652. default:
  653. if (openSubMenu == null)
  654. openSubMenu = new List<Menu> ();
  655. if (sIndex > -1) {
  656. RemoveSubMenu (sIndex);
  657. } else {
  658. var last = openSubMenu.Count > 0 ? openSubMenu.Last () : openMenu;
  659. openCurrentMenu = new Menu (this, last.Frame.Left + last.Frame.Width, last.Frame.Top + 1 + last.current, subMenu);
  660. openCurrentMenu.previousSubFocused = last.previousSubFocused;
  661. openSubMenu.Add (openCurrentMenu);
  662. SuperView.Add (openCurrentMenu);
  663. }
  664. selectedSub = openSubMenu.Count - 1;
  665. SuperView?.SetFocus (openCurrentMenu);
  666. break;
  667. }
  668. isMenuOpening = false;
  669. isMenuClosed = false;
  670. MenuOpen = true;
  671. }
  672. // Starts the menu from a hotkey
  673. void StartMenu ()
  674. {
  675. if (openMenu != null)
  676. return;
  677. selected = 0;
  678. SetNeedsDisplay ();
  679. previousFocused = SuperView.Focused;
  680. OpenMenu (selected);
  681. Application.GrabMouse (this);
  682. }
  683. // Activates the menu, handles either first focus, or activating an entry when it was already active
  684. // For mouse events.
  685. internal void Activate (int idx, int sIdx = -1, MenuBarItem subMenu = null)
  686. {
  687. selected = idx;
  688. selectedSub = sIdx;
  689. if (openMenu == null)
  690. previousFocused = SuperView.Focused;
  691. OpenMenu (idx, sIdx, subMenu);
  692. SetNeedsDisplay ();
  693. }
  694. internal void CloseMenu (bool reopen = false, bool isSubMenu = false)
  695. {
  696. isMenuClosing = true;
  697. OnCloseMenu?.Invoke (this, null);
  698. switch (isSubMenu) {
  699. case false:
  700. if (openMenu != null)
  701. SuperView.Remove (openMenu);
  702. SetNeedsDisplay ();
  703. if (previousFocused != null && openMenu != null && previousFocused.ToString () != openCurrentMenu.ToString ())
  704. previousFocused?.SuperView?.SetFocus (previousFocused);
  705. openMenu = null;
  706. if (lastFocused is Menu) {
  707. lastFocused = null;
  708. }
  709. LastFocused = lastFocused;
  710. lastFocused = null;
  711. if (LastFocused != null) {
  712. if (!reopen)
  713. selected = -1;
  714. LastFocused.SuperView?.SetFocus (LastFocused);
  715. } else {
  716. SuperView.SetFocus (this);
  717. isMenuClosed = true;
  718. PositionCursor ();
  719. }
  720. isMenuClosed = true;
  721. break;
  722. case true:
  723. selectedSub = -1;
  724. SetNeedsDisplay ();
  725. RemoveAllOpensSubMenus ();
  726. openCurrentMenu.previousSubFocused?.SuperView?.SetFocus (openCurrentMenu.previousSubFocused);
  727. openSubMenu = null;
  728. break;
  729. }
  730. isMenuClosing = false;
  731. MenuOpen = false;
  732. }
  733. void RemoveSubMenu (int index)
  734. {
  735. if (openSubMenu == null)
  736. return;
  737. for (int i = openSubMenu.Count - 1; i > index; i--) {
  738. isMenuClosing = true;
  739. if (openSubMenu.Count - 1 > 0)
  740. SuperView.SetFocus (openSubMenu [i - 1]);
  741. else
  742. SuperView.SetFocus (openMenu);
  743. if (openSubMenu != null) {
  744. SuperView.Remove (openSubMenu [i]);
  745. openSubMenu.Remove (openSubMenu [i]);
  746. }
  747. RemoveSubMenu (i);
  748. }
  749. if (openSubMenu.Count > 0)
  750. openCurrentMenu = openSubMenu.Last ();
  751. //if (openMenu.Subviews.Count == 0)
  752. // return;
  753. //if (index == 0) {
  754. // //SuperView.SetFocus (previousSubFocused);
  755. // FocusPrev ();
  756. // return;
  757. //}
  758. //for (int i = openMenu.Subviews.Count - 1; i > index; i--) {
  759. // isMenuClosing = true;
  760. // if (openMenu.Subviews.Count - 1 > 0)
  761. // SuperView.SetFocus (openMenu.Subviews [i - 1]);
  762. // else
  763. // SuperView.SetFocus (openMenu);
  764. // if (openMenu != null) {
  765. // Remove (openMenu.Subviews [i]);
  766. // openMenu.Remove (openMenu.Subviews [i]);
  767. // }
  768. // RemoveSubMenu (i);
  769. //}
  770. isMenuClosing = false;
  771. }
  772. internal void RemoveAllOpensSubMenus ()
  773. {
  774. if (openSubMenu != null) {
  775. foreach (var item in openSubMenu) {
  776. SuperView.Remove (item);
  777. }
  778. }
  779. }
  780. internal void CloseAllMenus ()
  781. {
  782. if (!isMenuOpening && !isMenuClosing) {
  783. if (openSubMenu != null)
  784. CloseMenu (false, true);
  785. CloseMenu ();
  786. if (LastFocused != null && LastFocused != this)
  787. selected = -1;
  788. }
  789. isMenuClosed = true;
  790. openedByHotKey = false;
  791. openedByAltKey = false;
  792. }
  793. View FindDeepestMenu (View view, ref int count)
  794. {
  795. count = count > 0 ? count : 0;
  796. foreach (var menu in view.Subviews) {
  797. if (menu is Menu) {
  798. count++;
  799. return FindDeepestMenu ((Menu)menu, ref count);
  800. }
  801. }
  802. return view;
  803. }
  804. internal void PreviousMenu (bool isSubMenu = false)
  805. {
  806. switch (isSubMenu) {
  807. case false:
  808. if (selected <= 0)
  809. selected = Menus.Length - 1;
  810. else
  811. selected--;
  812. if (selected > -1)
  813. CloseMenu (true, false);
  814. OpenMenu (selected);
  815. break;
  816. case true:
  817. if (selectedSub > -1) {
  818. selectedSub--;
  819. RemoveSubMenu (selectedSub);
  820. SetNeedsDisplay ();
  821. } else
  822. PreviousMenu ();
  823. break;
  824. }
  825. }
  826. internal void NextMenu (bool isSubMenu = false)
  827. {
  828. switch (isSubMenu) {
  829. case false:
  830. if (selected == -1)
  831. selected = 0;
  832. else if (selected + 1 == Menus.Length)
  833. selected = 0;
  834. else
  835. selected++;
  836. if (selected > -1)
  837. CloseMenu (true);
  838. OpenMenu (selected);
  839. break;
  840. case true:
  841. if (UseKeysUpDownAsKeysLeftRight) {
  842. CloseMenu (false, true);
  843. NextMenu ();
  844. } else {
  845. if ((selectedSub == -1 || openSubMenu == null || openSubMenu?.Count == selectedSub) && openCurrentMenu.barItems.Children [openCurrentMenu.current].SubMenu == null) {
  846. if (openSubMenu != null)
  847. CloseMenu (false, true);
  848. NextMenu ();
  849. } else if (openCurrentMenu.barItems.Children [openCurrentMenu.current].SubMenu != null ||
  850. !openCurrentMenu.barItems.Children [openCurrentMenu.current].IsFromSubMenu)
  851. selectedSub++;
  852. else
  853. return;
  854. SetNeedsDisplay ();
  855. openCurrentMenu.CheckSubMenu ();
  856. }
  857. break;
  858. }
  859. }
  860. bool openedByHotKey;
  861. internal bool FindAndOpenMenuByHotkey (KeyEvent kb)
  862. {
  863. //int pos = 0;
  864. var c = ((uint)kb.Key & (uint)Key.CharMask);
  865. for (int i = 0; i < Menus.Length; i++) {
  866. // TODO: this code is duplicated, hotkey should be part of the MenuBarItem
  867. var mi = Menus [i];
  868. int p = mi.Title.IndexOf ('_');
  869. if (p != -1 && p + 1 < mi.Title.Length) {
  870. if (Char.ToUpperInvariant ((char)mi.Title [p + 1]) == c) {
  871. ProcessMenu (i, mi);
  872. return true;
  873. }
  874. }
  875. }
  876. return false;
  877. }
  878. private void ProcessMenu (int i, MenuBarItem mi)
  879. {
  880. if (mi.IsTopLevel) {
  881. var menu = new Menu (this, i, 0, mi);
  882. menu.Run (mi.Action);
  883. } else {
  884. openedByHotKey = true;
  885. Application.GrabMouse (this);
  886. selected = i;
  887. OpenMenu (i);
  888. }
  889. }
  890. ///<inheritdoc cref="ProcessHotKey"/>
  891. public override bool ProcessHotKey (KeyEvent kb)
  892. {
  893. if (kb.Key == Key.F9) {
  894. if (isMenuClosed)
  895. StartMenu ();
  896. else
  897. CloseAllMenus ();
  898. return true;
  899. }
  900. // To ncurses simulate a AltMask key pressing Alt+Space because
  901. // it can�t detect an alone special key down was pressed.
  902. if (kb.IsAlt && kb.Key == Key.AltMask && openMenu == null) {
  903. OnKeyDown (kb);
  904. OnKeyUp (kb);
  905. return true;
  906. } else if (kb.IsAlt) {
  907. if (FindAndOpenMenuByHotkey (kb)) return true;
  908. }
  909. //var kc = kb.KeyValue;
  910. return base.ProcessHotKey (kb);
  911. }
  912. ///<inheritdoc cref="ProcessKey"/>
  913. public override bool ProcessKey (KeyEvent kb)
  914. {
  915. switch (kb.Key) {
  916. case Key.CursorLeft:
  917. selected--;
  918. if (selected < 0)
  919. selected = Menus.Length - 1;
  920. break;
  921. case Key.CursorRight:
  922. selected = (selected + 1) % Menus.Length;
  923. break;
  924. case Key.Esc:
  925. case Key.ControlC:
  926. //TODO: Running = false;
  927. CloseMenu ();
  928. if (openedByAltKey) {
  929. openedByAltKey = false;
  930. LastFocused.SuperView?.SetFocus (LastFocused);
  931. }
  932. break;
  933. case Key.CursorDown:
  934. case Key.Enter:
  935. ProcessMenu (selected, Menus [selected]);
  936. break;
  937. default:
  938. var key = kb.KeyValue;
  939. if ((key >= 'a' && key <= 'z') || (key >= 'A' && key <= 'Z') || (key >= '0' && key <= '9')) {
  940. char c = Char.ToUpper ((char)key);
  941. if (selected == -1 || Menus [selected].IsTopLevel)
  942. return false;
  943. foreach (var mi in Menus [selected].Children) {
  944. if (mi == null)
  945. continue;
  946. int p = mi.Title.IndexOf ('_');
  947. if (p != -1 && p + 1 < mi.Title.Length) {
  948. if (mi.Title [p + 1] == c) {
  949. Selected (mi);
  950. return true;
  951. }
  952. }
  953. }
  954. }
  955. return false;
  956. }
  957. SetNeedsDisplay ();
  958. return true;
  959. }
  960. ///<inheritdoc cref="MouseEvent"/>
  961. public override bool MouseEvent (MouseEvent me)
  962. {
  963. if (!handled && !HandleGrabView (me, this)) {
  964. return false;
  965. }
  966. handled = false;
  967. if (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1Clicked || me.Flags == MouseFlags.Button1DoubleClicked ||
  968. (me.Flags == MouseFlags.ReportMousePosition && selected > -1) ||
  969. (me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition) && selected > -1)) {
  970. int pos = 1;
  971. int cx = me.X;
  972. for (int i = 0; i < Menus.Length; i++) {
  973. if (cx > pos && me.X < pos + 1 + Menus [i].TitleLength) {
  974. if (selected == i && (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked) &&
  975. !isMenuClosed) {
  976. Application.UngrabMouse ();
  977. if (Menus [i].IsTopLevel) {
  978. var menu = new Menu (this, i, 0, Menus [i]);
  979. menu.Run (Menus [i].Action);
  980. } else {
  981. CloseMenu ();
  982. }
  983. } else if ((me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked) &&
  984. isMenuClosed) {
  985. if (Menus [i].IsTopLevel) {
  986. var menu = new Menu (this, i, 0, Menus [i]);
  987. menu.Run (Menus [i].Action);
  988. } else {
  989. Activate (i);
  990. }
  991. } else if (selected != i && selected > -1 && me.Flags == MouseFlags.ReportMousePosition) {
  992. if (!isMenuClosed) {
  993. CloseMenu ();
  994. Activate (i);
  995. }
  996. } else {
  997. if (!isMenuClosed)
  998. Activate (i);
  999. }
  1000. return true;
  1001. }
  1002. pos += 2 + Menus [i].TitleLength + 1;
  1003. }
  1004. }
  1005. return false;
  1006. }
  1007. internal bool handled;
  1008. internal bool HandleGrabView (MouseEvent me, View current)
  1009. {
  1010. if (Application.mouseGrabView != null) {
  1011. if (me.View is MenuBar || me.View is Menu) {
  1012. if (me.View != current) {
  1013. Application.UngrabMouse ();
  1014. Application.GrabMouse (me.View);
  1015. me.View.MouseEvent (me);
  1016. }
  1017. } else if (!(me.View is MenuBar || me.View is Menu) && (me.Flags.HasFlag (MouseFlags.Button1Clicked) ||
  1018. me.Flags == MouseFlags.Button1DoubleClicked || me.Flags == MouseFlags.Button1Pressed)) {
  1019. Application.UngrabMouse ();
  1020. CloseAllMenus ();
  1021. handled = false;
  1022. return false;
  1023. } else {
  1024. handled = false;
  1025. return false;
  1026. }
  1027. } else if (isMenuClosed && (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked ||
  1028. me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition))) {
  1029. Application.GrabMouse (current);
  1030. } else {
  1031. handled = false;
  1032. return false;
  1033. }
  1034. //if (me.View != this && (me.Flags != MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked))
  1035. // return true;
  1036. //else if (me.View != this && (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked)) {
  1037. // Application.UngrabMouse ();
  1038. // host.CloseAllMenus ();
  1039. // return true;
  1040. //}
  1041. //if (!(me.View is MenuBar) && !(me.View is Menu) && (me.Flags != MouseFlags.Button1Pressed ||
  1042. // me.Flags != MouseFlags.Button1DoubleClicked))
  1043. // return false;
  1044. //if (Application.mouseGrabView != null) {
  1045. // if (me.View is MenuBar || me.View is Menu) {
  1046. // me.X -= me.OfX;
  1047. // me.Y -= me.OfY;
  1048. // me.View.MouseEvent (me);
  1049. // return true;
  1050. // } else if (!(me.View is MenuBar || me.View is Menu) && (me.Flags == MouseFlags.Button1Pressed ||
  1051. // me.Flags == MouseFlags.Button1DoubleClicked)) {
  1052. // Application.UngrabMouse ();
  1053. // CloseAllMenus ();
  1054. // }
  1055. //} else if (!isMenuClosed && selected == -1 && (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked)) {
  1056. // Application.GrabMouse (this);
  1057. // return true;
  1058. //}
  1059. //if (Application.mouseGrabView != null) {
  1060. // if (Application.mouseGrabView == me.View && me.View == current) {
  1061. // me.X -= me.OfX;
  1062. // me.Y -= me.OfY;
  1063. // } else if (me.View != current && me.View is MenuBar && me.View is Menu) {
  1064. // Application.UngrabMouse ();
  1065. // Application.GrabMouse (me.View);
  1066. // } else if (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked) {
  1067. // Application.UngrabMouse ();
  1068. // CloseMenu ();
  1069. // }
  1070. //} else if ((!isMenuClosed && selected > -1)) {
  1071. // Application.GrabMouse (current);
  1072. //}
  1073. handled = true;
  1074. return true;
  1075. }
  1076. }
  1077. }