diff --git a/Example/demo.cs b/Example/demo.cs index dbba7ae12..b7bf1e79e 100644 --- a/Example/demo.cs +++ b/Example/demo.cs @@ -3,7 +3,10 @@ using System; using Mono.Terminal; using System.Collections; using System.Collections.Generic; - +using System.Diagnostics; +using System.Globalization; +using System.Reflection; +using NStack; static class Demo { class Box10x : View { @@ -42,6 +45,9 @@ static class Demo { Rune r; switch (x % 3) { case 0: + Driver.AddRune (y.ToString ().ToCharArray (0, 1) [0]); + if (y > 9) + Driver.AddRune (y.ToString ().ToCharArray (1, 1) [0]); r = '.'; break; case 1: @@ -60,11 +66,15 @@ static class Demo { static void ShowTextAlignments (View container) { + int i = 0; + string txt = "Hello world, how are you doing today"; container.Add ( - new Label (new Rect (0, 0, 40, 3), "1-Hello world, how are you doing today") { TextAlignment = TextAlignment.Left }, - new Label (new Rect (0, 4, 40, 3), "2-Hello world, how are you doing today") { TextAlignment = TextAlignment.Right }, - new Label (new Rect (0, 8, 40, 3), "3-Hello world, how are you doing today") { TextAlignment = TextAlignment.Centered }, - new Label (new Rect (0, 12, 40, 3), "4-Hello world, how are you doing today") { TextAlignment = TextAlignment.Justified }); + new FrameView (new Rect (75, 1, txt.Length + 6, 20), "Text Alignments") { + new Label(new Rect(0, 1, 40, 3), $"{i+1}-{txt}") { TextAlignment = TextAlignment.Left }, + new Label(new Rect(0, 5, 40, 3), $"{i+2}-{txt}") { TextAlignment = TextAlignment.Right }, + new Label(new Rect(0, 9, 40, 3), $"{i+3}-{txt}") { TextAlignment = TextAlignment.Centered }, + new Label(new Rect(0, 13, 40, 3), $"{i+4}-{txt}") { TextAlignment = TextAlignment.Justified } + }); } static void ShowEntries (View container) @@ -75,9 +85,11 @@ static class Demo { ShowVerticalScrollIndicator = true, ShowHorizontalScrollIndicator = true }; - +#if false scrollView.Add (new Box10x (0, 0)); - //scrollView.Add (new Filler (new Rect (0, 0, 40, 40))); +#else + scrollView.Add (new Filler (new Rect (0, 0, 40, 40))); +#endif // This is just to debug the visuals of the scrollview when small var scrollView2 = new ScrollView (new Rect (72, 10, 3, 3)) { @@ -142,10 +154,10 @@ static class Demo { new Button (10, 19, "Cancel"), new TimeField (3, 20, DateTime.Now), new TimeField (23, 20, DateTime.Now, true), - new DateField(3, 22, DateTime.Now), - new DateField(23, 22, DateTime.Now, true), progress, - new Label (3, 24, "Press F9 (on Unix, ESC+9 is an alias) to activate the menubar") + new Label (3, 24, "Press F9 (on Unix, ESC+9 is an alias) to activate the menubar"), + menuKeysStyle, + menuAutoMouseNav ); @@ -166,10 +178,11 @@ static class Demo { // // Creates a nested editor - static void Editor(Toplevel top) { + static void Editor (Toplevel top) + { var tframe = top.Frame; - var ntop = new Toplevel(tframe); - var menu = new MenuBar(new MenuBarItem[] { + var ntop = new Toplevel (tframe); + var menu = new MenuBar (new MenuBarItem [] { new MenuBarItem ("_File", new MenuItem [] { new MenuItem ("_Close", "", () => {Application.RequestStop ();}), }), @@ -179,25 +192,25 @@ static class Demo { new MenuItem ("_Paste", "", null) }), }); - ntop.Add(menu); + ntop.Add (menu); string fname = null; - foreach (var s in new[] { "/etc/passwd", "c:\\windows\\win.ini" }) - if (System.IO.File.Exists(s)) { + foreach (var s in new [] { "/etc/passwd", "c:\\windows\\win.ini" }) + if (System.IO.File.Exists (s)) { fname = s; break; } - var win = new Window(fname ?? "Untitled") { + var win = new Window (fname ?? "Untitled") { X = 0, Y = 1, - Width = Dim.Fill(), - Height = Dim.Fill() + Width = Dim.Fill (), + Height = Dim.Fill () }; - ntop.Add(win); + ntop.Add (win); + + var text = new TextView (new Rect (0, 0, tframe.Width - 2, tframe.Height - 3)); - var text = new TextView(new Rect(0, 0, tframe.Width - 2, tframe.Height - 3)); - if (fname != null) text.Text = System.IO.File.ReadAllText (fname); win.Add (text); @@ -213,7 +226,7 @@ static class Demo { static void Close () { - MessageBox.ErrorQuery (50, 5, "Error", "There is nothing to close", "Ok"); + MessageBox.ErrorQuery (50, 7, "Error", "There is nothing to close", "Ok"); } // Watch what happens when I try to introduce a newline after the first open brace @@ -222,13 +235,10 @@ static class Demo { public static void Open () { - var d = new OpenDialog ("Open", "Open a file") { - AllowsMultipleSelection = true - }; + var d = new OpenDialog ("Open", "Open a file"); Application.Run (d); - if (!d.Canceled) - MessageBox.Query(50, 7, "Selected File", string.Join(", ", d.FilePaths), "Ok"); + MessageBox.Query (50, 7, "Selected File", string.Join (", ", d.FilePaths), "Ok"); } public static void ShowHex (Toplevel top) @@ -259,9 +269,63 @@ static class Demo { }; win.Add (hex); Application.Run (ntop); - + } + public class MenuItemDetails : MenuItem { + ustring title; + string help; + Action action; + + public MenuItemDetails (ustring title, string help, Action action) : base (title, help, action) + { + this.title = title; + this.help = help; + this.action = action; + } + + public static MenuItemDetails Instance (MenuItem mi) + { + return (MenuItemDetails)mi.GetMenuItem (); + } + } + + public delegate MenuItem MenuItemDelegate (MenuItemDetails menuItem); + + public static void ShowMenuItem (MenuItem mi) + { + BindingFlags flags = BindingFlags.Public | BindingFlags.Static; + MethodInfo minfo = typeof (MenuItemDetails).GetMethod ("Instance", flags); + MenuItemDelegate mid = (MenuItemDelegate)Delegate.CreateDelegate (typeof (MenuItemDelegate), minfo); + MessageBox.Query (70, 7, mi.Title.ToString (), + $"{mi.Title.ToString ()} selected. Is from submenu: {mi.GetMenuBarItem ()}", "Ok"); + } + + private static void MenuKeysStyle_Toggled (object sender, EventArgs e) + { + menu.UseKeysUpDownAsKeysLeftRight = menuKeysStyle.Checked; + } + + private static void MenuAutoMouseNav_Toggled (object sender, EventArgs e) + { + menu.WantMousePositionReports = menuAutoMouseNav.Checked; + } + + //private static TextField GetTextFieldSelText (View vt) + //{ + // TextField textField; + // foreach (View v in vt.Subviews) { + // if (v is TextField && ((TextField)v).SelText != "") + // return v as TextField; + // else + // textField = GetTextFieldSelText (v); + // if (textField != null) + // return textField; + // } + // return null; + //} + + #region Selection Demo static void ListSelectionDemo () @@ -300,14 +364,19 @@ static class Demo { public static Label ml; + public static MenuBar menu; + public static CheckBox menuKeysStyle; + public static CheckBox menuAutoMouseNav; static void Main () { + if (Debugger.IsAttached) + CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.GetCultureInfo ("en-US"); + //Application.UseSystemConsole = true; Application.Init (); - + var top = Application.Top; - - var tframe = top.Frame; + //Open (); #if true var win = new Window ("Hello") { @@ -317,27 +386,64 @@ static class Demo { Height = Dim.Fill () }; #else + var tframe = top.Frame; + var win = new Window (new Rect (0, 1, tframe.Width, tframe.Height - 1), "Hello"); #endif - var menu = new MenuBar (new MenuBarItem [] { + MenuItemDetails [] menuItems = { + new MenuItemDetails ("F_ind", "", null), + new MenuItemDetails ("_Replace", "", null), + new MenuItemDetails ("_Item1", "", null), + new MenuItemDetails ("_Not From Sub Menu", "", null) + }; + + menuItems [0].Action = () => ShowMenuItem (menuItems [0]); + menuItems [1].Action = () => ShowMenuItem (menuItems [1]); + menuItems [2].Action = () => ShowMenuItem (menuItems [2]); + menuItems [3].Action = () => ShowMenuItem (menuItems [3]); + + menu = new MenuBar (new MenuBarItem [] { new MenuBarItem ("_File", new MenuItem [] { new MenuItem ("Text Editor Demo", "", () => { Editor (top); }), new MenuItem ("_New", "Creates new file", NewFile), new MenuItem ("_Open", "", Open), new MenuItem ("_Hex", "", () => ShowHex (top)), new MenuItem ("_Close", "", () => Close ()), + new MenuItem ("_Disabled", "", () => { }, () => false), + null, new MenuItem ("_Quit", "", () => { if (Quit ()) top.Running = false; }) }), new MenuBarItem ("_Edit", new MenuItem [] { new MenuItem ("_Copy", "", null), new MenuItem ("C_ut", "", null), - new MenuItem ("_Paste", "", null) + new MenuItem ("_Paste", "", null), + new MenuItem ("_Find and Replace", + new MenuBarItem (new MenuItem[] {menuItems [0], menuItems [1] })), + menuItems[3] }), - new MenuBarItem ("_List Demos", new MenuItem [] { + new MenuBarItem ("_List Demos", new MenuItem [] { new MenuItem ("Select Items", "", ListSelectionDemo), }), + new MenuBarItem ("Test Menu and SubMenus", new MenuItem [] { + new MenuItem ("SubMenu1Item1", + new MenuBarItem (new MenuItem[] { + new MenuItem ("SubMenu2Item1", + new MenuBarItem (new MenuItem [] { + new MenuItem ("SubMenu3Item1", + new MenuBarItem (new MenuItem [] { menuItems [2] }) + ) + }) + ) + }) + ) + }), }); + menuKeysStyle = new CheckBox (3, 25, "UseKeysUpDownAsKeysLeftRight", true); + menuKeysStyle.Toggled += MenuKeysStyle_Toggled; + menuAutoMouseNav = new CheckBox (40, 25, "UseMenuAutoNavigation", true); + menuAutoMouseNav.Toggled += MenuAutoMouseNav_Toggled; + ShowEntries (win); int count = 0; @@ -346,14 +452,14 @@ static class Demo { ml.Text = $"Mouse: ({me.X},{me.Y}) - {me.Flags} {count++}"; }; - + var test = new Label (3, 18, "Se iniciará el análisis"); win.Add (test); win.Add (ml); - - // ShowTextAlignments (win); + + ShowTextAlignments (win); top.Add (win); top.Add (menu); Application.Run (); } -} \ No newline at end of file +} diff --git a/Terminal.Gui/Core.cs b/Terminal.Gui/Core.cs index 113f7dcde..755730193 100644 --- a/Terminal.Gui/Core.cs +++ b/Terminal.Gui/Core.cs @@ -228,6 +228,8 @@ namespace Terminal.Gui { View container = null; View focused = null; Direction focusDirection; + public event EventHandler OnEnter; + public event EventHandler OnLeave; internal Direction FocusDirection { get => SuperView?.FocusDirection ?? focusDirection; @@ -826,11 +828,16 @@ namespace Terminal.Gui { } internal set { if (base.HasFocus != value) + if (value == true) + OnEnter?.Invoke (this, new EventArgs ()); + else + OnLeave?.Invoke (this, new EventArgs ()); SetNeedsDisplay (); base.HasFocus = value; // Remove focus down the chain of subviews if focus is removed if (value == false && focused != null) { + OnLeave?.Invoke (focused, new EventArgs ()); focused.HasFocus = false; focused = null; } diff --git a/Terminal.Gui/Drivers/ConsoleDriver.cs b/Terminal.Gui/Drivers/ConsoleDriver.cs index 684245cc6..fffb90ee4 100644 --- a/Terminal.Gui/Drivers/ConsoleDriver.cs +++ b/Terminal.Gui/Drivers/ConsoleDriver.cs @@ -161,6 +161,10 @@ namespace Terminal.Gui { /// public Attribute Normal { get { return _normal; } set { _normal = SetAttribute (value); } } + /// + /// The default color for text, when the view is disabled. + /// + public Attribute Disabled; /// /// The color for text when the view has the focus. /// diff --git a/Terminal.Gui/Drivers/CursesDriver.cs b/Terminal.Gui/Drivers/CursesDriver.cs index 6697579b0..0b9ce8cde 100644 --- a/Terminal.Gui/Drivers/CursesDriver.cs +++ b/Terminal.Gui/Drivers/CursesDriver.cs @@ -264,6 +264,7 @@ namespace Terminal.Gui { Colors.Menu.Focus = Curses.A_BOLD | MakeColor (Curses.COLOR_WHITE, Curses.COLOR_BLACK); Colors.Menu.HotNormal = Curses.A_BOLD | MakeColor (Curses.COLOR_YELLOW, Curses.COLOR_CYAN); Colors.Menu.Normal = Curses.A_BOLD | MakeColor (Curses.COLOR_WHITE, Curses.COLOR_CYAN); + Colors.Menu.Disabled = MakeColor(Curses.COLOR_WHITE, Curses.COLOR_CYAN); Colors.Dialog.Normal = MakeColor (Curses.COLOR_BLACK, Curses.COLOR_WHITE); Colors.Dialog.Focus = MakeColor (Curses.COLOR_BLACK, Curses.COLOR_CYAN); diff --git a/Terminal.Gui/Drivers/NetDriver.cs b/Terminal.Gui/Drivers/NetDriver.cs index 0487d0d7b..aec15a73c 100644 --- a/Terminal.Gui/Drivers/NetDriver.cs +++ b/Terminal.Gui/Drivers/NetDriver.cs @@ -144,6 +144,7 @@ namespace Terminal.Gui { Colors.Menu.Focus = MakeColor (ConsoleColor.White, ConsoleColor.Black); Colors.Menu.HotNormal = MakeColor (ConsoleColor.Yellow, ConsoleColor.Cyan); Colors.Menu.Normal = MakeColor (ConsoleColor.White, ConsoleColor.Cyan); + Colors.Menu.Disabled = MakeColor(ConsoleColor.DarkGray, ConsoleColor.Cyan); Colors.Dialog.Normal = MakeColor (ConsoleColor.Black, ConsoleColor.Gray); Colors.Dialog.Focus = MakeColor (ConsoleColor.Black, ConsoleColor.Cyan); diff --git a/Terminal.Gui/Drivers/WindowsDriver.cs b/Terminal.Gui/Drivers/WindowsDriver.cs index 616820144..42fdadf9f 100644 --- a/Terminal.Gui/Drivers/WindowsDriver.cs +++ b/Terminal.Gui/Drivers/WindowsDriver.cs @@ -813,7 +813,8 @@ namespace Terminal.Gui { Colors.Menu.Normal = MakeColor (ConsoleColor.White, ConsoleColor.Cyan); Colors.Menu.Focus = MakeColor (ConsoleColor.White, ConsoleColor.Black); Colors.Menu.HotNormal = MakeColor (ConsoleColor.Yellow, ConsoleColor.Cyan); - Colors.Menu.HotFocus = MakeColor (ConsoleColor.Yellow, ConsoleColor.Black); + Colors.Menu.Normal = MakeColor (ConsoleColor.White, ConsoleColor.Cyan); + Colors.Menu.Disabled = MakeColor(ConsoleColor.DarkGray, ConsoleColor.Cyan); Colors.Dialog.Normal = MakeColor (ConsoleColor.Black, ConsoleColor.Gray); Colors.Dialog.Focus = MakeColor (ConsoleColor.Black, ConsoleColor.Cyan); diff --git a/Terminal.Gui/Views/Menu.cs b/Terminal.Gui/Views/Menu.cs index ee120eb5f..d26b5fd67 100644 --- a/Terminal.Gui/Views/Menu.cs +++ b/Terminal.Gui/Views/Menu.cs @@ -1,515 +1,796 @@ -// -// Menu.cs: application menus and submenus -// -// Authors: -// Miguel de Icaza (miguel@gnome.org) -// -// 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 { - - /// - /// A menu item has a title, an associated help text, and an action to execute on activation. - /// - public class MenuItem { - - /// - /// Initializes a new . - /// - /// Title for the menu item. - /// Help text to display. - /// Action to invoke when the menu item is activated. - 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; - } - } - } - - // - // - - /// - /// 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 - /// - public Rune HotKey; - - /// - /// This is the global setting that can be used as a global shortcut to invoke the action on the menu. - /// - public Key ShortCut; - - /// - /// Gets or sets the title. - /// - /// The title. - public ustring Title { get; set; } - - /// - /// Gets or sets the help text for the menu item. - /// - /// The help text. - public ustring Help { get; set; } - - /// - /// Gets or sets the action to be invoked when the menu is triggered - /// - /// Method to invoke. - public Action Action { get; set; } - internal int Width => Title.Length + Help.Length + 1 + 2; - } - - /// - /// A menu bar item contains other menu items. - /// - 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; - } - - /// - /// Gets or sets the title to display. - /// - /// The title. - public ustring Title { get; set; } - - /// - /// Gets or sets the children for this MenuBarItem - /// - /// The children. - 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 = items.Max(z=>z?.Width) ?? 0; - - 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; - } - } - - /// - /// A menu bar for your application. - /// - public class MenuBar : View { - /// - /// The menus that were defined when the menubar was created. This can be updated if the menu is not currently visible. - /// - /// The menu array. - public MenuBarItem [] Menus { get; set; } - int selected; - Action action; - - - /// - /// Initializes a new instance of the class with the specified set of toplevel menu items. - /// - /// Individual menu items, if one of those contains a null, then a separator is drawn. - 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; +// +// Menu.cs: application menus and submenus +// +// Authors: +// Miguel de Icaza (miguel@gnome.org) +// +// TODO: +// Add accelerator support, but should also support chords (ShortCut in MenuItem) +// Allow menus inside menus + +using System; +using NStack; +using System.Linq; +using System.Collections.Generic; + +namespace Terminal.Gui { + + /// + /// A menu item has a title, an associated help text, and an action to execute on activation. + /// + public class MenuItem { + + /// + /// Initializes a new . + /// + /// Title for the menu item. + /// Help text to display. + /// Action to invoke when the menu item is activated. + /// Function to determine if the action can currently be executred. + public MenuItem (ustring title, string help, Action action, Func canExecute = null) + { + Title = title ?? ""; + Help = help ?? ""; + Action = action; + CanExecute = canExecute; + bool nextIsHot = false; + foreach (var x in Title) { + if (x == '_') + nextIsHot = true; + else { + if (nextIsHot) { + HotKey = Char.ToUpper ((char)x); + break; + } + nextIsHot = false; + } + } + } + + public MenuItem(ustring title, MenuBarItem subMenu) : this (title, "", null) + { + SubMenu = subMenu; + IsFromSubMenu = true; + } + + // + // + + /// + /// 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 + /// + public Rune HotKey; + + /// + /// This is the global setting that can be used as a global shortcut to invoke the action on the menu. + /// + public Key ShortCut; + + /// + /// Gets or sets the title. + /// + /// The title. + public ustring Title { get; set; } + + /// + /// Gets or sets the help text for the menu item. + /// + /// The help text. + public ustring Help { get; set; } + + /// + /// Gets or sets the action to be invoked when the menu is triggered + /// + /// Method to invoke. + public Action Action { get; set; } + + /// + /// Gets or sets the action to be invoked if the menu can be triggered + /// + /// Function to determine if action is ready to be executed. + public Func CanExecute { get; set; } + + /// + /// Shortcut to check if the menu item is enabled + /// + public bool IsEnabled () + { + return CanExecute == null ? true : CanExecute (); + } + + internal int Width => Title.Length + Help.Length + 1 + 2; + + /// + /// Gets or sets the parent for this MenuBarItem + /// + /// The parent. + internal MenuBarItem SubMenu { get; set; } + internal bool IsFromSubMenu { get; set; } + + /// + /// Merely a debugging aid to see the interaction with main + /// + public MenuItem GetMenuItem () + { + return this; + } + + /// + /// Merely a debugging aid to see the interaction with main + /// + public bool GetMenuBarItem () + { + return IsFromSubMenu; + } + } + + /// + /// A menu bar item contains other menu items. + /// + public class MenuBarItem { + public MenuBarItem (ustring title, MenuItem [] children) + { + SetTitle (title ?? ""); + Children = children; + } + + public MenuBarItem (MenuItem[] children) : this (new string (' ', GetMaxTitleLength (children)), children) + { + } + + private static int GetMaxTitleLength (MenuItem[] children) + { + int maxLength = 0; + foreach (var item in children) { + int len = GetMenuBarItemLength (item.Title); + if (len > maxLength) + maxLength = len; + item.IsFromSubMenu = true; + } + + return maxLength; + } + + void SetTitle (ustring title) + { + if (title == null) + title = ""; + Title = title; + TitleLength = GetMenuBarItemLength(Title); + } + + static int GetMenuBarItemLength(ustring title) + { + int len = 0; + foreach (var ch in title) { + if (ch == '_') + continue; + len++; + } + + return len; + } + + /// + /// Gets or sets the title to display. + /// + /// The title. + public ustring Title { get; set; } + + /// + /// Gets or sets the children for this MenuBarItem + /// + /// The children. + 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) { + if (item == null) continue; + 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; + WantMousePositionReports = host.WantMousePositionReports; + selectedSub = -1; + OnLeave += Menu_OnLeave; + } + + internal Attribute DetermineColorSchemeFor (MenuItem item, int index) + { + if (item != null) { + if (index == current) return ColorScheme.Focus; + if (!item.IsEnabled ()) return ColorScheme.Disabled; + } + return ColorScheme.Normal; + } + + 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]; + Driver.SetAttribute (item == null ? ColorScheme.Normal : i == current ? ColorScheme.Focus : ColorScheme.Normal); + if (item == null) { + Move (0, i + 1); + Driver.AddRune (Driver.LeftTee); + } else + Move (1, i+1); + + Driver.SetAttribute (DetermineColorSchemeFor (item, i)); + for (int p = 0; p < Frame.Width - 2; p++) + if (item == null) + Driver.AddRune (Driver.HLine); + else if (p == Frame.Width - 3 && barItems.Children[i].SubMenu != null) + Driver.AddRune ('>'); + else + Driver.AddRune (' '); + + if (item == null) { + Move (region.Right - 1, i + 1); + Driver.AddRune (Driver.RightTee); + continue; + } + + Move (2, i + 1); + if (!item.IsEnabled ()) + DrawHotString (item.Title, ColorScheme.Disabled, ColorScheme.Disabled); + else + 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); + } + PositionCursor (); + } + + public override void PositionCursor () + { + Move (2, 1 + current); + } + + void Run (Action action) + { + if (action == null) + return; + + CloseSubMenu (); + host.CloseMenu (); + + Application.MainLoop.AddIdle (() => { + action (); + return false; + }); + } + + public override bool ProcessKey (KeyEvent kb) + { + bool disabled; + switch (kb.Key) { + case Key.CursorUp: + if (current == -1) + break; + do { + disabled = false; + current--; + if (host.UseKeysUpDownAsKeysLeftRight) { + if (current == -1 && barItems.Children [current + 1].IsFromSubMenu && selectedSub > -1) { + current++; + PreviousMenu (); + break; + } + } + if (current < 0) + current = barItems.Children.Length - 1; + var item = barItems.Children [current]; + if (item == null || !item.IsEnabled ()) disabled = true; + } while (barItems.Children [current] == null || disabled); + SetNeedsDisplay (); + break; + case Key.CursorDown: + do { + current++; + disabled = false; + if (current == barItems.Children.Length) + current = 0; + var item = barItems.Children [current]; + if (item == null || !item.IsEnabled ()) disabled = true; + if (host.UseKeysUpDownAsKeysLeftRight && barItems.Children [current] != null && !disabled) { + CheckSubMenu (); + break; + } + } while (barItems.Children [current] == null || disabled); + SetNeedsDisplay (); + break; + case Key.CursorLeft: + PreviousMenu (); + break; + case Key.CursorRight: + NextMenu (); + break; + case Key.Esc: + CloseSubMenu (); + host.CloseMenu (); + break; + case Key.Enter: + CheckSubMenu (); + 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 == null) continue; + if (item.IsEnabled () && item.HotKey == x) { + host.CloseMenu (); + Run (item.Action); + return true; + } + } + } + break; + } + return true; + } + + public override bool MouseEvent(MouseEvent me) + { + bool disabled; + if (me.Flags == MouseFlags.Button1Clicked || me.Flags == MouseFlags.Button1Released) { + disabled = false; + if (me.Y < 1) + return true; + var meY = me.Y - 1; + if (meY >= barItems.Children.Length) + return true; + var item = barItems.Children [meY]; + if (item == null || !item.IsEnabled ()) disabled = true; + if (item != null && !disabled) + Run (barItems.Children [meY].Action); + return true; + } + if (me.Flags == MouseFlags.Button1Pressed || + me.Flags == MouseFlags.ReportMousePosition) { + disabled = false; + if (me.Y < 1) + return true; + if (me.Y - 1 >= barItems.Children.Length) + return true; + var item = barItems.Children [me.Y - 1]; + if (item == null || !item.IsEnabled ()) disabled = true; + if (item != null && !disabled) + current = me.Y - 1; + HasFocus = true; + SetNeedsDisplay (); + CheckSubMenu (); + return true; + } + return false; + } + + private void CheckSubMenu () + { + if (barItems.Children [current] == null) + return; + var subMenu = barItems.Children [current].SubMenu; + if (subMenu != null) { + int pos = -1; + if (openSubMenu != null) + pos = openSubMenu.FindIndex (o => o?.barItems == subMenu); + Activate (pos); + } else if (openSubMenu != null && !barItems.Children [current].IsFromSubMenu) + CloseSubMenu (); + } + + internal static List openSubMenu; + View previousSubFocused; + static int selectedSub; + + private void Menu_OnLeave (object sender, EventArgs e) + { + if (!host.isMenuOpening && !host.isMenuClosing) { + CloseSubMenu (); + if (openSubMenu == null) + host.CloseMenu (); + } + } + + void Activate (int idx) + { + selectedSub = idx; + if (openSubMenu == null || openSubMenu?.Count == 0 || (openSubMenu.Count > 0 && current == 0)) + previousSubFocused = SuperView.Focused; + + OpenSubMenu (idx); + SetNeedsDisplay (); + } + + void OpenSubMenu (int index) + { + host.isMenuOpening = true; + if (openSubMenu == null) + openSubMenu = new List (); + + if (index > -1) { + RemoveSubMenu (index); + } else { + openSubMenu.Add (new Menu (host, Frame.Left + Frame.Width, Frame.Top + 1 + current, barItems.Children [current].SubMenu)); + SuperView.Add (openSubMenu.Last ()); + } + selectedSub = openSubMenu.Count - 1; + SuperView.SetFocus (openSubMenu.Last ()); + host.isMenuOpening = false; + } + + private void RemoveSubMenu (int index) + { + if (openSubMenu == null) + return; + for (int i = openSubMenu.Count - 1; i > index; i--) { + host.isMenuClosing = true; + if (openSubMenu.Count - 1 > 0) + SuperView.SetFocus (openSubMenu [i - 1]); + else + SuperView.SetFocus (host.openMenu); + if (openSubMenu != null) { + SuperView.Remove (openSubMenu [i]); + openSubMenu.Remove (openSubMenu [i]); + } + RemoveSubMenu (i); + } + host.isMenuClosing = false; + } + + internal void CloseSubMenu () + { + host.isMenuClosing = true; + selectedSub = -1; + SetNeedsDisplay (); + RemoveAllOpensSubMenus (); + previousSubFocused?.SuperView?.SetFocus (previousSubFocused); + openSubMenu = null; + host.isMenuClosing = false; + } + + private void RemoveAllOpensSubMenus () + { + if (openSubMenu != null) { + foreach (var item in openSubMenu) { + SuperView.Remove (item); + } + } + } + + void PreviousMenu () + { + if (selectedSub > -1) { + selectedSub--; + RemoveSubMenu (selectedSub); + SetNeedsDisplay (); + } else + host.PreviousMenu (); + } + + void NextMenu () + { + if (host.UseKeysUpDownAsKeysLeftRight) + host.NextMenu (); + else { + if ((selectedSub == -1 || openSubMenu == null || openSubMenu?.Count == selectedSub) && barItems.Children [current].SubMenu == null) { + if (openSubMenu != null) + CloseSubMenu (); + host.NextMenu (); + } else + selectedSub++; + SetNeedsDisplay (); + CheckSubMenu (); + } + } + } + + + + /// + /// A menu bar for your application. + /// + public class MenuBar : View { + /// + /// The menus that were defined when the menubar was created. This can be updated if the menu is not currently visible. + /// + /// The menu array. + public MenuBarItem [] Menus { get; set; } + int selected; + Action action; + + /// + /// Used for change the navigation key style. + /// + public bool UseKeysUpDownAsKeysLeftRight { get; set; } = true; + + /// + /// Initializes a new instance of the class with the specified set of toplevel menu items. + /// + /// Individual menu items, if one of those contains a null, then a separator is drawn. + public MenuBar (MenuBarItem [] menus) : base () + { + X = 0; + Y = 0; + Width = Dim.Fill (); + Height = 1; + Menus = menus; + CanFocus = false; + selected = -1; + ColorScheme = Colors.Menu; + WantMousePositionReports = true; + } + + 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; + internal Menu openMenu; + View previousFocused; + internal bool isMenuOpening; + internal bool isMenuClosing; + + View lastFocused; + public View LastFocused { get; set; } + + void OpenMenu (int index) + { + isMenuOpening = true; + lastFocused = lastFocused ?? SuperView.MostFocused; + OnOpenMenu?.Invoke (this, null); + if (Menu.openSubMenu != null) + openMenu.CloseSubMenu (); + 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); + isMenuOpening = false; + } + + // 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 () + { + isMenuClosing = true; + selected = -1; + SetNeedsDisplay (); + SuperView.Remove (openMenu); + previousFocused?.SuperView?.SetFocus (previousFocused); + openMenu = null; + LastFocused = lastFocused; + lastFocused = null; + LastFocused?.SuperView?.SetFocus (LastFocused); + isMenuClosing = false; + } + + 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) { + 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 true; + } + } } - return false; + 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; - } - } - -} + 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 || + (me.Flags == MouseFlags.ReportMousePosition && selected > -1)) { + 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) { + if (selected == i && me.Flags == MouseFlags.Button1Clicked) { + CloseMenu (); + } else { + Activate (i); + } + return true; + } + pos += 2 + Menus [i].TitleLength + 1; + } + } + return false; + } + } + +}