123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520 |
- //
- // Menu.cs: application menus and submenus
- //
- // Authors:
- // Miguel de Icaza ([email protected])
- //
- // TODO:
- // Add accelerator support, but should also support chords (ShortCut in MenuItem)
- // Allow menus inside menus
- using System;
- using NStack;
- using System.Linq;
- namespace Terminal.Gui {
- /// <summary>
- /// A menu item has a title, an associated help text, and an action to execute on activation.
- /// </summary>
- public class MenuItem {
- /// <summary>
- /// Initializes a new <see cref="T:Terminal.Gui.MenuItem"/>.
- /// </summary>
- /// <param name="title">Title for the menu item.</param>
- /// <param name="help">Help text to display.</param>
- /// <param name="action">Action to invoke when the menu item is activated.</param>
- public MenuItem (ustring title, string help, Action action)
- {
- Title = title ?? "";
- Help = help ?? "";
- Action = action;
- bool nextIsHot = false;
- foreach (var x in Title) {
- if (x == '_')
- nextIsHot = true;
- else {
- if (nextIsHot) {
- HotKey = Char.ToUpper ((char)x);
- break;
- }
- nextIsHot = false;
- }
- }
- }
- //
- //
- /// <summary>
- /// The hotkey is used when the menu is active, the shortcut can be triggered when the menu is not active.
- /// For example HotKey would be "N" when the File Menu is open (assuming there is a "_New" entry
- /// if the ShortCut is set to "Control-N", this would be a global hotkey that would trigger as well
- /// </summary>
- public Rune HotKey;
- /// <summary>
- /// This is the global setting that can be used as a global shortcut to invoke the action on the menu.
- /// </summary>
- public Key ShortCut;
- /// <summary>
- /// Gets or sets the title.
- /// </summary>
- /// <value>The title.</value>
- public ustring Title { get; set; }
- /// <summary>
- /// Gets or sets the help text for the menu item.
- /// </summary>
- /// <value>The help text.</value>
- public ustring Help { get; set; }
- /// <summary>
- /// Gets or sets the action to be invoked when the menu is triggered
- /// </summary>
- /// <value>Method to invoke.</value>
- public Action Action { get; set; }
- internal int Width => Title.Length + Help.Length + 1 + 2;
- }
- /// <summary>
- /// A menu bar item contains other menu items.
- /// </summary>
- public class MenuBarItem {
- public MenuBarItem (ustring title, MenuItem [] children)
- {
- SetTitle (title ?? "");
- Children = children;
- }
- void SetTitle (ustring title)
- {
- if (title == null)
- title = "";
- Title = title;
- int len = 0;
- foreach (var ch in Title) {
- if (ch == '_')
- continue;
- len++;
- }
- TitleLength = len;
- }
- /// <summary>
- /// Gets or sets the title to display.
- /// </summary>
- /// <value>The title.</value>
- public ustring Title { get; set; }
- /// <summary>
- /// Gets or sets the children for this MenuBarItem
- /// </summary>
- /// <value>The children.</value>
- public MenuItem [] Children { get; set; }
- internal int TitleLength { get; private set; }
- }
- class Menu : View {
- MenuBarItem barItems;
- MenuBar host;
- int current;
- static Rect MakeFrame (int x, int y, MenuItem [] items)
- {
- int maxW = 0;
- foreach (var item in items) {
- var l = item.Width;
- maxW = Math.Max (l, maxW);
- }
- return new Rect (x, y, maxW + 2, items.Length + 2);
- }
- public Menu (MenuBar host, int x, int y, MenuBarItem barItems) : base (MakeFrame (x, y, barItems.Children))
- {
- this.barItems = barItems;
- this.host = host;
- current = -1;
- for (int i = 0; i < barItems.Children.Length; i++) {
- if (barItems.Children[i] != null) {
- current = i;
- break;
- }
- }
- ColorScheme = Colors.Menu;
- CanFocus = true;
- }
- public override void Redraw (Rect region)
- {
- Driver.SetAttribute (ColorScheme.Normal);
- DrawFrame (region, padding: 0, fill: true);
- for (int i = 0; i < barItems.Children.Length; i++){
- var item = barItems.Children [i];
- Move (1, i+1);
- Driver.SetAttribute (item == null ? Colors.Base.Focus : i == current ? ColorScheme.Focus : ColorScheme.Normal);
- for (int p = 0; p < Frame.Width-2; p++)
- if (item == null)
- Driver.AddRune (Driver.HLine);
- else
- Driver.AddRune (' ');
- if (item == null)
- continue;
- Move (2, i + 1);
- DrawHotString (item.Title,
- i == current? ColorScheme.HotFocus : ColorScheme.HotNormal,
- i == current ? ColorScheme.Focus : ColorScheme.Normal);
- // The help string
- var l = item.Help.Length;
- Move (Frame.Width - l - 2, 1 + i);
- Driver.AddStr (item.Help);
- }
- }
- public override void PositionCursor ()
- {
- Move (2, 1 + current);
- }
- void Run (Action action)
- {
- if (action == null)
- return;
-
- Application.MainLoop.AddIdle (() => {
- action ();
- return false;
- });
- }
- public override bool ProcessKey (KeyEvent kb)
- {
- switch (kb.Key) {
- case Key.CursorUp:
- if (current == -1)
- break;
- do {
- current--;
- if (current < 0)
- current = barItems.Children.Length - 1;
- } while (barItems.Children [current] == null);
- SetNeedsDisplay ();
- break;
- case Key.CursorDown:
- do {
- current++;
- if (current == barItems.Children.Length)
- current = 0;
- } while (barItems.Children [current] == null);
- SetNeedsDisplay ();
- break;
- case Key.CursorLeft:
- host.PreviousMenu ();
- break;
- case Key.CursorRight:
- host.NextMenu ();
- break;
- case Key.Esc:
- host.CloseMenu ();
- break;
- case Key.Enter:
- host.CloseMenu ();
- Run (barItems.Children [current].Action);
- break;
- default:
- // TODO: rune-ify
- if (Char.IsLetterOrDigit ((char)kb.KeyValue)) {
- var x = Char.ToUpper ((char)kb.KeyValue);
- foreach (var item in barItems.Children) {
- if (item.HotKey == x) {
- host.CloseMenu ();
- Run (item.Action);
- return true;
- }
- }
- }
- break;
- }
- return true;
- }
- public override bool MouseEvent(MouseEvent me)
- {
- if (me.Flags == MouseFlags.Button1Clicked || me.Flags == MouseFlags.Button1Released) {
- if (me.Y < 1)
- return true;
- var item = me.Y - 1;
- if (item >= barItems.Children.Length)
- return true;
- host.CloseMenu ();
- Run (barItems.Children [item].Action);
- return true;
- }
- if (me.Flags == MouseFlags.Button1Pressed) {
- if (me.Y < 1)
- return true;
- if (me.Y - 1 >= barItems.Children.Length)
- return true;
- current = me.Y - 1;
- SetNeedsDisplay ();
- return true;
- }
- return false;
- }
- }
- /// <summary>
- /// A menu bar for your application.
- /// </summary>
- public class MenuBar : View {
- /// <summary>
- /// The menus that were defined when the menubar was created. This can be updated if the menu is not currently visible.
- /// </summary>
- /// <value>The menu array.</value>
- public MenuBarItem [] Menus { get; set; }
- int selected;
- Action action;
- /// <summary>
- /// Initializes a new instance of the <see cref="T:Terminal.Gui.MenuBar"/> class with the specified set of toplevel menu items.
- /// </summary>
- /// <param name="menus">Individual menu items, if one of those contains a null, then a separator is drawn.</param>
- public MenuBar (MenuBarItem [] menus) : base ()
- {
- X = 0;
- Y = 0;
- Width = Dim.Fill ();
- Height = 1;
- Menus = menus;
- CanFocus = false;
- selected = -1;
- ColorScheme = Colors.Menu;
- }
- public override void Redraw (Rect region)
- {
- Move (0, 0);
- Driver.SetAttribute (Colors.Base.Focus);
- for (int i = 0; i < Frame.Width; i++)
- Driver.AddRune (' ');
- Move (1, 0);
- int pos = 1;
- for (int i = 0; i < Menus.Length; i++) {
- var menu = Menus [i];
- Move (pos, 0);
- Attribute hotColor, normalColor;
- if (i == selected){
- hotColor = i == selected ? ColorScheme.HotFocus : ColorScheme.HotNormal;
- normalColor = i == selected ? ColorScheme.Focus : ColorScheme.Normal;
- } else {
- hotColor = Colors.Base.Focus;
- normalColor = Colors.Base.Focus;
- }
- DrawHotString (" " + menu.Title + " " + " ", hotColor, normalColor);
- pos += menu.TitleLength+ 3;
- }
- PositionCursor ();
- }
- public override void PositionCursor ()
- {
- int pos = 0;
- for (int i = 0; i < Menus.Length; i++) {
- if (i == selected) {
- pos++;
- Move (pos, 0);
- return;
- } else {
- pos += Menus [i].TitleLength + 4;
- }
- }
- Move (0, 0);
- }
- void Selected (MenuItem item)
- {
- // TODO: Running = false;
- action = item.Action;
- }
- public event EventHandler OnOpenMenu;
- Menu openMenu;
- View previousFocused;
- void OpenMenu (int index)
- {
- OnOpenMenu?.Invoke(this, null);
- if (openMenu != null)
- SuperView.Remove (openMenu);
-
- int pos = 0;
- for (int i = 0; i < index; i++)
- pos += Menus [i].Title.Length + 3;
- openMenu = new Menu (this, pos, 1, Menus [index]);
- SuperView.Add (openMenu);
- SuperView.SetFocus (openMenu);
- }
- // Starts the menu from a hotkey
- void StartMenu ()
- {
- if (openMenu != null)
- return;
- selected = 0;
- SetNeedsDisplay ();
- previousFocused = SuperView.Focused;
- OpenMenu (selected);
- }
- // Activates the menu, handles either first focus, or activating an entry when it was already active
- // For mouse events.
- void Activate (int idx)
- {
- selected = idx;
- if (openMenu == null)
- previousFocused = SuperView.Focused;
-
- OpenMenu (idx);
- SetNeedsDisplay ();
- }
- internal void CloseMenu ()
- {
- selected = -1;
- SetNeedsDisplay ();
- SuperView.Remove (openMenu);
- previousFocused?.SuperView?.SetFocus (previousFocused);
- openMenu = null;
- }
- internal void PreviousMenu ()
- {
- if (selected <= 0)
- selected = Menus.Length - 1;
- else
- selected--;
- OpenMenu (selected);
- }
- internal void NextMenu ()
- {
- if (selected == -1)
- selected = 0;
- else if (selected + 1 == Menus.Length)
- selected = 0;
- else
- selected++;
- OpenMenu (selected);
- }
- internal bool FindAndOpenMenuByHotkey(KeyEvent kb)
- {
- int pos = 0;
- var c = ((uint)kb.Key & (uint)Key.CharMask);
- for (int i = 0; i < Menus.Length; i++)
- {
- // TODO: this code is duplicated, hotkey should be part of the MenuBarItem
- var mi = Menus[i];
- int p = mi.Title.IndexOf('_');
- if (p != -1 && p + 1 < mi.Title.Length) {
- if (mi.Title[p + 1] == c) {
- OpenMenu(i);
- return true;
- }
- }
- }
- return false;
- }
- public override bool ProcessHotKey (KeyEvent kb)
- {
- if (kb.Key == Key.F9) {
- StartMenu ();
- return true;
- }
- if (kb.IsAlt)
- {
- if (FindAndOpenMenuByHotkey(kb)) return true;
- }
- var kc = kb.KeyValue;
- return base.ProcessHotKey (kb);
- }
- public override bool ProcessKey (KeyEvent kb)
- {
- switch (kb.Key) {
- case Key.CursorLeft:
- selected--;
- if (selected < 0)
- selected = Menus.Length - 1;
- break;
- case Key.CursorRight:
- selected = (selected + 1) % Menus.Length;
- break;
- case Key.Esc:
- case Key.ControlC:
- //TODO: Running = false;
- break;
- default:
- var key = kb.KeyValue;
- if ((key >= 'a' && key <= 'z') || (key >= 'A' && key <= 'Z') || (key >= '0' && key <= '9')) {
- char c = Char.ToUpper ((char)key);
- if (Menus [selected].Children == null)
- return false;
- foreach (var mi in Menus [selected].Children) {
- int p = mi.Title.IndexOf ('_');
- if (p != -1 && p + 1 < mi.Title.Length) {
- if (mi.Title [p + 1] == c) {
- Selected (mi);
- return true;
- }
- }
- }
- }
- return false;
- }
- SetNeedsDisplay ();
- return true;
- }
- public override bool MouseEvent(MouseEvent me)
- {
- if (me.Flags == MouseFlags.Button1Clicked) {
- int pos = 1;
- int cx = me.X;
- for (int i = 0; i < Menus.Length; i++) {
- if (cx > pos && me.X < pos + 1 + Menus [i].TitleLength) {
- Activate (i);
- return true;
- }
- pos += 2 + Menus [i].TitleLength + 1;
- }
- }
- return false;
- }
- }
- }
|