Menu.cs 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734
  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. namespace Terminal.Gui {
  15. /// <summary>
  16. /// Specifies how a <see cref="MenuItem"/> shows selection state.
  17. /// </summary>
  18. [Flags]
  19. public enum MenuItemCheckStyle {
  20. /// <summary>
  21. /// The menu item will be shown normally, with no check indicator.
  22. /// </summary>
  23. NoCheck = 0b_0000_0000,
  24. /// <summary>
  25. /// The menu item will indicate checked/un-checked state (see <see cref="Checked"/>.
  26. /// </summary>
  27. Checked = 0b_0000_0001,
  28. /// <summary>
  29. /// The menu item is part of a menu radio group (see <see cref="Checked"/> and will indicate selected state.
  30. /// </summary>
  31. Radio = 0b_0000_0010,
  32. };
  33. /// <summary>
  34. /// A <see cref="MenuItem"/> has a title, an associated help text, and an action to execute on activation.
  35. /// </summary>
  36. public class MenuItem {
  37. ustring title;
  38. Key shortCut;
  39. /// <summary>
  40. /// Initializes a new instance of <see cref="MenuItem"/>
  41. /// </summary>
  42. public MenuItem (Key shortCut = Key.ControlSpace)
  43. {
  44. Title = "";
  45. Help = "";
  46. if (shortCut != Key.ControlSpace) {
  47. ShortCut = shortCut;
  48. }
  49. }
  50. /// <summary>
  51. /// Initializes a new instance of <see cref="MenuItem"/>.
  52. /// </summary>
  53. /// <param name="title">Title for the menu item.</param>
  54. /// <param name="help">Help text to display.</param>
  55. /// <param name="action">Action to invoke when the menu item is activated.</param>
  56. /// <param name="canExecute">Function to determine if the action can currently be executed.</param>
  57. /// <param name="parent">The <see cref="Parent"/> of this menu item.</param>
  58. /// <param name="shortCut">The <see cref="ShortCut"/> keystroke combination.</param>
  59. public MenuItem (ustring title, ustring help, Action action, Func<bool> canExecute = null, MenuItem parent = null, Key shortCut = Key.ControlSpace)
  60. {
  61. Title = title ?? "";
  62. Help = help ?? "";
  63. Action = action;
  64. CanExecute = canExecute;
  65. Parent = parent;
  66. if (shortCut != Key.ControlSpace) {
  67. ShortCut = shortCut;
  68. }
  69. }
  70. /// <summary>
  71. /// The HotKey is used when the menu is active, the shortcut can be triggered when the menu is not active.
  72. /// For example HotKey would be "N" when the File Menu is open (assuming there is a "_New" entry
  73. /// if the ShortCut is set to "Control-N", this would be a global hotkey that would trigger as well
  74. /// </summary>
  75. public Rune HotKey;
  76. /// <summary>
  77. /// This is the global setting that can be used as a global shortcut to invoke the action on the menu.
  78. /// </summary>
  79. public Key ShortCut {
  80. get => shortCut;
  81. set {
  82. if (shortCut != value) {
  83. if (GetKeyToString (value).Contains ("Control")) {
  84. shortCut = Key.CtrlMask | value;
  85. } else {
  86. shortCut = value;
  87. }
  88. }
  89. }
  90. }
  91. /// <summary>
  92. /// The keystroke combination used in the <see cref="ShortCut"/> as string.
  93. /// </summary>
  94. public ustring ShortCutTag => GetShortCutTag (shortCut);
  95. /// <summary>
  96. /// Gets or sets the title.
  97. /// </summary>
  98. /// <value>The title.</value>
  99. public ustring Title {
  100. get { return title; }
  101. set {
  102. if (title != value) {
  103. title = value;
  104. GetHotKey ();
  105. }
  106. }
  107. }
  108. /// <summary>
  109. /// Gets or sets the help text for the menu item.
  110. /// </summary>
  111. /// <value>The help text.</value>
  112. public ustring Help { get; set; }
  113. /// <summary>
  114. /// Gets or sets the action to be invoked when the menu is triggered
  115. /// </summary>
  116. /// <value>Method to invoke.</value>
  117. public Action Action { get; set; }
  118. /// <summary>
  119. /// Gets or sets the action to be invoked if the menu can be triggered
  120. /// </summary>
  121. /// <value>Function to determine if action is ready to be executed.</value>
  122. public Func<bool> CanExecute { get; set; }
  123. /// <summary>
  124. /// Shortcut to check if the menu item is enabled
  125. /// </summary>
  126. public bool IsEnabled ()
  127. {
  128. return CanExecute == null ? true : CanExecute ();
  129. }
  130. internal int Width => Title.RuneCount + Help.RuneCount + 1 + 2 +
  131. (Checked || CheckType.HasFlag (MenuItemCheckStyle.Checked) || CheckType.HasFlag (MenuItemCheckStyle.Radio) ? 2 : 0) +
  132. (ShortCutTag.RuneCount > 0 ? ShortCutTag.RuneCount + 2 : 0);
  133. /// <summary>
  134. /// Sets or gets whether the <see cref="MenuItem"/> shows a check indicator or not. See <see cref="MenuItemCheckStyle"/>.
  135. /// </summary>
  136. public bool Checked { set; get; }
  137. /// <summary>
  138. /// Sets or gets the type selection indicator the menu item will be displayed with.
  139. /// </summary>
  140. public MenuItemCheckStyle CheckType { get; set; }
  141. /// <summary>
  142. /// Gets or sets the parent for this <see cref="MenuItem"/>.
  143. /// </summary>
  144. /// <value>The parent.</value>
  145. public MenuItem Parent { get; internal set; }
  146. /// <summary>
  147. /// Gets if this <see cref="MenuItem"/> is from a sub-menu.
  148. /// </summary>
  149. internal bool IsFromSubMenu { get {return Parent != null; } }
  150. /// <summary>
  151. /// Merely a debugging aid to see the interaction with main
  152. /// </summary>
  153. public MenuItem GetMenuItem ()
  154. {
  155. return this;
  156. }
  157. /// <summary>
  158. /// Merely a debugging aid to see the interaction with main
  159. /// </summary>
  160. public bool GetMenuBarItem ()
  161. {
  162. return IsFromSubMenu;
  163. }
  164. void GetHotKey ()
  165. {
  166. bool nextIsHot = false;
  167. foreach (var x in title) {
  168. if (x == '_') {
  169. nextIsHot = true;
  170. } else {
  171. if (nextIsHot) {
  172. HotKey = Char.ToUpper ((char)x);
  173. break;
  174. }
  175. nextIsHot = false;
  176. HotKey = default;
  177. }
  178. }
  179. }
  180. /// <summary>
  181. /// Get the <see cref="ShortCut"/> key as string.
  182. /// </summary>
  183. /// <param name="shortCut">The shortcut key.</param>
  184. /// <returns></returns>
  185. public ustring GetShortCutTag (Key shortCut)
  186. {
  187. if (shortCut == Key.ControlSpace) {
  188. return "";
  189. }
  190. var k = (uint)shortCut;
  191. var delimiter = MenuBar.ShortCutDelimiter;
  192. ustring tag = ustring.Empty;
  193. var sCut = GetKeyToString (shortCut).ToString ();
  194. if (sCut.Contains ("Control") || (shortCut & Key.CtrlMask) != 0) {
  195. tag = "Ctrl";
  196. }
  197. if ((shortCut & Key.ShiftMask) != 0) {
  198. if (!tag.IsEmpty) {
  199. tag += delimiter;
  200. }
  201. tag += "Shift";
  202. }
  203. if ((shortCut & Key.AltMask) != 0) {
  204. if (!tag.IsEmpty) {
  205. tag += delimiter;
  206. }
  207. tag += "Alt";
  208. }
  209. ustring [] keys = ustring.Make (sCut).Split (",");
  210. for (int i = 0; i < keys.Length; i++) {
  211. var key = keys [i].TrimSpace ();
  212. if (key == Key.AltMask.ToString () || key == Key.ShiftMask.ToString () || key == Key.CtrlMask.ToString ()) {
  213. continue;
  214. }
  215. if (!tag.IsEmpty) {
  216. tag += delimiter;
  217. }
  218. if (key.Contains ("Control")) {
  219. tag += ((char)key.ElementAt (7)).ToString ();
  220. } else if (!key.Contains ("F") && key.Length > 2 && keys.Length == 1) {
  221. k = (uint)Key.AltMask + k;
  222. tag += ((char)k).ToString ();
  223. } else if (key.Length == 2 && key.StartsWith ("D")) {
  224. tag += ((char)key.ElementAt (1)).ToString ();
  225. } else {
  226. tag += key;
  227. }
  228. }
  229. return tag;
  230. }
  231. ustring GetKeyToString (Key key)
  232. {
  233. if (key == Key.ControlSpace) {
  234. return "";
  235. }
  236. var mK = key & (Key.AltMask | Key.CtrlMask | Key.ShiftMask);
  237. for (uint i = (uint)Key.F1; i < (uint)Key.F12; i++) {
  238. if ((key | (Key)i) == key) {
  239. mK |= (Key)i;
  240. }
  241. }
  242. var k = key;
  243. k &= ~mK;
  244. int.TryParse (k.ToString (), out int c);
  245. var s = mK == Key.ControlSpace ? "" : mK.ToString ();
  246. if (s != "" && (k != Key.ControlSpace || c > 0)) {
  247. s += ",";
  248. }
  249. s += c == 0 ? k == Key.ControlSpace ? "" : k.ToString () : ((char)c).ToString ();
  250. return s;
  251. }
  252. /// <summary>
  253. /// Allows to generate a <see cref="Key"/> from a <see cref="ShortCutTag"/>
  254. /// </summary>
  255. /// <param name="tag">The key as string.</param>
  256. /// <returns></returns>
  257. public Key CreateShortCutFromTag (ustring tag)
  258. {
  259. var sCut = tag;
  260. if (sCut.IsEmpty) {
  261. return default;
  262. }
  263. Key key = Key.ControlSpace;
  264. var hasCtrl = false;
  265. var delimiter = MenuBar.ShortCutDelimiter;
  266. ustring [] keys = sCut.Split (delimiter);
  267. for (int i = 0; i < keys.Length; i++) {
  268. var k = keys [i];
  269. if (k == "Ctrl") {
  270. hasCtrl = true;
  271. key |= Key.CtrlMask;
  272. } else if (k == "Shift") {
  273. key |= Key.ShiftMask;
  274. } else if (k == "Alt") {
  275. key |= Key.AltMask;
  276. } else if (k.StartsWith ("F") && k.Length > 1) {
  277. int.TryParse (k.Substring (1).ToString (), out int n);
  278. for (uint j = (uint)Key.F1; j < (uint)Key.F12; j++) {
  279. int.TryParse (((Key)j).ToString ().Substring (1), out int f);
  280. if (f == n) {
  281. key |= (Key)j;
  282. }
  283. }
  284. } else if (k [0] >= 'A' && k [0] <= 'Z') {
  285. if (hasCtrl) {
  286. var n = k [0] - 'A' + 1;
  287. var d = n - (uint)Key.ControlA;
  288. key |= (Key)((uint)Key.ControlA + d);
  289. } else {
  290. key |= (Key)k [0];
  291. }
  292. } else if (k [0] >= '0' && k [0] <= '9') {
  293. //var n = k [0] - (uint)Key.D0 + 1;
  294. //var d = n - (uint)Key.D0;
  295. //key |= (Key)((uint)Key.D0 + d);
  296. key |= (Key)k [0];
  297. }
  298. }
  299. return key;
  300. }
  301. }
  302. /// <summary>
  303. /// A <see cref="MenuBarItem"/> contains <see cref="MenuBarItem"/>s or <see cref="MenuItem"/>s.
  304. /// </summary>
  305. public class MenuBarItem : MenuItem {
  306. /// <summary>
  307. /// Initializes a new <see cref="MenuBarItem"/> as a <see cref="MenuItem"/>.
  308. /// </summary>
  309. /// <param name="title">Title for the menu item.</param>
  310. /// <param name="help">Help text to display.</param>
  311. /// <param name="action">Action to invoke when the menu item is activated.</param>
  312. /// <param name="canExecute">Function to determine if the action can currently be executed.</param>
  313. /// <param name="parent">The parent <see cref="MenuItem"/> of this if exist, otherwise is null.</param>
  314. public MenuBarItem (ustring title, ustring help, Action action, Func<bool> canExecute = null, MenuItem parent = null) : base (title, help, action, canExecute, parent)
  315. {
  316. SetTitle (title ?? "");
  317. Children = null;
  318. }
  319. /// <summary>
  320. /// Initializes a new <see cref="MenuBarItem"/>.
  321. /// </summary>
  322. /// <param name="title">Title for the menu item.</param>
  323. /// <param name="children">The items in the current menu.</param>
  324. /// <param name="parent">The parent <see cref="MenuItem"/> of this if exist, otherwise is null.</param>
  325. public MenuBarItem (ustring title, MenuItem [] children, MenuItem parent = null)
  326. {
  327. if (children == null) {
  328. throw new ArgumentNullException (nameof (children), "The parameter cannot be null. Use an empty array instead.");
  329. }
  330. SetTitle (title ?? "");
  331. if (parent != null) {
  332. Parent = parent;
  333. }
  334. SetChildrensParent (children);
  335. Children = children;
  336. }
  337. /// <summary>
  338. /// Initializes a new <see cref="MenuBarItem"/>.
  339. /// </summary>
  340. /// <param name="children">The items in the current menu.</param>
  341. public MenuBarItem (MenuItem [] children) : this ("", children) { }
  342. /// <summary>
  343. /// Initializes a new <see cref="MenuBarItem"/>.
  344. /// </summary>
  345. public MenuBarItem () : this (children: new MenuItem [] { }) { }
  346. //static int GetMaxTitleLength (MenuItem [] children)
  347. //{
  348. // int maxLength = 0;
  349. // foreach (var item in children) {
  350. // int len = GetMenuBarItemLength (item.Title);
  351. // if (len > maxLength)
  352. // maxLength = len;
  353. // item.IsFromSubMenu = true;
  354. // }
  355. // return maxLength;
  356. //}
  357. void SetChildrensParent (MenuItem [] childrens)
  358. {
  359. foreach (var child in childrens) {
  360. if (child != null && child.Parent == null) {
  361. child.Parent = this;
  362. }
  363. }
  364. }
  365. /// <summary>
  366. /// Check if the children parameter is a <see cref="MenuBarItem"/>.
  367. /// </summary>
  368. /// <param name="children"></param>
  369. /// <returns>Returns a <see cref="MenuBarItem"/> or null otherwise.</returns>
  370. public MenuBarItem SubMenu (MenuItem children)
  371. {
  372. return children as MenuBarItem;
  373. }
  374. /// <summary>
  375. /// Check if the <see cref="MenuItem"/> parameter is a child of this.
  376. /// </summary>
  377. /// <param name="menuItem"></param>
  378. /// <returns>Returns <c>true</c> if it is a child of this. <c>false</c> otherwise.</returns>
  379. public bool IsSubMenuOf (MenuItem menuItem)
  380. {
  381. foreach (var child in Children) {
  382. if (child == menuItem && child.Parent == menuItem.Parent) {
  383. return true;
  384. }
  385. }
  386. return false;
  387. }
  388. /// <summary>
  389. /// Get the index of the <see cref="MenuItem"/> parameter.
  390. /// </summary>
  391. /// <param name="children"></param>
  392. /// <returns>Returns a value bigger than -1 if the <see cref="MenuItem"/> is a child of this.</returns>
  393. public int GetChildrenIndex (MenuItem children)
  394. {
  395. if (Children?.Length == 0) {
  396. return -1;
  397. }
  398. int i = 0;
  399. foreach (var child in Children) {
  400. if (child == children) {
  401. return i;
  402. }
  403. i++;
  404. }
  405. return -1;
  406. }
  407. void SetTitle (ustring title)
  408. {
  409. if (title == null)
  410. title = "";
  411. Title = title;
  412. }
  413. int GetMenuBarItemLength (ustring title)
  414. {
  415. int len = 0;
  416. foreach (var ch in title) {
  417. if (ch == '_')
  418. continue;
  419. len++;
  420. }
  421. return len;
  422. }
  423. ///// <summary>
  424. ///// Gets or sets the title to display.
  425. ///// </summary>
  426. ///// <value>The title.</value>
  427. //public ustring Title { get; set; }
  428. /// <summary>
  429. /// Gets or sets an array of <see cref="MenuItem"/> objects that are the children of this <see cref="MenuBarItem"/>
  430. /// </summary>
  431. /// <value>The children.</value>
  432. public MenuItem [] Children { get; set; }
  433. internal int TitleLength => GetMenuBarItemLength (Title);
  434. internal bool IsTopLevel { get => Parent == null && (Children == null || Children.Length == 0) && Action != null; }
  435. }
  436. class Menu : View {
  437. internal MenuBarItem barItems;
  438. MenuBar host;
  439. internal int current;
  440. internal View previousSubFocused;
  441. static Rect MakeFrame (int x, int y, MenuItem [] items)
  442. {
  443. if (items == null || items.Length == 0) {
  444. return new Rect ();
  445. }
  446. int maxW = items.Max (z => z?.Width) ?? 0;
  447. return new Rect (x, y, maxW + 2, items.Length + 2);
  448. }
  449. public Menu (MenuBar host, int x, int y, MenuBarItem barItems) : base (MakeFrame (x, y, barItems.Children))
  450. {
  451. this.barItems = barItems;
  452. this.host = host;
  453. if (barItems.IsTopLevel) {
  454. // This is a standalone MenuItem on a MenuBar
  455. ColorScheme = Colors.Menu;
  456. CanFocus = true;
  457. } else {
  458. current = -1;
  459. for (int i = 0; i < barItems.Children.Length; i++) {
  460. if (barItems.Children [i] != null) {
  461. current = i;
  462. break;
  463. }
  464. }
  465. ColorScheme = Colors.Menu;
  466. CanFocus = true;
  467. WantMousePositionReports = host.WantMousePositionReports;
  468. }
  469. }
  470. internal Attribute DetermineColorSchemeFor (MenuItem item, int index)
  471. {
  472. if (item != null) {
  473. if (index == current) return ColorScheme.Focus;
  474. if (!item.IsEnabled ()) return ColorScheme.Disabled;
  475. }
  476. return ColorScheme.Normal;
  477. }
  478. public override void Redraw (Rect bounds)
  479. {
  480. Driver.SetAttribute (ColorScheme.Normal);
  481. DrawFrame (bounds, padding: 0, fill: true);
  482. for (int i = 0; i < barItems.Children.Length; i++) {
  483. var item = barItems.Children [i];
  484. Driver.SetAttribute (item == null ? ColorScheme.Normal : i == current ? ColorScheme.Focus : ColorScheme.Normal);
  485. if (item == null) {
  486. Move (0, i + 1);
  487. Driver.AddRune (Driver.LeftTee);
  488. } else
  489. Move (1, i + 1);
  490. Driver.SetAttribute (DetermineColorSchemeFor (item, i));
  491. for (int p = 0; p < Frame.Width - 2; p++)
  492. if (item == null)
  493. Driver.AddRune (Driver.HLine);
  494. else if (p == Frame.Width - 3 && barItems.SubMenu(barItems.Children [i]) != null)
  495. Driver.AddRune (Driver.RightArrow);
  496. else
  497. Driver.AddRune (' ');
  498. if (item == null) {
  499. Move (Frame.Right - 1, i + 1);
  500. Driver.AddRune (Driver.RightTee);
  501. continue;
  502. }
  503. ustring textToDraw;
  504. var checkChar = Driver.Selected;
  505. var uncheckedChar = Driver.UnSelected;
  506. if (item.CheckType.HasFlag (MenuItemCheckStyle.Checked)) {
  507. checkChar = Driver.Checked;
  508. uncheckedChar = Driver.UnChecked;
  509. }
  510. // Support Checked even though CheckType wasn't set
  511. if (item.Checked) {
  512. textToDraw = ustring.Make (new Rune [] { checkChar, ' ' }) + item.Title;
  513. } else if (item.CheckType.HasFlag (MenuItemCheckStyle.Checked) || item.CheckType.HasFlag (MenuItemCheckStyle.Radio)) {
  514. textToDraw = ustring.Make (new Rune [] { uncheckedChar, ' ' }) + item.Title;
  515. } else {
  516. textToDraw = item.Title;
  517. }
  518. Move (2, i + 1);
  519. if (!item.IsEnabled ())
  520. DrawHotString (textToDraw, ColorScheme.Disabled, ColorScheme.Disabled);
  521. else
  522. DrawHotString (textToDraw,
  523. i == current ? ColorScheme.HotFocus : ColorScheme.HotNormal,
  524. i == current ? ColorScheme.Focus : ColorScheme.Normal);
  525. // The help string
  526. var l = item.ShortCutTag.RuneCount == 0 ? item.Help.RuneCount : item.Help.RuneCount + item.ShortCutTag.RuneCount + 2;
  527. Move (Frame.Width - l - 2, 1 + i);
  528. Driver.AddStr (item.Help);
  529. // The shortcut tag string
  530. if (!item.ShortCutTag.IsEmpty) {
  531. l = item.ShortCutTag.RuneCount;
  532. Move (Frame.Width - l - 2, 1 + i);
  533. Driver.AddStr (item.ShortCutTag);
  534. }
  535. }
  536. PositionCursor ();
  537. }
  538. public override void PositionCursor ()
  539. {
  540. if (host == null || host.IsMenuOpen)
  541. if (barItems.IsTopLevel) {
  542. host.PositionCursor ();
  543. } else
  544. Move (2, 1 + current);
  545. else
  546. host.PositionCursor ();
  547. }
  548. public void Run (Action action)
  549. {
  550. if (action == null)
  551. return;
  552. Application.UngrabMouse ();
  553. host.CloseAllMenus ();
  554. Application.Refresh ();
  555. Application.MainLoop.AddIdle (() => {
  556. action ();
  557. return false;
  558. });
  559. }
  560. public override bool OnLeave (View view)
  561. {
  562. return host.OnLeave (view);
  563. }
  564. public override bool OnKeyDown (KeyEvent keyEvent)
  565. {
  566. if (keyEvent.IsAlt) {
  567. host.CloseAllMenus ();
  568. return true;
  569. }
  570. return false;
  571. }
  572. public override bool ProcessHotKey (KeyEvent keyEvent)
  573. {
  574. // To ncurses simulate a AltMask key pressing Alt+Space because
  575. // it can�t detect an alone special key down was pressed.
  576. if (keyEvent.IsAlt && keyEvent.Key == Key.AltMask) {
  577. OnKeyDown (keyEvent);
  578. return true;
  579. }
  580. return false;
  581. }
  582. public override bool ProcessKey (KeyEvent kb)
  583. {
  584. switch (kb.Key) {
  585. case Key.Tab:
  586. host.CleanUp ();
  587. return true;
  588. case Key.CursorUp:
  589. return MoveUp ();
  590. case Key.CursorDown:
  591. return MoveDown ();
  592. case Key.CursorLeft:
  593. host.PreviousMenu (true);
  594. return true;
  595. case Key.CursorRight:
  596. host.NextMenu (barItems.IsTopLevel || (barItems.Children != null && current > -1 && current < barItems.Children.Length && barItems.Children [current].IsFromSubMenu) ? true : false);
  597. return true;
  598. case Key.Esc:
  599. Application.UngrabMouse ();
  600. host.CloseAllMenus ();
  601. return true;
  602. case Key.Enter:
  603. if (barItems.IsTopLevel) {
  604. Run (barItems.Action);
  605. } else if (current > -1) {
  606. Run (barItems.Children [current].Action);
  607. }
  608. return true;
  609. default:
  610. // TODO: rune-ify
  611. if (barItems.Children != null && Char.IsLetterOrDigit ((char)kb.KeyValue)) {
  612. var x = Char.ToUpper ((char)kb.KeyValue);
  613. foreach (var item in barItems.Children) {
  614. if (item == null) continue;
  615. if (item.IsEnabled () && item.HotKey == x) {
  616. host.CloseMenu ();
  617. Run (item.Action);
  618. return true;
  619. }
  620. }
  621. }
  622. break;
  623. }
  624. return false;
  625. }
  626. bool MoveDown ()
  627. {
  628. if (barItems.IsTopLevel) {
  629. return true;
  630. }
  631. bool disabled;
  632. do {
  633. current++;
  634. if (current >= barItems.Children.Length) {
  635. current = 0;
  636. }
  637. if (this != host.openCurrentMenu && barItems.Children [current].IsFromSubMenu && host.selectedSub > -1) {
  638. host.PreviousMenu (true);
  639. host.SelectEnabledItem (barItems.Children, current, out current);
  640. host.openCurrentMenu = this;
  641. }
  642. var item = barItems.Children [current];
  643. if (item?.IsEnabled () != true) {
  644. disabled = true;
  645. } else {
  646. disabled = false;
  647. }
  648. if (host.UseKeysUpDownAsKeysLeftRight && barItems.SubMenu (barItems.Children [current]) != null &&
  649. !disabled && host.IsMenuOpen) {
  650. CheckSubMenu ();
  651. break;
  652. }
  653. if (!host.IsMenuOpen) {
  654. host.OpenMenu (host.selected);
  655. }
  656. } while (barItems.Children [current] == null || disabled);
  657. SetNeedsDisplay ();
  658. return true;
  659. }
  660. bool MoveUp ()
  661. {
  662. if (barItems.IsTopLevel || current == -1) {
  663. return true;
  664. }
  665. bool disabled;
  666. do {
  667. current--;
  668. if (host.UseKeysUpDownAsKeysLeftRight) {
  669. if ((current == -1 || this != host.openCurrentMenu) && barItems.Children [current + 1].IsFromSubMenu && host.selectedSub > -1) {
  670. current++;
  671. host.PreviousMenu (true);
  672. if (current > 0) {
  673. current--;
  674. host.openCurrentMenu = this;
  675. }
  676. break;
  677. }
  678. }
  679. if (current < 0)
  680. current = barItems.Children.Length - 1;
  681. if (!host.SelectEnabledItem (barItems.Children, current, out current, false)) {
  682. current = 0;
  683. if (!host.SelectEnabledItem (barItems.Children, current, out current)) {
  684. host.CloseMenu ();
  685. }
  686. break;
  687. }
  688. var item = barItems.Children [current];
  689. if (item?.IsEnabled () != true) {
  690. disabled = true;
  691. } else {
  692. disabled = false;
  693. }
  694. if (host.UseKeysUpDownAsKeysLeftRight && barItems.SubMenu (barItems.Children [current]) != null &&
  695. !disabled && host.IsMenuOpen) {
  696. CheckSubMenu ();
  697. break;
  698. }
  699. } while (barItems.Children [current] == null || disabled);
  700. SetNeedsDisplay ();
  701. return true;
  702. }
  703. public override bool MouseEvent (MouseEvent me)
  704. {
  705. if (!host.handled && !host.HandleGrabView (me, this)) {
  706. return false;
  707. }
  708. host.handled = false;
  709. bool disabled;
  710. if (me.Flags == MouseFlags.Button1Clicked) {
  711. disabled = false;
  712. if (me.Y < 1)
  713. return true;
  714. var meY = me.Y - 1;
  715. if (meY >= barItems.Children.Length)
  716. return true;
  717. var item = barItems.Children [meY];
  718. if (item == null || !item.IsEnabled ()) disabled = true;
  719. if (item != null && !disabled)
  720. Run (barItems.Children [meY].Action);
  721. return true;
  722. } else if (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked ||
  723. me.Flags == MouseFlags.Button1TripleClicked || me.Flags == MouseFlags.ReportMousePosition ||
  724. me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition)) {
  725. disabled = false;
  726. if (me.Y < 1 || me.Y - 1 >= barItems.Children.Length) {
  727. return true;
  728. }
  729. var item = barItems.Children [me.Y - 1];
  730. if (item == null || !item.IsEnabled ()) disabled = true;
  731. if (item != null && !disabled)
  732. current = me.Y - 1;
  733. CheckSubMenu ();
  734. return true;
  735. }
  736. return false;
  737. }
  738. internal void CheckSubMenu ()
  739. {
  740. if (current == -1 || barItems.Children [current] == null) {
  741. return;
  742. }
  743. var subMenu = barItems.SubMenu (barItems.Children [current]);
  744. if (subMenu != null) {
  745. int pos = -1;
  746. if (host.openSubMenu != null) {
  747. pos = host.openSubMenu.FindIndex (o => o?.barItems == subMenu);
  748. }
  749. if (pos == -1 && this != host.openCurrentMenu && subMenu.Children != host.openCurrentMenu.barItems.Children) {
  750. host.CloseMenu (false, true);
  751. }
  752. host.Activate (host.selected, pos, subMenu);
  753. } else if (host.openSubMenu?.Last ().barItems.IsSubMenuOf (barItems.Children [current]) == false) {
  754. host.CloseMenu (false, true);
  755. } else {
  756. SetNeedsDisplay ();
  757. }
  758. }
  759. int GetSubMenuIndex (MenuBarItem subMenu)
  760. {
  761. int pos = -1;
  762. if (this != null && Subviews.Count > 0) {
  763. Menu v = null;
  764. foreach (var menu in Subviews) {
  765. if (((Menu)menu).barItems == subMenu)
  766. v = (Menu)menu;
  767. }
  768. if (v != null)
  769. pos = Subviews.IndexOf (v);
  770. }
  771. return pos;
  772. }
  773. }
  774. /// <summary>
  775. /// The MenuBar provides a menu for Terminal.Gui applications.
  776. /// </summary>
  777. /// <remarks>
  778. /// <para>
  779. /// The <see cref="MenuBar"/> appears on the first row of the terminal.
  780. /// </para>
  781. /// <para>
  782. /// The <see cref="MenuBar"/> provides global hotkeys for the application.
  783. /// </para>
  784. /// </remarks>
  785. public class MenuBar : View {
  786. /// <summary>
  787. /// Gets or sets the array of <see cref="MenuBarItem"/>s for the menu. Only set this when the <see cref="MenuBar"/> is vislble.
  788. /// </summary>
  789. /// <value>The menu array.</value>
  790. public MenuBarItem [] Menus { get; set; }
  791. internal int selected;
  792. internal int selectedSub;
  793. Action action;
  794. /// <summary>
  795. /// Used for change the navigation key style.
  796. /// </summary>
  797. public bool UseKeysUpDownAsKeysLeftRight { get; set; } = true;
  798. static ustring shortCutDelimiter = "+";
  799. /// <summary>
  800. /// Used for change the shortcut delimiter separator.
  801. /// </summary>
  802. public static ustring ShortCutDelimiter {
  803. get => shortCutDelimiter;
  804. set {
  805. if (shortCutDelimiter != value) {
  806. shortCutDelimiter = value == ustring.Empty ? " " : value;
  807. }
  808. }
  809. }
  810. /// <summary>
  811. /// Initializes a new instance of the <see cref="MenuBar"/>.
  812. /// </summary>
  813. public MenuBar () : this (new MenuBarItem [] { }) { }
  814. /// <summary>
  815. /// Initializes a new instance of the <see cref="MenuBar"/> class with the specified set of toplevel menu items.
  816. /// </summary>
  817. /// <param name="menus">Individual menu items; a null item will result in a separator being drawn.</param>
  818. public MenuBar (MenuBarItem [] menus) : base ()
  819. {
  820. X = 0;
  821. Y = 0;
  822. Width = Dim.Fill ();
  823. Height = 1;
  824. Menus = menus;
  825. //CanFocus = true;
  826. selected = -1;
  827. selectedSub = -1;
  828. ColorScheme = Colors.Menu;
  829. WantMousePositionReports = true;
  830. IsMenuOpen = false;
  831. }
  832. bool openedByAltKey;
  833. bool isCleaning;
  834. ///<inheritdoc/>
  835. public override bool OnLeave (View view)
  836. {
  837. if ((!(view is MenuBar) && !(view is Menu) || !(view is MenuBar) && !(view is Menu) && openMenu != null) && !isCleaning && !reopen) {
  838. CleanUp ();
  839. return true;
  840. }
  841. return false;
  842. }
  843. ///<inheritdoc/>
  844. public override bool OnKeyDown (KeyEvent keyEvent)
  845. {
  846. if (keyEvent.IsAlt) {
  847. openedByAltKey = true;
  848. SetNeedsDisplay ();
  849. openedByHotKey = false;
  850. }
  851. return false;
  852. }
  853. ///<inheritdoc/>
  854. public override bool OnKeyUp (KeyEvent keyEvent)
  855. {
  856. if (keyEvent.IsAlt) {
  857. // User pressed Alt - this may be a precursor to a menu accelerator (e.g. Alt-F)
  858. if (!keyEvent.IsCtrl && openedByAltKey && !IsMenuOpen && openMenu == null && ((uint)keyEvent.Key & (uint)Key.CharMask) == 0) {
  859. // There's no open menu, the first menu item should be highlight.
  860. // The right way to do this is to SetFocus(MenuBar), but for some reason
  861. // that faults.
  862. //Activate (0);
  863. //StartMenu ();
  864. IsMenuOpen = true;
  865. selected = 0;
  866. CanFocus = true;
  867. lastFocused = SuperView.MostFocused;
  868. SetFocus ();
  869. SetNeedsDisplay ();
  870. Application.GrabMouse (this);
  871. } else if (!openedByHotKey) {
  872. // There's an open menu. If this Alt key-up is a pre-cursor to an accelerator
  873. // we don't want to close the menu because it'll flash.
  874. // How to deal with that?
  875. CleanUp ();
  876. }
  877. return true;
  878. }
  879. return false;
  880. }
  881. internal void CleanUp ()
  882. {
  883. isCleaning = true;
  884. if (openMenu != null) {
  885. CloseAllMenus ();
  886. }
  887. openedByAltKey = false;
  888. IsMenuOpen = false;
  889. selected = -1;
  890. CanFocus = false;
  891. if (lastFocused != null) {
  892. lastFocused.SetFocus ();
  893. }
  894. SetNeedsDisplay ();
  895. Application.UngrabMouse ();
  896. isCleaning = false;
  897. }
  898. ///<inheritdoc/>
  899. public override void Redraw (Rect bounds)
  900. {
  901. Move (0, 0);
  902. Driver.SetAttribute (Colors.Menu.Normal);
  903. for (int i = 0; i < Frame.Width; i++)
  904. Driver.AddRune (' ');
  905. Move (1, 0);
  906. int pos = 1;
  907. for (int i = 0; i < Menus.Length; i++) {
  908. var menu = Menus [i];
  909. Move (pos, 0);
  910. Attribute hotColor, normalColor;
  911. if (i == selected) {
  912. hotColor = i == selected ? ColorScheme.HotFocus : ColorScheme.HotNormal;
  913. normalColor = i == selected ? ColorScheme.Focus : ColorScheme.Normal;
  914. } else if (openedByAltKey) {
  915. hotColor = ColorScheme.HotNormal;
  916. normalColor = ColorScheme.Normal;
  917. } else {
  918. hotColor = ColorScheme.Normal;
  919. normalColor = ColorScheme.Normal;
  920. }
  921. DrawHotString (menu.Help.IsEmpty ? $" {menu.Title} " : $" {menu.Title} {menu.Help} ", hotColor, normalColor);
  922. pos += 1 + menu.TitleLength + (menu.Help.Length > 0 ? menu.Help.Length + 2 : 0) + 2;
  923. }
  924. PositionCursor ();
  925. }
  926. ///<inheritdoc/>
  927. public override void PositionCursor ()
  928. {
  929. int pos = 0;
  930. for (int i = 0; i < Menus.Length; i++) {
  931. if (i == selected) {
  932. pos++;
  933. if (IsMenuOpen)
  934. Move (pos + 1, 0);
  935. else {
  936. Move (pos + 1, 0);
  937. }
  938. return;
  939. } else if (IsMenuOpen) {
  940. pos += 1 + Menus [i].TitleLength + (Menus [i].Help.Length > 0 ? Menus [i].Help.Length + 2 : 0) + 2;
  941. } else {
  942. pos += 2 + Menus [i].TitleLength + (Menus [i].Help.Length > 0 ? Menus [i].Help.Length + 2 : 0) + 1;
  943. }
  944. }
  945. //Move (0, 0);
  946. }
  947. void Selected (MenuItem item)
  948. {
  949. // TODO: Running = false;
  950. action = item.Action;
  951. }
  952. /// <summary>
  953. /// Raised as a menu is opening.
  954. /// </summary>
  955. public event Action MenuOpening;
  956. /// <summary>
  957. /// Raised when a menu is closing.
  958. /// </summary>
  959. public event Action MenuClosing;
  960. internal Menu openMenu;
  961. internal Menu openCurrentMenu;
  962. internal List<Menu> openSubMenu;
  963. View previousFocused;
  964. internal bool isMenuOpening;
  965. internal bool isMenuClosing;
  966. /// <summary>
  967. /// True if the menu is open; otherwise false.
  968. /// </summary>
  969. public bool IsMenuOpen { get; protected set; }
  970. /// <summary>
  971. /// Virtual method that will invoke the <see cref="MenuOpening"/>
  972. /// </summary>
  973. public virtual void OnMenuOpening ()
  974. {
  975. MenuOpening?.Invoke ();
  976. }
  977. /// <summary>
  978. /// Virtual method that will invoke the <see cref="MenuClosing"/>
  979. /// </summary>
  980. public virtual void OnMenuClosing ()
  981. {
  982. MenuClosing?.Invoke ();
  983. }
  984. View lastFocused;
  985. /// <summary>
  986. /// Get the lasted focused view before open the menu.
  987. /// </summary>
  988. public View LastFocused { get; private set; }
  989. internal void OpenMenu (int index, int sIndex = -1, MenuBarItem subMenu = null)
  990. {
  991. isMenuOpening = true;
  992. OnMenuOpening ();
  993. int pos = 0;
  994. switch (subMenu) {
  995. case null:
  996. lastFocused = lastFocused ?? SuperView.MostFocused;
  997. if (openSubMenu != null)
  998. CloseMenu (false, true);
  999. if (openMenu != null) {
  1000. SuperView.Remove (openMenu);
  1001. openMenu.Dispose ();
  1002. }
  1003. for (int i = 0; i < index; i++)
  1004. pos += Menus [i].Title.RuneCount + (Menus [i].Help.RuneCount > 0 ? Menus [i].Help.RuneCount + 2 : 0) + 2;
  1005. openMenu = new Menu (this, pos, 1, Menus [index]);
  1006. openCurrentMenu = openMenu;
  1007. openCurrentMenu.previousSubFocused = openMenu;
  1008. SuperView.Add (openMenu);
  1009. openMenu.SetFocus ();
  1010. break;
  1011. default:
  1012. if (openSubMenu == null)
  1013. openSubMenu = new List<Menu> ();
  1014. if (sIndex > -1) {
  1015. RemoveSubMenu (sIndex);
  1016. } else {
  1017. var last = openSubMenu.Count > 0 ? openSubMenu.Last () : openMenu;
  1018. openCurrentMenu = new Menu (this, last.Frame.Left + last.Frame.Width, last.Frame.Top + 1 + last.current, subMenu);
  1019. openCurrentMenu.previousSubFocused = last.previousSubFocused;
  1020. openSubMenu.Add (openCurrentMenu);
  1021. SuperView.Add (openCurrentMenu);
  1022. }
  1023. selectedSub = openSubMenu.Count - 1;
  1024. if (selectedSub > -1 && SelectEnabledItem (openCurrentMenu.barItems.Children, openCurrentMenu.current, out openCurrentMenu.current)) {
  1025. openCurrentMenu.SetFocus ();
  1026. }
  1027. break;
  1028. }
  1029. isMenuOpening = false;
  1030. IsMenuOpen = true;
  1031. }
  1032. /// <summary>
  1033. /// Opens the current Menu programatically.
  1034. /// </summary>
  1035. public void OpenMenu ()
  1036. {
  1037. if (openMenu != null)
  1038. return;
  1039. selected = 0;
  1040. SetNeedsDisplay ();
  1041. previousFocused = SuperView.Focused;
  1042. OpenMenu (selected);
  1043. if (!SelectEnabledItem (openCurrentMenu.barItems.Children, openCurrentMenu.current, out openCurrentMenu.current)) {
  1044. CloseMenu ();
  1045. }
  1046. openCurrentMenu.CheckSubMenu ();
  1047. Application.GrabMouse (this);
  1048. }
  1049. // Activates the menu, handles either first focus, or activating an entry when it was already active
  1050. // For mouse events.
  1051. internal void Activate (int idx, int sIdx = -1, MenuBarItem subMenu = null)
  1052. {
  1053. selected = idx;
  1054. selectedSub = sIdx;
  1055. if (openMenu == null)
  1056. previousFocused = SuperView.Focused;
  1057. OpenMenu (idx, sIdx, subMenu);
  1058. if (!SelectEnabledItem (openCurrentMenu.barItems.Children, openCurrentMenu.current, out openCurrentMenu.current)) {
  1059. if (subMenu == null) {
  1060. CloseMenu ();
  1061. }
  1062. }
  1063. SetNeedsDisplay ();
  1064. }
  1065. internal bool SelectEnabledItem (IEnumerable<MenuItem> chldren, int current, out int newCurrent, bool forward = true)
  1066. {
  1067. if (chldren == null) {
  1068. newCurrent = -1;
  1069. return true;
  1070. }
  1071. IEnumerable<MenuItem> childrens;
  1072. if (forward) {
  1073. childrens = chldren;
  1074. } else {
  1075. childrens = chldren.Reverse ();
  1076. }
  1077. int count;
  1078. if (forward) {
  1079. count = -1;
  1080. } else {
  1081. count = childrens.Count ();
  1082. }
  1083. foreach (var child in childrens) {
  1084. if (forward) {
  1085. if (++count < current) {
  1086. continue;
  1087. }
  1088. } else {
  1089. if (--count > current) {
  1090. continue;
  1091. }
  1092. }
  1093. if (child == null || !child.IsEnabled ()) {
  1094. if (forward) {
  1095. current++;
  1096. } else {
  1097. current--;
  1098. }
  1099. } else {
  1100. newCurrent = current;
  1101. return true;
  1102. }
  1103. }
  1104. newCurrent = -1;
  1105. return false;
  1106. }
  1107. /// <summary>
  1108. /// Closes the current Menu programatically, if open.
  1109. /// </summary>
  1110. public void CloseMenu ()
  1111. {
  1112. CloseMenu (false, false);
  1113. }
  1114. bool reopen;
  1115. internal void CloseMenu (bool reopen = false, bool isSubMenu = false)
  1116. {
  1117. isMenuClosing = true;
  1118. this.reopen = reopen;
  1119. OnMenuClosing ();
  1120. switch (isSubMenu) {
  1121. case false:
  1122. if (openMenu != null) {
  1123. SuperView?.Remove (openMenu);
  1124. }
  1125. SetNeedsDisplay ();
  1126. if (previousFocused != null && previousFocused is Menu && openMenu != null && previousFocused.ToString () != openCurrentMenu.ToString ())
  1127. previousFocused.SetFocus ();
  1128. openMenu?.Dispose ();
  1129. openMenu = null;
  1130. if (lastFocused is Menu || lastFocused is MenuBar) {
  1131. lastFocused = null;
  1132. }
  1133. LastFocused = lastFocused;
  1134. lastFocused = null;
  1135. if (LastFocused != null) {
  1136. if (!reopen) {
  1137. selected = -1;
  1138. }
  1139. LastFocused.SetFocus ();
  1140. } else {
  1141. SetFocus ();
  1142. PositionCursor ();
  1143. }
  1144. IsMenuOpen = false;
  1145. break;
  1146. case true:
  1147. selectedSub = -1;
  1148. SetNeedsDisplay ();
  1149. RemoveAllOpensSubMenus ();
  1150. openCurrentMenu.previousSubFocused.SetFocus ();
  1151. openSubMenu = null;
  1152. IsMenuOpen = true;
  1153. break;
  1154. }
  1155. this.reopen = false;
  1156. isMenuClosing = false;
  1157. }
  1158. void RemoveSubMenu (int index)
  1159. {
  1160. if (openSubMenu == null)
  1161. return;
  1162. for (int i = openSubMenu.Count - 1; i > index; i--) {
  1163. isMenuClosing = true;
  1164. if (openSubMenu.Count - 1 > 0)
  1165. openSubMenu [i - 1].SetFocus ();
  1166. else
  1167. openMenu.SetFocus ();
  1168. if (openSubMenu != null) {
  1169. var menu = openSubMenu [i];
  1170. SuperView.Remove (menu);
  1171. openSubMenu.Remove (menu);
  1172. menu.Dispose ();
  1173. }
  1174. RemoveSubMenu (i);
  1175. }
  1176. if (openSubMenu.Count > 0)
  1177. openCurrentMenu = openSubMenu.Last ();
  1178. //if (openMenu.Subviews.Count == 0)
  1179. // return;
  1180. //if (index == 0) {
  1181. // //SuperView.SetFocus (previousSubFocused);
  1182. // FocusPrev ();
  1183. // return;
  1184. //}
  1185. //for (int i = openMenu.Subviews.Count - 1; i > index; i--) {
  1186. // isMenuClosing = true;
  1187. // if (openMenu.Subviews.Count - 1 > 0)
  1188. // SuperView.SetFocus (openMenu.Subviews [i - 1]);
  1189. // else
  1190. // SuperView.SetFocus (openMenu);
  1191. // if (openMenu != null) {
  1192. // Remove (openMenu.Subviews [i]);
  1193. // openMenu.Remove (openMenu.Subviews [i]);
  1194. // }
  1195. // RemoveSubMenu (i);
  1196. //}
  1197. isMenuClosing = false;
  1198. }
  1199. internal void RemoveAllOpensSubMenus ()
  1200. {
  1201. if (openSubMenu != null) {
  1202. foreach (var item in openSubMenu) {
  1203. SuperView.Remove (item);
  1204. item.Dispose ();
  1205. }
  1206. }
  1207. }
  1208. internal void CloseAllMenus ()
  1209. {
  1210. if (!isMenuOpening && !isMenuClosing) {
  1211. if (openSubMenu != null)
  1212. CloseMenu (false, true);
  1213. CloseMenu ();
  1214. if (LastFocused != null && LastFocused != this)
  1215. selected = -1;
  1216. }
  1217. IsMenuOpen = false;
  1218. openedByHotKey = false;
  1219. openedByAltKey = false;
  1220. }
  1221. View FindDeepestMenu (View view, ref int count)
  1222. {
  1223. count = count > 0 ? count : 0;
  1224. foreach (var menu in view.Subviews) {
  1225. if (menu is Menu) {
  1226. count++;
  1227. return FindDeepestMenu ((Menu)menu, ref count);
  1228. }
  1229. }
  1230. return view;
  1231. }
  1232. internal void PreviousMenu (bool isSubMenu = false)
  1233. {
  1234. switch (isSubMenu) {
  1235. case false:
  1236. if (selected <= 0)
  1237. selected = Menus.Length - 1;
  1238. else
  1239. selected--;
  1240. if (selected > -1)
  1241. CloseMenu (true, false);
  1242. OpenMenu (selected);
  1243. if (!SelectEnabledItem (openCurrentMenu.barItems.Children, openCurrentMenu.current, out openCurrentMenu.current, false)) {
  1244. openCurrentMenu.current = 0;
  1245. if (!SelectEnabledItem (openCurrentMenu.barItems.Children, openCurrentMenu.current, out openCurrentMenu.current)) {
  1246. CloseMenu ();
  1247. }
  1248. break;
  1249. }
  1250. break;
  1251. case true:
  1252. if (selectedSub > -1) {
  1253. selectedSub--;
  1254. RemoveSubMenu (selectedSub);
  1255. SetNeedsDisplay ();
  1256. } else
  1257. PreviousMenu ();
  1258. break;
  1259. }
  1260. }
  1261. internal void NextMenu (bool isSubMenu = false)
  1262. {
  1263. switch (isSubMenu) {
  1264. case false:
  1265. if (selected == -1)
  1266. selected = 0;
  1267. else if (selected + 1 == Menus.Length)
  1268. selected = 0;
  1269. else
  1270. selected++;
  1271. if (selected > -1)
  1272. CloseMenu (true);
  1273. OpenMenu (selected);
  1274. SelectEnabledItem (openCurrentMenu.barItems.Children, openCurrentMenu.current, out openCurrentMenu.current);
  1275. break;
  1276. case true:
  1277. if (UseKeysUpDownAsKeysLeftRight) {
  1278. CloseMenu (false, true);
  1279. NextMenu ();
  1280. } else {
  1281. var subMenu = openCurrentMenu.barItems.SubMenu (openCurrentMenu.barItems.Children [openCurrentMenu.current]);
  1282. if ((selectedSub == -1 || openSubMenu == null || openSubMenu?.Count == selectedSub) && subMenu == null) {
  1283. if (openSubMenu != null)
  1284. CloseMenu (false, true);
  1285. NextMenu ();
  1286. } else if (subMenu != null ||
  1287. !openCurrentMenu.barItems.Children [openCurrentMenu.current].IsFromSubMenu)
  1288. selectedSub++;
  1289. else
  1290. return;
  1291. SetNeedsDisplay ();
  1292. openCurrentMenu.CheckSubMenu ();
  1293. }
  1294. break;
  1295. }
  1296. }
  1297. bool openedByHotKey;
  1298. internal bool FindAndOpenMenuByHotkey (KeyEvent kb)
  1299. {
  1300. //int pos = 0;
  1301. var c = ((uint)kb.Key & (uint)Key.CharMask);
  1302. for (int i = 0; i < Menus.Length; i++) {
  1303. // TODO: this code is duplicated, hotkey should be part of the MenuBarItem
  1304. var mi = Menus [i];
  1305. int p = mi.Title.IndexOf ('_');
  1306. if (p != -1 && p + 1 < mi.Title.RuneCount) {
  1307. if (Char.ToUpperInvariant ((char)mi.Title [p + 1]) == c) {
  1308. ProcessMenu (i, mi);
  1309. return true;
  1310. }
  1311. }
  1312. }
  1313. return false;
  1314. }
  1315. internal bool FindAndOpenMenuByShortCut (KeyEvent kb, MenuItem [] children = null)
  1316. {
  1317. if (children == null) {
  1318. children = Menus;
  1319. }
  1320. var key = kb.KeyValue;
  1321. var keys = GetModifiersKey (kb);
  1322. key |= (int)keys;
  1323. //if (kb.IsShift) {
  1324. // key |= (int)Key.ShiftMask;
  1325. //}
  1326. //if (kb.IsAlt) {
  1327. // key |= unchecked((int)Key.AltMask);
  1328. //}
  1329. for (int i = 0; i < children.Length; i++) {
  1330. var mi = children [i];
  1331. if (mi == null) {
  1332. continue;
  1333. }
  1334. if ((!(mi is MenuBarItem mbiTopLevel) || mbiTopLevel.IsTopLevel) && mi.ShortCut != Key.ControlSpace && mi.ShortCut == (Key)key) {
  1335. var action = mi.Action;
  1336. if (action != null) {
  1337. Application.MainLoop.AddIdle (() => {
  1338. action ();
  1339. return false;
  1340. });
  1341. }
  1342. return true;
  1343. }
  1344. if (mi is MenuBarItem menuBarItem && !menuBarItem.IsTopLevel && FindAndOpenMenuByShortCut (kb, menuBarItem.Children)) {
  1345. return true;
  1346. }
  1347. }
  1348. return false;
  1349. }
  1350. private void ProcessMenu (int i, MenuBarItem mi)
  1351. {
  1352. if (mi.IsTopLevel) {
  1353. var menu = new Menu (this, i, 0, mi);
  1354. menu.Run (mi.Action);
  1355. menu.Dispose ();
  1356. } else {
  1357. openedByHotKey = true;
  1358. Application.GrabMouse (this);
  1359. selected = i;
  1360. OpenMenu (i);
  1361. if (!SelectEnabledItem (openCurrentMenu.barItems.Children, openCurrentMenu.current, out openCurrentMenu.current)) {
  1362. CloseMenu ();
  1363. }
  1364. openCurrentMenu.CheckSubMenu ();
  1365. }
  1366. }
  1367. ///<inheritdoc/>
  1368. public override bool ProcessHotKey (KeyEvent kb)
  1369. {
  1370. if (kb.Key == Key.F9) {
  1371. if (!IsMenuOpen)
  1372. OpenMenu ();
  1373. else
  1374. CloseAllMenus ();
  1375. return true;
  1376. }
  1377. // To ncurses simulate a AltMask key pressing Alt+Space because
  1378. // it can�t detect an alone special key down was pressed.
  1379. if (kb.IsAlt && kb.Key == Key.AltMask && openMenu == null) {
  1380. OnKeyDown (kb);
  1381. OnKeyUp (kb);
  1382. return true;
  1383. } else if (kb.IsAlt && !kb.IsCtrl && !kb.IsShift) {
  1384. if (FindAndOpenMenuByHotkey (kb)) return true;
  1385. }
  1386. //var kc = kb.KeyValue;
  1387. return base.ProcessHotKey (kb);
  1388. }
  1389. ///<inheritdoc/>
  1390. public override bool ProcessKey (KeyEvent kb)
  1391. {
  1392. switch (kb.Key) {
  1393. case Key.CursorLeft:
  1394. selected--;
  1395. if (selected < 0)
  1396. selected = Menus.Length - 1;
  1397. break;
  1398. case Key.CursorRight:
  1399. selected = (selected + 1) % Menus.Length;
  1400. break;
  1401. case Key.Esc:
  1402. case Key.ControlC:
  1403. //TODO: Running = false;
  1404. CloseMenu ();
  1405. if (openedByAltKey) {
  1406. openedByAltKey = false;
  1407. LastFocused.SetFocus ();
  1408. }
  1409. break;
  1410. case Key.CursorDown:
  1411. case Key.Enter:
  1412. if (selected > -1) {
  1413. ProcessMenu (selected, Menus [selected]);
  1414. }
  1415. break;
  1416. default:
  1417. var key = kb.KeyValue;
  1418. if ((key >= 'a' && key <= 'z') || (key >= 'A' && key <= 'Z') || (key >= '0' && key <= '9')) {
  1419. char c = Char.ToUpper ((char)key);
  1420. if (selected == -1 || Menus [selected].IsTopLevel)
  1421. return false;
  1422. foreach (var mi in Menus [selected].Children) {
  1423. if (mi == null)
  1424. continue;
  1425. int p = mi.Title.IndexOf ('_');
  1426. if (p != -1 && p + 1 < mi.Title.RuneCount) {
  1427. if (mi.Title [p + 1] == c) {
  1428. Selected (mi);
  1429. return true;
  1430. }
  1431. }
  1432. }
  1433. }
  1434. return false;
  1435. }
  1436. SetNeedsDisplay ();
  1437. return true;
  1438. }
  1439. ///<inheritdoc/>
  1440. public override bool ProcessColdKey (KeyEvent kb)
  1441. {
  1442. return FindAndOpenMenuByShortCut (kb);
  1443. }
  1444. ///<inheritdoc/>
  1445. public override bool MouseEvent (MouseEvent me)
  1446. {
  1447. if (!handled && !HandleGrabView (me, this)) {
  1448. return false;
  1449. }
  1450. handled = false;
  1451. if (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked || me.Flags == MouseFlags.Button1TripleClicked || me.Flags == MouseFlags.Button1Clicked ||
  1452. (me.Flags == MouseFlags.ReportMousePosition && selected > -1) ||
  1453. (me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition) && selected > -1)) {
  1454. int pos = 1;
  1455. int cx = me.X;
  1456. for (int i = 0; i < Menus.Length; i++) {
  1457. if (cx >= pos && cx < pos + 1 + Menus [i].TitleLength + Menus [i].Help.RuneCount + 2) {
  1458. if (me.Flags == MouseFlags.Button1Clicked) {
  1459. if (Menus [i].IsTopLevel) {
  1460. var menu = new Menu (this, i, 0, Menus [i]);
  1461. menu.Run (Menus [i].Action);
  1462. menu.Dispose ();
  1463. }
  1464. } else if (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked || me.Flags == MouseFlags.Button1TripleClicked) {
  1465. if (IsMenuOpen && !Menus [i].IsTopLevel) {
  1466. CloseAllMenus ();
  1467. } else if (!Menus [i].IsTopLevel) {
  1468. Activate (i);
  1469. }
  1470. } else if (selected != i && selected > -1 && (me.Flags == MouseFlags.ReportMousePosition ||
  1471. me.Flags == MouseFlags.Button1Pressed && me.Flags == MouseFlags.ReportMousePosition)) {
  1472. if (IsMenuOpen) {
  1473. CloseMenu (true, false);
  1474. Activate (i);
  1475. }
  1476. } else {
  1477. if (IsMenuOpen)
  1478. Activate (i);
  1479. }
  1480. return true;
  1481. }
  1482. pos += 1 + Menus [i].TitleLength + 2;
  1483. }
  1484. }
  1485. return false;
  1486. }
  1487. internal bool handled;
  1488. internal bool HandleGrabView (MouseEvent me, View current)
  1489. {
  1490. if (Application.mouseGrabView != null) {
  1491. if (me.View is MenuBar || me.View is Menu) {
  1492. if (me.View != current) {
  1493. Application.UngrabMouse ();
  1494. var v = me.View;
  1495. Application.GrabMouse (v);
  1496. var newxy = v.ScreenToView (me.X, me.Y);
  1497. var nme = new MouseEvent () {
  1498. X = newxy.X,
  1499. Y = newxy.Y,
  1500. Flags = me.Flags,
  1501. OfX = me.X - newxy.X,
  1502. OfY = me.Y - newxy.Y,
  1503. View = v
  1504. };
  1505. v.MouseEvent (nme);
  1506. return false;
  1507. }
  1508. } else if (!(me.View is MenuBar || me.View is Menu) && (me.Flags.HasFlag (MouseFlags.Button1Clicked) ||
  1509. me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked || me.Flags == MouseFlags.Button1TripleClicked)) {
  1510. Application.UngrabMouse ();
  1511. CloseAllMenus ();
  1512. handled = false;
  1513. return false;
  1514. } else {
  1515. handled = false;
  1516. return false;
  1517. }
  1518. } else if (!IsMenuOpen && (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked || me.Flags == MouseFlags.Button1TripleClicked || me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition))) {
  1519. Application.GrabMouse (current);
  1520. } else if (IsMenuOpen && (me.View is MenuBar || me.View is Menu)) {
  1521. Application.GrabMouse (me.View);
  1522. } else {
  1523. handled = false;
  1524. return false;
  1525. }
  1526. //if (me.View != this && me.Flags != MouseFlags.Button1Pressed)
  1527. // return true;
  1528. //else if (me.View != this && me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked) {
  1529. // Application.UngrabMouse ();
  1530. // host.CloseAllMenus ();
  1531. // return true;
  1532. //}
  1533. //if (!(me.View is MenuBar) && !(me.View is Menu) && me.Flags != MouseFlags.Button1Pressed))
  1534. // return false;
  1535. //if (Application.mouseGrabView != null) {
  1536. // if (me.View is MenuBar || me.View is Menu) {
  1537. // me.X -= me.OfX;
  1538. // me.Y -= me.OfY;
  1539. // me.View.MouseEvent (me);
  1540. // return true;
  1541. // } else if (!(me.View is MenuBar || me.View is Menu) && me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked) {
  1542. // Application.UngrabMouse ();
  1543. // CloseAllMenus ();
  1544. // }
  1545. //} else if (!isMenuClosed && selected == -1 && me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked) {
  1546. // Application.GrabMouse (this);
  1547. // return true;
  1548. //}
  1549. //if (Application.mouseGrabView != null) {
  1550. // if (Application.mouseGrabView == me.View && me.View == current) {
  1551. // me.X -= me.OfX;
  1552. // me.Y -= me.OfY;
  1553. // } else if (me.View != current && me.View is MenuBar && me.View is Menu) {
  1554. // Application.UngrabMouse ();
  1555. // Application.GrabMouse (me.View);
  1556. // } else if (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked) {
  1557. // Application.UngrabMouse ();
  1558. // CloseMenu ();
  1559. // }
  1560. //} else if ((!isMenuClosed && selected > -1)) {
  1561. // Application.GrabMouse (current);
  1562. //}
  1563. handled = true;
  1564. return true;
  1565. }
  1566. }
  1567. }